关于java:并发编程Atomic原子类

5次阅读

共计 11567 个字符,预计需要花费 29 分钟才能阅读完成。

1 Atomic 原子类简介

原子(atomic)本意是“不能被进一步宰割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。

即便是在多个线程一起执行的时候,一个操作一旦开始,就不会被其余线程烦扰。简而言之,原子类就是具备原子 / 原子操作特色的类,它们能无锁地防止原子性问题

并发包 java.util.concurrent 的原子类都寄存在 java.util.concurrent.atomic 下,如下图所示。

依据操作的数据类型,能够将 JUC 包中的原子类分为次要 5 类:

(1)根本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

(2)数组类型

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:援用类型数组原子类

(3)援用类型

  • AtomicReference:援用类型原子类
  • AtomicMarkableReference:原子更新带有标记的援用类型。
  • AtomicStampedReference:原子更新带有版本号的援用类型。

(4)对象的属性批改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新援用类型里的字段

(5)原子累加器

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

1.1 根本类型

根本类型原子类有三个:AtomicIntegerAtomicLongAtomicBoolean 。介于三个类提供的办法简直雷同,这里以整型原子类 AtomicInteger 为例来学习。

AtomicInteger 类罕用办法有:

办法 形容
get() 获取以后的值
getAndSet(int newValue) 获取以后的值,并设置新的值
getAndIncrement() 获取以后的值,并自增
getAndDecrement() 获取以后的值,并自减
getAndAdd(int delta) 获取以后的值,并加上预期的值
boolean compareAndSet(int expect, int update) 如果输出的数值等于预期值,则以原子形式将该值设置为输出值(update)
lazySet(int newValue) 最终设置为 newValue, 应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。

代码示例:

public class AtomicIntegerTest1 {public static void main(String[] args) {
        int temValue = 0;
        AtomicInteger i = new AtomicInteger(0);
        temValue = i.getAndSet(12);
        System.out.println("temValue:" + temValue + ";  i:" + i);
        temValue = i.getAndIncrement();
        System.out.println("temValue:" + temValue + ";  i:" + i);
        temValue = i.getAndAdd(-10);
        System.out.println("temValue:" + temValue + ";  i:" + i);
    }
}

执行后果:

temValue:0;  i:12
temValue:12;  i:13
temValue:13;  i:3

1.2 数组类型

数组类型原子类有三个:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray 。介于三个类提供的办法简直雷同,这里以 AtomicIntegerArray 为例来学习。

AtomicIntegerArray 罕用办法

办法 形容
get(int i) 获取 index=i 地位元素的值
getAndSet(int i, int newValue) 返回 index=i 地位的以后的值,并将其设置为新值:newValue
getAndIncrement(int i) 获取 index=i 地位元素的值,并让该地位的元素自增
getAndDecrement(int i) 获取 index=i 地位元素的值,并让该地位的元素自减
getAndAdd(int i, int delta) 获取 index=i 地位元素的值,并加上预期的值
compareAndSet(int i, int expect, int update) 如果输出的数值等于预期值,则以原子形式将 index=i 地位的元素值设置为输出值(update)
lazySet(int i, int newValue) 最终将 index=i 地位的元素设置为 newValue, 应用 lazySet 设置之后可能导致其余线程在之后的一小段时间内还是能够读到旧的值。

代码示例:

public class AtomicIntegerArrayTest {public static void main(String[] args) {
        int temValue = 0;
        int[] nums = {-2, -1, 1, 2, 3, 4};
        AtomicIntegerArray i = new AtomicIntegerArray(nums);
        for (int j = 0; j < nums.length; j++) {System.out.println(i.get(j));
        }
        temValue = i.getAndSet(0, 2);
        System.out.println("temValue:" + temValue + ";  i:" + i);
        temValue = i.getAndIncrement(0);
        System.out.println("temValue:" + temValue + ";  i:" + i);
        temValue = i.getAndAdd(0, 5);
        System.out.println("temValue:" + temValue + ";  i:" + i);
    }
}

执行后果:

-2
-1
1
2
3
4
temValue:-2;  i:[2, -1, 1, 2, 3, 4]
temValue:2;  i:[3, -1, 1, 2, 3, 4]
temValue:3;  i:[8, -1, 1, 2, 3, 4]

Process finished with exit code 0

1.3 对象的属性批改类型

对象的属性批改类型原子类有 AtomicIntegerFieldUpdaterAtomicLongFieldUpdaterAtomicReferenceFieldUpdater 三个。

