深入理解Java高并发编程(5) - 无锁并发/乐观锁
1. CAS
CAS(Compare and Swap) 是一种无锁操作,包括以下三个操作数
- 内存地址/变量地址:要修改的变量地址
- 期望的原值
- 期望的新值
CAS的逻辑是通过将要赋值时候,比较要修改的变量值是否还等于原值,等于才把原值改为新值,否则则不进行任何操作,它的底层是依赖CPU的硬件指令(如 x86 架构的 CMPXCHG 指令),由硬件保证原子性,效率很高。
CAS需要配合Volatile使用,因为CAS需要借助volatile拿到变量的当前最新值来实现比较和交换。CAS仅仅是保证了赋值操作的原子性。
CAS适合线程数少的时候使用,尽量避免线程上下文切换
CAS操作的优点
- 无锁,开销小
- 多核CPU下CAS的线程上下文切换少(时间片耗尽发生),CAS通过线程不断运行不断尝试,减少线程阻塞,减小线程上下文切换的开销。
- 充分利用cpu资源
2. 原子类
JUC包下的原子类实现了CAS操作,底层是通过持有一个unsafe的成员变量,通过操纵unsafe的native方法实现CAS(正如我们之前所说,CAS操作底层依赖CPU的硬件指令)
原子类内部会维护一个volatile的value值,使得原子类保证了原子性,可见性,有序性。

2.1 原子整数
-
AtomicBoolean
-
AtomicInteger:对四字节整数封装,使其操作是原子的
-
compareAndSet:只做一次CAS操作,成功返回true。
-
getAndUpdate:接受一个IntUnaryOperator的函数式接口,在里面写lambda表达式执行具体的计算逻辑,底层还是调用compareAndSet搭配while循环。
-

- AtomicLong:对八字节整数封装,使其操作是原子的
2.2 原子引用类型
保证引用类型变量的线程安全。
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
ABA问题:AtomicReference能保证修改时值是没有发生变化的,但却不能保证对象有没有被修改过(也就是从A修改成B又修改成A,检测的时候还是两个A,依然可以CAS),这时候就可以用AtomicStampedReference和AtomicMarkableReference
- AtomicStampedReference
- 构造函数可以传入对象和一个int类型作为时间戳(版本号),每次完成一次修改都去修改这个时间戳,表示发生了一次修改
- AtomicStampedReference能得知对象发生了多少次修改
- AtomicMarkableReference
- AtomicMarkableReference关注对象是否发生了修改
- 构造函数传入对象和boolean类型表示是否被修改过
2.3 原子数组
当存在多线程操作数组中元素的场景,可以用到原子数组类来保证线程安全。
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
2.4 字段更新器
利用字段更新器,可以针对对象某个field进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常。
- AtomicReferenceFieldUpdater
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
2.5 原子累加器
jdk8后新增了几个累加类,且性能相对原子整数类的累加更好,更适合做累加。
因为累加器通过设置多个累加单元,在多线程的场景下,不同的线程操作不同的累加单元,最后再汇总,而原子整数被多线程累加后,可能存在线程竞争阻塞,这就导致了其性能不如原子累加器。
-
原子累加器:
-
LongAdder:只能做相加,底层还是调用LongAccumulator方法,会检查是否有二元运算函数,因为没有所以是默认累加。
-
LongAccumulator:支持自己定义二元的LongBinaryOperator并进行运算
-
-
原理:
- LongAdder类的关键域:
//累加单元数组,懒惰初始化
transient volatile Cell[] cells;
//基础值,但没有竞争,就使用这个域cas累加
transient volatile long base;
//在cells创建或扩容时置为1,表示加锁
transient volatile int cellsBusy
- CAS锁:
- CAS虽然是无锁的,也可以用有锁的思想,像这里去修改cellsBusy就是CAS(1表示加锁,0表示未加锁),这里也是为了保护共享变量cellsBusy多线程时的线程安全。
- 伪共享问题:
-
伪共享问题指的是,在cpu中分为多级缓存,因为cpu直接去内存中找耗时较高,cpu会先把数据读到缓存中,而缓存是以缓存行为单位,一般是64byte,不同cpu核心可能会把数据读到同一个缓存的缓存行中,当某个核心修改了这个缓存行的数据,这个cpu核心和其他所有cpu核心都要重新去读当前修改数据所在的缓存行,这就是伪共享问题。
-
Cell的源码中,@sun.misc.Contended注解是为了避免伪共享问题的注解,一个cell一般是24字节,这就导致了多个cell可能出现在多个cpu核心的同一个缓存的缓存行中。假设cell[0] 和cell[1]放在一个缓存行中,当核心1去修改cell[0],核心2的缓存行就会失效,就得重新从内存中读数据。@sun.misc.Contended通过在每个cell的前后加了128byte的空隙,使得不同cell放在了不同的缓存行,这样就能避免伪共享的问题。

LongAdder add原理:
- 当调用add方法时候,先判断cells是否为null
- 如果为null 就使用base进行CAS累加
- 如果CAS累加成功,就return掉
- 如果失败了就调用longAccumulate方法进行cells的创建
- 当cells不为null时候
- 去判断当前线程是否创建了cell,如果创建了,就在当前线程对应的cell上累加,累加成功就return,失败了则调用longAccumulate方法。
- 如果当前线程没创建cell,也是调用longAccumulate方法
- 如果为null 就使用base进行CAS累加


3. Unsafe类
Unsafe对象提供了非常底层,操作内存,线程的方法,之前的CAS,park/unpark 中都是调用了Unsafe中的方法。Unsafe对象不能直接调用,只能通过反射获得。

浙公网安备 33010602011771号