学了这么久的高并发编程,连 Java 中的并发原子类都不知道?
本文分享自华为云社区《学了这么久的高并发编程,连Java中的并发原子类都不知道?这也太Low了吧》,作者:冰 河。
今天我们一起来聊聊 Java 中的并发原子类。在 java.util.concurrent.atomic 包下有很多支持并发的原子类,某种程度上,我们可以将其分成:基本数据类型的原子类、对象引用类型的原子类、数组类型的原子类、对象属性类型的原子类和累加器类型的原子类 五大类。
接下来,我们就一起来看看这些并发原子类吧。
基本数据类型的原子类
基本数据类型的原子类包含:AtomicBoolean、AtomicInteger 和 AtomicLong。
打开这些原子类的源码,我们可以发现,这些原子类在使用上还是非常简单的,主要提供了如下这些比较常用的方法。
原子化加 1 或减 1 操作
原子化增加指定的值
CAS 操作
接收函数计算结果
对象引用类型的原子类
对象引用类型的原子类包含:AtomicReference、AtomicStampedReference 和 AtomicMarkableReference。
利用这些对象引用类型的原子类,可以实现对象引用更新的原子化。AtomicReference 提供的原子化更新操作与基本数据类型的原子类提供的更新操作差不多,只不过 AtomicReference 提供的原子化操作常用于更新对象信息。这里不再赘述。
需要特别注意的是:使用对象引用类型的原子类,要重点关注 ABA 问题。
关于 ABA 问题,文章的最后部分会说明。
好在 AtomicStampedReference 和 AtomicMarkableReference 这两个原子类解决了 ABA 问题。
AtomicStampedReference 类中的 compareAndSet 的方法签名如下所示。
可以看到,AtomicStampedReference 类解决 ABA 问题的方案与乐观锁的机制比较相似,实现的 CAS 方法增加了版本号。只有 expectedReference 的值与内存中的引用值相等,并且 expectedStamp 版本号与内存中的版本号相同时,才会将内存中的引用值更新为 newReference,同时将内存中的版本号更新为 newStamp。
AtomicMarkableReference 类中的 compareAndSet 的方法签名如下所示。
可以看到,AtomicMarkableReference 解决 ABA 问题的方案就更简单了,在 compareAndSet 方法中,新增了 boolean 类型的校验值。这些理解起来也比较简单,这里,我也不再赘述了。
对象属性类型的原子类
对象属性类型的原子类包含:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater。
利用对象属性类型的原子类可以原子化的更新对象的属性。值得一提的是,这三个类的对象都是通过反射的方式生成的,如下是三个类的 newUpdater()方法。
这里,我们不难看出,在 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater 三个类的 newUpdater()方法中,只有传递的 Class 信息,并没有传递对象的引用信息。如果要更新对象的属性,则一定要使用对象的引用,那对象的引用是在哪里传递的呢?
其实,对象的引用是在真正调用原子操作的方法时传入的。这里,我们就以 compareAndSet()方法为例,如下所示。
可以看到,原子化的操作方法仅仅是多了一个对象的引用,使用起来也非常简单,这里,我就不再赘述了。
另外,需要注意的是:使用 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater 更新对象的属性时,对象属性必须是 volatile 类型的,只有这样才能保证可见性;如果对象属性不是 volatile 类型的,newUpdater()方法会抛出 IllegalArgumentException 这个运行时异常。
数组类型的原子类
数组类型的原子类包含:AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray。
利用数组类型的原子类可以原子化的更新数组里面的每一个元素,使用起来也非常简单,数组类型的原子类提供的原子化方法仅仅是在基本数据类型的原子类和对象引用类型的原子类提供的原子化方法的基础上增加了一个数组的索引参数。
例如,我们以 compareAndSet()方法为例,如下所示。
可以看到,原子化的操作方法仅仅是对多了一个数组的下标,使用起来也非常简单,这里,我就不再赘述了。
累加器类型的原子类
累加器类型的原子类包含:DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder。
累加器类型的原子类就比较简单了:仅仅支持值的累加操作,不支持 compareAndSet()方法。对于值的累加操作,比基本数据类型的原子类速度更快,性能更好。
使用原子类实现 count+1
在并发编程领域,一个经典的问题就是 count+1 问题。也就是在高并发环境下,如何保证 count+1 的正确性。一种方案就是在临界区加锁来保护共享变量 count,但是这种方式太消耗性能了。
如果使用 Java 提供的原子类来解决高并发环境下 count+的问题,则性能会大幅度提升。
简单的示例代码如下所示。
可以看到,原子类实现 count+1 问题,既没有使用 synchronized 锁,也没有使用 Lock 锁。
从本质上讲,它使用的是无锁或者是乐观锁方案解决的 count+问题,说的具体一点就是 CAS 操作。
CAS 原理
CAS 操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。如果内存位置与预期原值的 A 相匹配,那么将内存位置的值更新为新值 B。
如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。
无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)
简单点理解就是:位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只返回位置 V 现在的值。这其实和乐观锁的冲突检测+数据更新的原理是一样的。
ABA 问题
因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加 1,那么 A-B-A 就会变成 1A-2B-3A。
从 Java1.5 开始 JDK 的 atomic 包里提供的 AtomicStampedReference 类和 AtomicMarkableReference 类能够解决 CAS 的 ABA 问题。
关于 AtomicStampedReference 类和 AtomicMarkableReference 类前文有描述,这里不再赘述。
版权声明: 本文为 InfoQ 作者【华为云开发者联盟】的原创文章。
原文链接:【http://xie.infoq.cn/article/eaddac674a31b846c2c62af4a】。文章转载请联系作者。
评论