原子类¶
在编程领域里,原子性意味着“一组操作要么全都操作成功,要么全都失败,不能只操作成功其中的一部分”。而java.util.concurrent.atomic下的类,就是具有原子性的类,可以原子性地执行添加、递增、递减等操作。比如之前多线程下的线程不安全的i++问题,到了原子类这里,就可以用功能相同且线程安全的getAndIncrement方法来优雅地解决。
原子类相比于锁,有一定的优势:
- 粒度更细:原子变量可以把竞争范围缩小到变量级别,通常情况下,锁的粒度都要大于原子变量的粒度。
- 效率更高:除了高度竞争的情况之外,使用原子类的效率通常会比使用同步互斥锁的效率更高,因为原子类底层利用了 CAS 操作,不会阻塞线程。
原子类分类¶
| 类型 | 具体类 |
|---|---|
| 基本类型 | AtomicInteger、AtomicLong、AtomicBoolean |
| 数组类型 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
| 引用类型 | AtomicReference、AtomicStampedReference、AtomicMarkableReference |
| 升级类型 | AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater |
| 累加器 | LongAdder、DoubleAdder |
| 积累器 | LongAccumulator、DoubleAccumulator |
Adder¶
Adder 和 Accumulator 都是 Java 8 引入的,是相对比较新的类。在高并发下 LongAdder 比 AtomicLong 效率更高,因为对于 AtomicLong 而言,它只适合用 于低并发场景,否则在高并发的场景下,由于 CAS 的冲突概率大,会导致经常自旋,影响整体效率。
而 LongAdder 引入了分段锁的概念,当竞争不激烈的时候,所有线程都是通过 CAS 对同一个 Base 变量进行修改,但是当竞争 激烈的时候,LongAdder 会把不同线程对应到不同的 Cell 上进行修改,降低了冲突的概率,从而提高了并发性。
Accumulator¶
那么Accumulator又是做什么的呢?Accumulator和Adder非常相似,实际上Accumulator就是一个更通用版本的Adder,比如LongAccumulator是LongAdder的功能增强版,因为LongAdder的API只有对数值的加减,而LongAccumulator提供了自定 义的函数操作。
接下来我们说一下 LongAccumulator 的适用场景。 第一点需要满足的条件,就是需要大量的计算,并且当需要并行计算的时候,我们可以考虑使用 LongAccumulator。
当计算量不大,或者串行计算就可以满足需求的时候,可以使用 for 循环;如果计算量大,需要提高计算的效率时,我们则可 以利用线程池,再加上 LongAccumulator 来配合的话,就可以达到并行计算的效果,效率非常高。 第二点需要满足的要求,就是计算的执行顺序并不关键,也就是说它不要求各个计算之间的执行顺序,也就是说线程 1 可能在 线程 5 之后执行,也可能在线程 5 之前执行,但是执行的先后并不影响最终的结果。 一些非常典型的满足这个条件的计算,就是类似于加法或者乘法,因为它们是有交换律的。同样,求最大值和最小值对于顺序 也是没有要求的,因为最终只会得出所有数字中的最大值或者最小值,无论先提交哪个或后提交哪个,都不会影响到最终的结 果。
原子类源码分析¶
以AtomicInteger为例,我们来看下AtomicInteger是如何通过CAS操作实现并发下的累加操作的,以其中一个重要方法getAndAdd方法为突破。
VALUE调用 Unsafe 的 objectFieldOffset 方法,从而得到当前这个原子类的 value 的偏移量,并 且赋给 valueOffset 变量,这样一来我们就获取到了 value 的偏移量,它的含义是在内存中的偏移地址,因为 Unsafe 就是根据 内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。
getAndAdd()调用了 unsafe.getAndAddInt 方法。
Unsafe 类¶
Unsafe 类主要是用于和操作系统打交道的,因为大部分的 Java 代码自身无法直接操作内存,所以在必要的时候,可以利用 Unsafe 类来和操作系统进行交互,CAS 正是利用到了 Unsafe 类。
- 第一个参数 object 就是将要操作的对象,传入的是 this,也就是 atomicInteger 这个对象本身;
- 第二个参数是 offset,也就是偏移量,借助它就可以获取到 value 的数值;
- 第三个参数 expectedValue,代表“期望值”,传入的是刚才获取到的 v;
- 而最后一个参数 newValue 是希望修改的数值 ,等于之前取到的数值 v 再加上 delta ,delta 就是我们希望原子类所改变的数值,比如可以传入 +1,也可以传入 -1。
所以 weakCompareAndSetInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 v 相等的话,那么就把计算出 来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。
一旦 CAS 操作成功,就会退出这个 while 循环,但是也有可能操作失败。如果操作失败就意味着在获取到 v 之后,并且在 CAS 操作之前,value 的数值已经发生变化了,证明有其他线程修改过这个变量。这样一来,就会再次执行循环体里面的代码,重新获取 v 的值,也就是获取最新的原子变量的数值,并且再次利用 CAS 去 尝试更新,直到更新成功为止。
原子类和 volatile 的使用场景¶
我们可以看出,volatile 和原子类的使用场景是不一样的,如果我们有一个可见性问题,那么可以使用 volatile 关键字,但如果我们的问题是一个组合操作,需要用同步来解决原子性问题的话,那么可以 使用原子变量,而不能使用 volatile 关键字。
通常情况下,volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了。
而对于会被多个线程同时操作的计数器 Counter 的场景,这种场景的一个典型特点就是,它不仅仅是一个简单的赋值操作,而是需要先读取当前的值,然后在此基础上进行一定的修改,再把它给赋值回 去。这样一来,我们的 volatile 就不足以保证这种情况的线程安全了。我们需要使用原子类来保证线程安全。
原子类和synchronized区别¶
-
原理不同:
- synchronized:在执行同步代码之前,需要首先获取到 monitor 锁,执行完毕后,再释放锁。
- 原子类:它保证线程安全的原理是利用了 CAS 操作。
-
使用范围的不同
- 原子类的使用范围是比较局限的。因为一个原子类仅仅是一个对象,不够灵活。
- 而 synchronized 的使用范围要广 泛得多。比如说 synchronized 既可以修饰一个方法,又可以修饰一段代码,相当于可以根据我们的需要,非常灵活地去控制它 的应用范围。
-
粒度的区别
- 原子变量的粒度是比较小的,它可以把竞争范围缩小到变量级别。
- 通常情况下,synchronized 锁的粒度都要大于原子变量的粒度。如果我们只把一行代码用 synchronized 给保护起来的话,有一点杀鸡焉用牛刀的感觉。
-
性能
因为 synchronized 是一种典型的悲观锁,而原子类恰恰相反,它利用的是乐观锁。所以,我们在比较 synchronized 和 AtomicInteger 的时候,其实也就相当于比较了悲观锁和乐观锁的区别。
从性能上来考虑的话,悲观锁的操作相对来讲是比较重量级的。因为 synchronized 在竞争激烈的情况下,会让拿不到锁的线程 阻塞,而原子类是永远不会让线程阻塞的。不过,虽然 synchronized 会让线程阻塞,但是这并不代表它的性能就比原子类差。
值得注意的是,synchronized 的性能随着 JDK 的升级,也得到了不断的优化。synchronized 会从无锁升级到偏向锁,再升级到 轻量级锁,最后才会升级到让线程阻塞的重量级锁。因此synchronized 在竞争不激烈的情况下,性能也是不错的,不需要“谈虎 色变”。