原子类
阿里巴巴2021版JDK源码笔记(2月第三版).pdf
链接:https://pan.baidu.com/s/1XhVcfbGTpU83snOZVu8AXg
提取码:l3gy
concurrent包的结构层次:Atomic类 -> 锁与条件 -> 同步工具 -> 并发容器 -> 编程池 -> CpmpletableFuture
1. AtomicInteger和AtomicLong
1.1 源码及其原理
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
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;
}
查看源码可知,这里主要是通过CAS(compareAndSwap)实现
1.2 悲观锁和乐观锁
-
悲观锁:悲观,开发人员认为数据发生并发冲突的概率很大,在读操作之前就上锁(正常都是读后写)
-
乐关锁:乐观,开发人员认为数据发生并发冲突的概率比较小,读操作不上锁,写操作才上锁,判断数据在此期间是否被其 他线程修改了,CAS就是典型的乐观锁模式(判断数据是否被修改,同时写回新值,这两个操作要合成一个原子操作,也就是CAS)
1.3 Unsafe的CAS详解
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
1.4 自旋和阻塞
- 自旋: 当程序拿不到锁时,不放弃CPU,空转,不断重试
- 阻塞:当程序拿不到锁时,放弃CPU,进入阻塞状态,等待后续被唤醒
就单核CPU而言,不能使用自旋,自旋会造成其他线程无法使用,对多核CPU而言,自旋可以减少线程切换的开销,节省资源
自旋和阻塞可以结合使用,先尝试自旋,若自旋几次还不能拿到锁,那就阻塞
2. AtomicBoolean和AtomicReference
在Unsafe类中,只提供了三种类型的CAS操作:int、long、Object(也就是引用类型)
2.1 如何实现 boolean
boolean通过int类型转换
3. AtomicStampedReference和 AtomicMarkableReference
3.1 ABA 问题及解决方案
-
问题:如果另外一个线 程把变量的值从A改为B,再从B改回到A,那么尽管修改过两次,可是 在当前线程做CAS操作的时候,却会因为值没变而认为数据没有被其他线程修改过
-
解决方案:要解决 ABA 问题,不仅要比较“值”,还要比较“版本号”
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) ;
3.2 为什么没有AtomicStampedInteger或 AtomictStampedLong
要同时比较数据的“值”和“版本号”,而Integer型或 者Long型的CAS没有办法同时比较两个变量,于是只能把值和版本号封装成一个对象,也就是这里面的 Pair 内部类,然后通过对象引用的CAS来实现。
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
3.3 AtomicMarkableReference
AtomicMarkableReference与AtomicStampedReference原理类似, 只是Pair里面的版本号是boolean类型的,而不是整型的累加变量
3.4 为什么需要AtomicXXXFieldUpdater
如果一个类是自己编写的,则可以在编写的时候把成员变量定义 为Atomic类型。但如果是一个已经有的类,在不能更改其源代码的情 况下,要想实现对其成员变量的原子操作,就需要AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater。
-
限制: 从代码层次可以看出,字段必须是volatile修饰,且不能是包装类
if (field.getType() != int.class) throw new IllegalArgumentException("Must be integer type"); if (!Modifier.isVolatile(modifiers)) throw new IllegalArgumentException("Must be volatile type");
4. AtomicIntegerArray、AtomicLongArray和 Atomic-ReferenceArray
数组元素的原子操作,并不是说 对整个数组的操作是原子的,而是针对数组中一个元素的原子操作而言
return unsafe.compareAndSwapInt(array, offset, expect, update);
5. Striped64与LongAdder
从JDK 8开始,针对Long型的原子操作,Java又提供了LongAdder、LongAccumulator;针对Double类型,Java提供了DoubleAdder、DoubleAccumulator
5. 1 LongAdder原理
AtomicLong内部是一个volatile long型变量,由多个线程对这个 变量进行CAS操作,多个线程同时对一个变量进行CAS操作,在高并发的场景下仍不够快。把一个变量拆成多份,变为多个变量,有些类似于 ConcurrentHashMap 的分段锁的例子,把一个Long型拆成一个base变 量外加多个Cell,每个Cell包装了一个Long型变量当多个线程并发 累加的时候,如果并发度低,就直接加到base变量上;如果并发度 高,冲突大,平摊到这些Cell上。在最后取值的时候,再把base和这些Cell求sum运算
由于无论是long,还是double,都是64位的。但因为没有double型的CAS操作,所以是通过把double型转化成long型来实现的。所以, 上面的base和cell[]变量,是位于基类Striped64当中的。英文Striped意为“条带”,也就是分片。
5.2 最终一致性
在sum求和函数中,并没有对cells[]数组加锁。也就是说,一边 有线程对其执行求和操作,一边还有线程修改数组里的值,也就是最 终一致性,而不是强一致性。这也类似于ConcurrentHashMap 中的 clear()函数,一边执行清空操作,一边还有线程放入数据,clear()函数调用完毕后再读取,hash map里面可能还有元素。因此,在LongAdder开篇的注释中,把它和AtomicLong 的使用场景做了比较。它 适合高并发的统计场景,而不适合要对某个 Long 型变量进行严格同步的场景
5.3 伪共享与缓存行填充
缓存 与主内存进行数据交换的基本单位叫Cache Line(缓存行)。在64位x86架构中,缓存行是64字节,也就是8个Long型的大小。这也意味着当缓存失效,要刷新到主内存的时候,最少要刷新64字节
主内存中有变量X、Y、Z(假设每个变量都是一个Long型),被CPU1和CPU2分别读入自己的缓存,放在了同一行Cache Line里面。当CPU1修改了X变量,它要失效整行Cache Line,也就是往总 线上发消息,通知CPU 2对应的Cache Line失效。由于Cache Line是数据交换的基本单位,无法只失效X,要失效就会失效整行的Cache Line,这会导致Y、Z变量的缓存也失效。
Y、Z和X变量处在了同一行Cache Line里面。要解 决这个问题,需要用到所谓的“缓存行填充”,分别在X、Y、Z后面加 上7个无用的Long型,填充整个缓存行,让X、Y、Z处在三行不同的缓存行中