介于三个类提供的办法简直雷同,这里以 AtomicIntegerFieldUpdater 为例来学习。

子地更新对象的属性须要两步:

  • 因为对象的属性批改类型原子类都是抽象类,所以每次应用都必须应用静态方法 newUpdater() 创立一个更新器,并且须要设置想要更新的类和属性。
  • 更新的对象属性必须应用public volatile 修饰符

AtomicIntegerFieldUpdater类应用示例

public class AtomicIntegerFieldUpdaterTest {
    public class AtomicIntegerFieldUpdaterTest {public static void main(String[] args) {AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

        User user = new User("ZhangSan", 20);
        System.out.println(a.getAndIncrement(user));
        System.out.println(a.get(user));
    }
}

class User {
    private String name;
    public volatile int age;

    public User(String name, int age) {super();
        this.name = name;
        this.age = age;
    }

    public String getName() {return name;}

    public void setName(String name) {this.name = name;}

    public int getAge() {return age;}

    public void setAge(int age) {this.age = age;}
}

输入后果:

20
21

1.4 援用类型

根本类型原子类只能更新一个变量,如果须要原子 更新多个变量 ,须要应用援用类型原子类。援用类型原子类分为AtomicReferenceAtomicStampedReferenceAtomicMarkableReference 三类。

  • AtomicReference:援用类型原子类
  • AtomicStampedReference:原子更新带有版本号的援用类型。该类将整数值与援用关联起来,可用于解决原子的更新数据和数据的版本号,能够解决应用 CAS 进行原子更新时可能呈现的 ABA 问题
  • AtomicMarkableReference:原子更新带有标记的援用类型。该类将 boolean 标记与援用关联起来。

(1)AtomicReference 类 应用示例:

public class AtomicReferenceTest {public static void main(String[] args) {AtomicReference<Person> personAtomicReference = new AtomicReference<>();
        Person person = new Person("Zhangsan", 20);
        personAtomicReference.set(person);
        Person updatePerson = new Person("LiSi", 30);
        personAtomicReference.compareAndSet(person, updatePerson);

        System.out.println(personAtomicReference.get().getName());
        System.out.println(personAtomicReference.get().getAge());
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {super();
        this.name = name;
        this.age = age;
    }

    public String getName() {return name;}

    public void setName(String name) {this.name = name;}

    public int getAge() {return age;}

    public void setAge(int age) {this.age = age;}
}

上述代码首先创立了一个 Person 对象,而后把 Person 对象设置进 AtomicReference 对象中,而后调用 compareAndSet 办法,该办法就是通过 CAS 操作设置 personAtomicReference。如果 personAtomicReference 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 办法雷同。

输入后果:

LiSi
30

(2) AtomicMarkableReference类应用示例

AtomicMarkableReference 是将一个 boolean 值作是否有更改的标记,实质就是它的版本号只有两个,true 和 false,批改的时候在这两个版本号之间来回切换,这样做并不能解决 ABA 的问题,只是会升高 ABA 问题产生的几率。

public class SolveABAByAtomicMarkableReference {private static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false);

    public static void main(String[] args) {Thread refT1 = new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            atomicMarkableReference.compareAndSet(100, 101, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
            atomicMarkableReference.compareAndSet(101, 100, atomicMarkableReference.isMarked(), !atomicMarkableReference.isMarked());
        });

        Thread refT2 = new Thread(() -> {boolean marked = atomicMarkableReference.isMarked();
            try {TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            boolean c3 = atomicMarkableReference.compareAndSet(100, 101, marked, !marked);
            System.out.println(c3); // 返回 true, 理论应该返回 false
        });

        refT1.start();
        refT2.start();}
}

输入后果:

true

1.5 原子累加器

原子类型累加器 JDK1.8引进的并发新技术,它能够看做 AtomicLongAtomicDouble的局部增强类型。

原子类型累加器的用法及原理能够参考 Java 多线程进阶(十七)—— J.U.C 之 atomic 框架:LongAdder 一文。

2 原子操作实现原理

在 Java 中能够通过 循环 CAS的形式来实现原子操作。

2.1 简析 CAS

CAS 全称 Compare And Swap(比拟与替换),是一种无锁算法。在不应用锁(没有线程被阻塞)的状况下实现多线程之间的 变量同步

CAS 是 原子性 的操作 (读和写两者同时具备原子性),其实现形式是通过借助C/C++ 调用 CPU 指令实现的,效率很高。

CAS 算法波及到三个操作数:

