原子类:无锁工具类的典范
- 对于简单的原子性问题,还有一种无锁方案。Java SDK 并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。
- 无锁方案相对互斥锁方案,最大的好处就是性能。
- 互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。
- 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。
无锁方案的实现原理
- 其实原子类性能高的秘密很简单,硬件支持而已。
- CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。
- CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;
- 并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。
- 作为⼀条 CPU 指令,CAS 指令本身是能够保证原子性的。
- 使用 CAS 来解决并发问题,⼀般都会伴随着自旋,而所谓自旋,其实就是循环尝试。
看 Java 如何实现原子化的 count += 1
- 在 Java 1.8 版本中,getAndIncrement() 方法会转调 unsafe.getAndAddLong() 方法。这里 this 和 valueOffset 两个参数可以唯一确定共享变量的内存地址。
final long getAndIncrement() {return unsafe.getAndAddLong(this, valueOffset, 1L); }
- unsafe.getAndAddLong() 方法首先会在内存中读取共享变量的值,之后循环调用 compareAndSwapLong() 方法来尝试设置共享变量的值,直到成功为止。
- compareAndSwapLong() 是⼀个 native 方法,只有当内存中共享变量的值等于 expected 时,才会将共享变量的值更新为 x,并且返回 true;否则返回 fasle。
- compareAndSwapLong 的语义和 CAS 指令的语义的差别仅仅是返回值不同而已。
- Java 提供的原子类里面 CAS 一般被实现为 compareAndSet(),compareAndSet() 的语义和 CAS 指令的语义的差别仅仅是返回值不同而已,compareAndSet() 里面如果更新成功,则会返回 true,否则返回 false。
原子类概览
- 原子化的基本数据类型
- 相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong。
- 原子化的对象引用类型
- 相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。
- 对象引用的更新需要重点关注 ABA 问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。
- 解决 ABA 问题的思路其实很简单,增加一个版本号维度就可以了,每次执行 CAS 操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来(版本号递增的)。
- 原子化数组
- 相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用这些原子类,我们可以原子化地更新数组里面的每一个元素。
- 这些类提供的方法和原子化的基本数据类型的区别仅仅是:每个方法多了一个数组的索引参数。
- 原子化对象属性更新器
- 相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和
AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的。 - 对象属性必须是 volatile 类型的,只有这样才能保证可见性;
- 如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。
- 相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和
- 原子化的累加器
- DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。
- 如果你仅仅需要累加操作,使⽤原子化的累加器性能会更好。