java原子类详解
原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。
作用和锁有类似之处,是为了保证并发情况下的线程安全。
粒度更细
原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度也大于原子变量的粒度
效率更高
除了在高并发之外,使用原子类的效率往往比使用同步互斥锁的效率更高,因为原子类底层利用了CAS,不会阻塞线程。
类型 具体类型
Atomic* 基本类型原子类 AtomicInteger、AtomicLong、AtomicBoolean
Atomic*Array 数组类型原子类 AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
Atomic*Reference 引用类型原子类 AtomicReference
Atomic*FieldUpdater 升级类型原子类 AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
Adder 累加器 LongAdder、DoubleAdder
Accumulator 积累器 LongAccumulator、DoubleAccumulator
上面列举了J.U.C中提供的一些原子操作类,接下来从简单的AtomicInteger开始分析,来看看它的常用方法,其他的两种AtomicLong、AtomicBoolean和它相似
方法 作用
public final int get() 获取当前的值
public final int getAndSet(int newValue) 获取当前的值,并设置新的值
public final int getAndIncrement() 获取当前的值,并自增+1
public final int getAndDecrement() 获取当前的值,并自减-1
public final int getAndAdd(int delta) 获取当前的值,并加上预期的值。
boolean compareAndSet(int expect, int update) 如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update)
AtomicArray 数组类型原子类,数组里的元素,都可以保证其原子性,比如 AtomicIntegerArray 相当于把 AtomicInteger 聚合起来,组合成一个数组。我们如果想用一个每一个元素都具备原子性的数组的话, 就可以使用 AtomicArray
该类包括:
类名 作用
AtomicIntegerArray 整形数组原子类
AtomicLongArray 长整形数组原子类
AtomicReferenceArray 引用类型数组原子类
Atomic*Reference 引用类型原子类
AtomicReference引用类型原子类,作用和AtomicInteger没有本质区别,AtomicReference是让一个对象保持原子性,而不局限一个变量。
类名 作用
AtomicReference 保证对象的原子性
AtomicStampedReference 它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。
AtomicMarkableReference 和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。
原子类更新器主要用于对已经声明的非原子变量,为它增加原子性,让该变量拥有CAS操作的能力。
该种类所有类型:
类名 作用
AtomicIntegerFieldUpdater 原子更新整形的更新器
AtomicLongFieldUpdater 原子更新长整形的更新器
AtomicReferenceFieldUpdater 原子更新引用的更新器
- public class AtomicIntegerFieldUpdaterDemo implements Runnable {
-
- static Score useUpdaterScore;
- static Score unusedComputerScore;
-
- //声明原子更新类,泛型为Score类,更新的是"score"字段
- public static AtomicIntegerFieldUpdater<Score> scoreUpdater = AtomicIntegerFieldUpdater
- .newUpdater(Score.class, "score");
-
- @Override
- public void run() {
- for (int i = 0; i < 1000; i++) {
- //非原子操作直接自增
- unusedComputerScore.score++;
- //使用更新器更新字段,同样执行自增
- scoreUpdater.getAndIncrement(useUpdaterScore);
- }
- }
-
- public static class Score {
- volatile int score;
- }
-
- public static void main(String[] args) throws InterruptedException {
- useUpdaterScore = new Score();
- unusedComputerScore = new Score();
- AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
- Thread t1 = new Thread(r);
- Thread t2 = new Thread(r);
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println("普通变量的结果:" + unusedComputerScore.score);
- System.out.println("升级后的结果:" + useUpdaterScore.score);
- }
- }
输出结果:
普通变量的结果:1980
升级后的结果:2000
从结果可以看出,升级后结果是期望值。
使用场景(相对于直接使用AtomicInteger)
历史原因
如果当前变量在之前开发版本中使用地方过多,这个时候为了不做大量改造,可以在需要原子性的时候,使用原子更新器
少部分情况需要原子性的时候
由于原子类型的变量比普通变量更耗资源。在大部分不需要原子性的时候,如果都设置成原子类型,这非常的耗资源。因此,我们可以再需要原子性的少数情况下,对当前变量使用AtomicIntegerFieldUpdater进行合理的升级。
- public final int getAndAdd(int delta) {
- return unsafe.getAndAddInt(this, valueOffset, delta);
- }
是Unsafe类
该方法实际调用的是,unsafe.getAndAddInt(this, VALUE, delta),这里先介绍一下Unsafe类。
Unsafe类
Unsafe是CAS的核心类。由于java无法直接访问底层操作系统,而是需要通过本地方法来实现。JVM还是提供了Unsafe类,他提供了硬件层面的原子操作,可以直接操作内存的数据。
- public class AtomicInteger extends Number implements java.io.Serializable {
- private static final long serialVersionUID = 6214790243416807050L;
-
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- private static final long valueOffset;
- private volatile int value;
-
- static {
- try {
- valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
- } catch (Exception var1) {
- throw new Error(var1);
- }
- }
-
- private volatile int value;
-
- public final int get() {
- return value;
- }
- ......
- }
-
- 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类型变量unsafe,并定义变量valueOffset,然后在静态代码块中value值在内存中的偏移地址,赋值给valueOffset变量。因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。
value是用volatile修饰的,他就是我们原子类存储值的变量,由于使用volatile修饰,所以在多线程中看到的value值都是同一份,保证了可见性。
接下来看实际执行cas的地方。Unsafe的getAndAddInt()方法
先看do-while循环,它是一个无限循环,知道满足条件才退出循环。do中的代码时,获取到当前内存中的值赋值给var5,var1是当前原子对象,var2是value在内存中的偏移量。此时var5就是此时原子类的数值。
再看看while中的退出条件。compareAndSwapInt这个方法参数,他们的实际意义是:
第一个参数 --> 当前原子类对象,即AtomicInteger 这个对象本身
第二个参数 --> 当前值的内存偏移量,接触它可以获取到value的值
第三个参数 --> 当前值,如果当前值不匹配,更新失败会返回false,开始下次循环。如果当前值匹配,同时更新成功,方法返回true,跳出循环,完成当前累加操作
第四个参数 --> 期望值,即当前值加上累加的值。
所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5 相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。
总结一下,Unsafe 的 getAndAddInt 方法是通过循环 + CAS 的方式来实现的,在此过程中,它会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败就重新获取,然后再次尝试更新,直到更新成功。
在并发情况下,如果我们需要实现计数器(例如下载任务数),可以利用AtomicInteger和AtomicLong,这样一来可以避免加锁和复杂的代码逻辑。虽然它们好用,但是也存在着一些问题。
- public class AtomicLongDemo {
-
- public static void main(String[] args) throws InterruptedException {
- AtomicLong counter = new AtomicLong(0);
- ExecutorService service = Executors.newFixedThreadPool(16);
- for (int i = 0; i < 100; i++) {
- service.submit(new Task(counter));
- }
-
- Thread.sleep(2000);
- System.out.println(counter.get());
- }
-
- static class Task implements Runnable {
-
- private final AtomicLong counter;
-
- public Task(AtomicLong counter) {
- this.counter = counter;
- }
-
- @Override
- public void run() {
- counter.incrementAndGet();
- }
- }
- }
在这里,定义了一个为0的AtomicLong变量counter,然后创建了线程池数为16的线程池。然后执行100个任务。然后任务是执行counter.incrementAndGet()。结果毫无疑问是100,虽然并发执行,但是AtomicLong依然能保证incrementAndGet是一个原子操作,所以不会发生线程安全问题。
不过我们仔细看细节,对于 AtomicLong 内部的 value 属性而言,也就是保存当前 AtomicLong 数值的属性,它是被 volatile 修饰的,所以它需要保证自身可见性。
当线程1执行incrementAndGet操作更新成功后,会将值向主内存中修改,同时主内存会将其他线程的value给修改掉,而且CAS也会经常失败,这两个操作是非常耗资源的。
在JDK 8中新增了LongAdder 类,来优化这一个问题。
我们将上面例子中做小小的调整,将AtomicLong换成LongAdder,get()方法换成sum(),incrementAndGet()换成increment()
- public class LongAdderDemo {
-
- public static void main(String[] args) throws InterruptedException {
- LongAdder counter = new LongAdder();
- ExecutorService service = Executors.newFixedThreadPool(16);
- for (int i = 0; i < 100; i++) {
- service.submit(new Task(counter));
- }
-
- Thread.sleep(2000);
- System.out.println(counter.sum());
- }
- static class Task implements Runnable {
-
- private final LongAdder counter;
-
- public Task(LongAdder counter) {
- this.counter = counter;
- }
-
- @Override
- public void run() {
- counter.increment();
- }
- }
- }
行得到的结果还是100,但是运行速度比刚才AtomicLong实现要快。
- //集成自Striped64
- public class LongAdder extends Striped64 implements Serializable {}
-
-
- abstract class Striped64 extends Number {
- ......
- /**
- * 单元格表。如果为非null,则大小为2的幂。
- */
- transient volatile Cell[] cells;
-
- /**
- * 基本值,主要在没有争用时使用,也用作表初始化过程中的回退。通过CAS更新。
- */
- transient volatile long base;
-
- ......
- }
LongAdder中引入了分段累加的概念,内部的Cell[]数组和base变量都参与了计数
其中base在竞争不激烈的情况下,直接把累加的结果改到base变量上;
在竞争激烈的时候,各个线程会分散累加到自己所对应的Cell[] 数组的某一个对象中,而不是公用一个。LongAdder这样分段的思想将不同线程到不同Cell上的修改,避免了大量的冲突,提升了并发性。更JDK 7 中的ConcurrentHashMap思想相似。
竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中降低了冲突的概率。这个思想的本质就是空间换时间,所以会耗费更多的内存。
最终的结果是通过LongAdder#sum()方法来获取的,将各个Cell值累计求和,再加上base返回。
- public long sum() {
- Cell[] as = cells;
- long sum = base;
- if (as != null) {
- for (Cell a : as)
- if (a != null)
- sum += a.value;
- }
- return sum;
- }
-
在低竞争的情况下,AtomicLong和LongAdder的性能相似;但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,经过试验,LongAdder 的吞吐量大约是 AtomicLong 的十倍,虽然性能提高了,但是LongAdder会耗费更多的空间
LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一
而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。
小结
如果我们的场景仅仅是需要用到加和减操作的话,那么可以直接使用更高效的 LongAdder,但如果我们需要利用 CAS 比如 compareAndSet 等操作的话,就需要使用 AtomicLong 来完成。
volatile只保证可见性
volatile只保证了可见性,而不能保证原子性。例如主内存中volatile int a=0,在两个线程中时,如果都同时获取当前的volatile修饰的变量值,但是都执行自增操作,由于两个线程都在之前获取了值即a=0,两线程都执行自增后都是a=1了,这是两个线程同时更新主内存中的a,最后得到的结果是a=1,这个就和预期值不一样,由于并没有保证获取和赋值这个操作的原子性,会有线程安全问题。
原子类保证了可见性和原子性
上面的例子可以使用原子类AtomicInteger来修复,将自增操作换成incrementAndGet(),底层通过CPU指令保证原子性,解决线程安全问题
使用场景
volatile 通常情况下,volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了
原子类 对于先获取值,然后做一定修改,再赋值回去的操作,就需要原子类来保证原子性
相同点
都能保证线程安全
不同点
AtomicInteger 使用CAS来保证线程的原子性 仅一个对象,少数场景使用 竞争在变量级别 乐观锁,低并发时性能好
synchronized 使用monitor锁来保证线程安全性。再执行同步代码之前,需要先获取到monitor锁,执行完毕,释放锁。 既可以修饰一个方法,又可以修饰一段代码,可以根据我们的需要,非常灵活地去控制它的应用范围 通常会大于变量级别 悲观锁(jdk6之后锁升级优化后,低并发情况性能也不错),在高并发时由于AtomicInteger
上面讲了Adder是通过CAS加分段思想来提高Atomic*的性能。
而LongAccumulator是LongAdder的功能增强版,LongAccumulator在LongAdder只有数值加减的基础上提供自定义的函数操作。
- public class LongAccumulatorDemo {
-
- public static void main(String[] args) throws InterruptedException {
- LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);
- ExecutorService executor = Executors.newFixedThreadPool(8);
- IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));
-
- Thread.sleep(2000);
- System.out.println(accumulator.getThenReset());
- }
- }
-
上面是使用8线程数的线程池累加0-9。最后获取到结果为45.
x是上一次的结果,y是传入的新值。由于使用多线程,当前的方法只适用于即使执行顺序不同,结果依然一样的情况,即交 换 性 \color{red}交换性交换性。
下面几种场景适合使用:
相加
相乘
最大值、最小值