  • V 内存地址寄存的理论值
  • A 比拟的旧值
  • B 更新的新值

当且仅当 V 的值等于 A 时(旧值和内存中理论的值雷同),表明旧值 A 曾经是目前最新版本的值,自然而然能够将新值 N 赋值给 V。反之则表明 V 和 A 变量不同步,间接返回 V 即可。当多个线程应用 CAS 操作一个变量时,只有一个线程会更新胜利,其余失败的线程会从新尝试。也就是说,“更新”是一个一直重试的操作。

上面以根本原子类 AtomicInteger 为例,来了解原子操作的实现原理。

查看 AtomicInteger 源码:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 获取并操作内存的数据。private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 存储 value 在 AtomicInteger 中的偏移量。private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {throw new Error(ex); }
    }
    
    // 存储 AtomicInteger 的 int 值,该属性须要借助 volatile 关键字保障其在线程间是可见的。private volatile int value;

接下来,查看 AtomicInteger 的自增函数 incrementAndGet()的源码时,发现自增函数底层调用的是 unsafe.getAndAddInt()。

    // AtomicInteger 自增办法
    public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }    
------------------------------------------------------------------------
    // Unsafe.class
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

Unsafe还有很多个 CAS 操作的相干办法,比方:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

--- omit ---

这些函数是是 CAS 缩写的由来。

还是以 compareAndSwapInt 为例,查看 OpenJDK 8 中 Unsafe.cpp 的源码:

// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}

依据 OpenJDK 8 的源码咱们能够看出,getAndAddInt()循环获取给定对象 o 中的偏移量处的值 v,而后判断内存值是否等于 v。如果相等则将内存值设置为 v + delta,否则返回 false,持续循环进行重试,直到设置胜利能力退出循环,并且将旧值返回。整个“比拟 + 更新”操作封装在 compareAndSwapInt()中,在 JNI 里是借助于一个 CPU 指令实现的,属于原子操作,能够保障多个线程都可能看到同一个变量的批改值。

后续 JDK 通过 CPU 的cmpxchg 指令,去比拟寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。而后通过 Java 代码中的while 循环调用 cmpxchg 指令进行重试,直到设置胜利为止

2.2 CAS 实现原子操作的三大问题

CAS 尽管很高效地解决了原子操作,然而 CAS 依然存在三大问题。ABA 问题,循环工夫长开销大,以及只能保障一个共享变量的原子操作。

2.2.1 ABA 问题

CAS 须要在操作值的时候去查看内存中的值是否发生变化,没有发生变化才会更新内存值。然而如果一个值原来是 A,变成了 B,又变成了 A,那么应用 CAS 进行查看时会发现它的值没有发生变化,然而实际上却变动了。这就是一个典型的 ABA 问题。

代码示例:

@Slf4j(topic = "AtomicReferenceTest")
public class AtomicReferenceTest {public static void main(String[] args) {Person person1 = new Person("ZhangSan");
        Person person2 = new Person("LiSi");
        Person person3 = new Person("WangWu");
        AtomicReference<Person> atomicReference = new AtomicReference<>(person1);
        log.info("success?" + atomicReference.compareAndSet(person1, person2));
        log.info("success?" + atomicReference.compareAndSet(person2, person1));
        log.info("success?" + atomicReference.compareAndSet(person1, person3));
    }
}

class Person {
    private String name;

    public Person(String name) {this.name = name;}

    public String getName() {return name;}

    public void setName(String name) {this.name = name;}

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}

输入后果:

// 三个 CAS 的后果都是 true。阐明 CAS 只是比拟的两者的值是否相等,对其余内容的变动并不关怀。11:17:13.359 [main] INFO AtomicReferenceTest - success? true
11:17:13.366 [main] INFO AtomicReferenceTest - success? true
11:17:13.366 [main] INFO AtomicReferenceTest - success? true

ABA 问题的解决思路就是在变量后面 增加版本号,每次变量更新的时候都把版本号加 1,这样变动过程就从“A-B-A”变成了“1A-2B-3A”。

JDK 从 1.5 开始提供了 带版本号的援用类型 AtomicStampedReference 类 来解决 ABA 问题,具体操作封装在 compareAndSet()中。compareAndSet()首先查看以后援用和以后标记与预期援用和预期标记是否相等,如果都相等,则以原子形式将援用值和标记的值设置为给定的更新值。

    public boolean compareAndSet(V   expectedReference,        // 预期援用
                                 V   newReference,            // 更新后援用
                                 int expectedStamp,            // 预期标记
                                 int newStamp) {            // 更新后标记
        --- omit ---
    }

下面代码批改为:

        AtomicStampedReference<Person> atomicStampedReference = new AtomicStampedReference(person1, 0);
        log.info("success?" + atomicStampedReference.compareAndSet(person1, person2, 0, 1));
        log.info("success?" + atomicStampedReference.compareAndSet(person2, person1, 1, 2));
        log.info("success?" + atomicStampedReference.compareAndSet(person1, person3, 0, 1));
        log.info("success?" + atomicStampedReference.compareAndSet(person1, person3, 2, 3));

运行后果:

11:19:37.839 [main] INFO AtomicReferenceTest - success? true
11:19:37.846 [main] INFO AtomicReferenceTest - success? true
11:19:37.846 [main] INFO AtomicReferenceTest - success? false    // 不是预期版本,CAS 失败
11:19:37.846 [main] INFO AtomicReferenceTest - success? true

2.2.2 循环工夫长开销大

CAS 操作如果长时间不胜利,会导致其始终自旋,给 CPU 带来十分大的开销。

如果 JVM 能反对处理器提供的pause 指令,那么效率会有肯定的晋升。pause 指令有两个作用:第一,它能够提早流水线执行指令(de-pipeline),使 CPU 不会耗费过多的执行资源,提早的工夫取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它能够防止在退出循环的时候因内存程序抵触(MemoryOrder Violation)而引起 CPU 流水线被清空(CPU Pipeline Flush),从而进步 CPU 的执行效率。

2.2.3 只能保障一个共享变量的原子操作

对一个共享变量执行操作时,CAS 可能保障原子操作,然而对多个共享变量操作时,CAS 是无奈保障操作的原子性的。

多个共享变量操作时,个别都用锁解决。

但 Java 从 1.5 开始 JDK 提供了 援用类型 AtomicReference 类 来保障援用对象之间的原子性,能够把多个变量放在一个对象里来进行 CAS 操作。

2.3 CAS 利用

(1)乐观锁

乐观锁 总是假如最坏的状况,每次去操作数据时候都认为会被的线程批改数据,所以在每次操作数据的时候都会给数据加锁 ,让别的线程无奈操作这个数据,别的线程会始终阻塞直到获取到这个数据的锁。传统的关系型数据库里边就用到了很多这种锁机制,比方行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中synchronizedReentrantLock等独占锁就是乐观锁思维的实现。这样的话就会影响效率,比方当有个线程产生一个很耗时的操作的时候,别的线程只是想获取这个数据的值而已都要期待很久。

(2)乐观锁

乐观锁 总是假如最好的状况,每次去操作数据都认为不会被别的线程批改数据,所以在每次操作数据的时候都不会给数据加锁 ,即在线程对数据进行操作的时候, 别的线程不会阻塞 依然能够对数据进行操作,只有在须要更新数据的时候才会去判断数据是否被别的线程批改过,如果数据被批改过则会回绝操作并且返回错误信息给用户。乐观锁实用于多读的利用类型,这样能够进步吞吐量 ,像数据库提供的相似于write_condition 机制,其实都是提供的乐观锁。在 Java 中java.util.concurrent.atomic 包上面的原子变量类就是应用了乐观锁的一种实现形式 CAS 实现的。

光说概念有些形象,看下乐观锁和乐观锁的调用形式简略示例:

// ------------------------- 乐观锁的调用形式 -------------------------
// synchronized
public synchronized void testMethod() {// 操作同步资源}
// ReentrantLock
private ReentrantLock lock = new ReentrantLock(); // 须要保障多个线程应用的是同一个锁
public void modifyPublicResources() {lock.lock();
    // 操作同步资源
    lock.unlock();}

// ------------------------- 乐观锁的调用形式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger();  // 须要保障多个线程应用的是同一个 AtomicInteger
atomicInteger.incrementAndGet(); // 执行自增 1 

参考

  • 《Java 并发编程的艺术》
  • Java 中的无锁编程
  • Atomic 原子类总结
  • ABA 问题的实质及其解决办法
  • Java 多线程进阶(十七)—— J.U.C 之 atomic 框架:LongAdder
正文完
 0