第五章 - 共享模型之无锁
Java中 synchronized 和 ReentrantLock 等 独占锁 都是
悲观锁
在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了
乐观锁
的一种实现方式 CAS
管程即
monitor
是阻塞式的悲观锁
实现并发控制
,这章我们将通过非阻塞式的乐观锁
的来实现并发控制
问题提出
有如下需求,保证
account.withdraw取款方法
的线程安全, 下面使用synchronized
保证线程安全
解决思路 - 无锁
上面的代码中使用
synchronized加锁
操作来保证线程安全
,但是 synchronized加锁操作太耗费资源 (因为底层使用了操作系统mutex指令, 造成内核态和用户态的切换),这里我们使用 无锁
CAS 与 volatile (重点)
CAS
前面看到的
AtomicInteger
的解决方法,内部并没有用锁
来保护共享变量
的线程安全。那么它是如何实现的呢?
- 其中的关键是
compareAndSwap(比较并设置值)
,它的简称就是CAS
(也有 Compare And Swap 的说法),它必须是原子操作
。
流程:
- 当一个线程要去修改
Account对象
中的值时,先获取值prev(调用get方法)
,然后再将其设置为新的值next
(调用cas方法)。在调用cas方法时,会将prev
与Account中的余额
进行比较。
- 如果两者
相等
,就说明该值还未被其他线程修改,此时便可以进行修改操作。 - 如果两者
不相等
,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法),直到修改成功为止。
注意:
- 其实
CAS
的底层是lock cmpxchg指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】
的 原子性
。 - 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
volatile的作用
- 在上面代码中的
AtomicInteger类
,保存值的value属性
使用了volatile 修饰
。获取共享变量时,为了保证该变量的可见性
,需要使用volatile 修饰。 - volatile可以用来修饰
成员变量和静态成员变量
,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。
注意:
volatile 仅仅保证了共享变量的可见性
,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
- CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
为什么CAS+重试(无锁)效率高
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻
- 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
- 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。(线程数少于cpu核心数时效果好)
CAS 的特点 (乐观锁和悲观锁的特点)
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
-
CAS
是基于乐观锁
的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。 -
synchronized
是基于悲观锁
的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。 - CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响
原子整数 (内部通过CAS来实现 - AtomicInteger)
-
java.util.concurrent.atomic
并发包提供了一些并发工具类,这里把它分成五类: - 使用原子的方式 (共享数据为基本数据类型原子类)
-
AtomicInteger
:整型原子类 -
AtomicLong
:长整型原子类 -
AtomicBoolean
:布尔型原子类
- 上面三个类提供的方法几乎相同,所以我们将以 AtomicInteger为例子来介绍。
先讨论原子整数类,以
AtomicInteger
为例讨论它的 API 接口:通过观察源码可以发现AtomicInteger
内部都是通过 CAS
的原理来实现的**
updateAndGet源码
-
IntUnaryOperator
是函数式接口,可以用lambda表达式
表示具体实现里面的方法intapplyAsInt(int operand)
;
- 如
i.updateAndGet(p -> p + 2)
,p -> p + 2
就是int applyAsInt(int operand);
的lambda实现。其实就是实现+2的操作
举个例子: updateAndGet的实现
- 调用
updateAndGet
方法, 将共享变量i
,IntUnaryOperator对象
传递过去 - updateAndGet方法内部, 传过来的
operator
对象, 调用IntUnaryOperator
中的applyAsInt
方法, 实际调用的就是传递过来的对象的方法, 进行 / 操作
原子引用 (AtomicReference)
为什么需要原子引用类型 ? (引用数据类型原子类)
保证
引用类型的共享变量是线程安全
的(确保这个原子引用没有引用过别人)。
-
AtomicReference
-
AtomicMarkableReference
-
AtomicStampedReference
(可以解决ABA问题)
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
举例:使用原子引用实现BigDecimal存取款的线程安全
- 试着提供不同的 DecimalAccount 实现,实现安全的取款操作
不安全实现
- 下面这个是不安全的实现过程:
安全实现 - 使用 CAS
解决代码如下:在
AtomicReference类
中,存在一个value类型的变量,保存对BigDecimal
对象的引用
ABA 问题及解决 (重点)
- 如下程序所示,虽然在other方法中存在两个线程对共享变量进行了修改,但是修改之后又变成了原值,main线程对
修改过共享变量的过程
是不可见的,这种操作对业务代码并无影响。
- 主线程仅能判断出
共享变量的值
与最初值 A
是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况 - 如果主线程希望:只要有其它线程【动过】共享变量,那么自己的 CAS 就算失败,这时,仅比较值是不够的,需要再加一个
版本号
。使用AtomicStampedReference
来解决。
AtomicStampedReference (加版本号解决ABA问题)
解决ABA问题
AtomicMarkableReference (标记CAS的共享变量是否被修改过)
-
AtomicStampedReference
可以给原子引用
加上版本号
,追踪原子引用整个的变化过程,如:A -> B -> A -> C
,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。 - 但是有时候,
并不关心引用变量更改了几次,只是单纯的关心是否更改过
,所以就有了AtomicMarkableReference
AtomicStampedReference和AtomicMarkableReference两者的区别
-
AtomicStampedReference
需要我们传入 整型变量
作为版本号
,来判定是否被更改过 -
AtomicMarkableReference
需要我们传入布尔变量
作为标记
,来判断是否被更改过
原子数组 (AtomicIntegerArray)
- 保证数组内的元素的
线程安全
- 使用原子的方式更新数组里的某个元素
-
AtomicIntegerArray
:整形数组原子类 -
AtomicLongArray
:长整形数组原子类 -
AtomicReferenceArray
:引用类型数组原子类
不安全的数组
普通数组内元素, 多线程访问造成安全问题
安全的数组
使用
AtomicIntegerArray
来创建安全数组
字段更新器
保证
多线程
访问同一个对象的成员变量
时, 成员变量的线程安全性
。
-
AtomicReferenceFieldUpdater
- 引用类型的属性 -
AtomicIntegerFieldUpdater
- 整形的属性 -
AtomicLongFieldUpdater
- 长整形的属性
注意:利用字段更新器,可以针对对象的某个域(Field)进行原子操作
,只能配合 volatile 修饰
的字段使用,否则会出现异常。
代码示例
原子累加器 (LongAddr) (重要)
-
LongAddr
- LongAccumulator
- DoubleAddr
- DoubleAccumulator
累加器性能比较 AtomicLong, LongAddr
LongAddr
- 性能提升的原因很简单,就是在有竞争时,设置多个
累加单元
(但不会超过cpu的核心数),Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败
,从而提高性能。
AtomicLong
- 之前AtomicLong等都是在一个
共享资源变量
上进行竞争, while(true)
循环进行CAS重试, 性能没有LongAdder
高
LongAdder原理 (了解)
- LongAdder 是并发大师 @author Doug Lea (大哥李)的作品,设计的非常精巧
- LongAdder 类有几个关键域
无锁并发,这个怎么用了锁呢。这里并没有真正加锁,而是用了CAS实现锁
模拟 CAS 锁
原理之伪共享
- 其中 Cell 即为累加单元
什么叫缓存行?什么叫伪共享?
- 得从缓存说起,缓存与内存的速度比较
- 因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
- 而缓存以
缓存行
为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long) - 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
- CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
- 因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此1个缓存行是可以存下 2 个的 Cell 对象。这样问题来了:
- Core-0 要修改 Cell[0]
- Core-1 要修改 Cell[1]
- 无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效
-
@sun.misc.Contended
用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding
,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
LongAdder的add( )累加源码
累加主要调用下面的方法
add 流程图
longAccumulate( )源码
- 每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位)
sum( )源码
获取最终结果通过 sum 方法
Unsafe (重点)
概述
- Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过
反射
获得
- 可以发现
AtomicInteger
以及其他的原子类, 底层都使用的是Unsafe
类
Unsafe CAS 操作
使用底层的
Unsafe
实现原子操作
Unsafe 案例实现
使用自定义的 AtomicData 实现之前线程安全的原子整数 Account 实现
- 创建获取Unsafe对象的工具类
- Account 类
- 真正实现代码
本章小结
- CAS 与 volatile
- API
- 原子整数
- 原子引用
- 原子数组
- 字段更新器
- 原子累加器
- Unsafe
- 原理方面
- LongAdder 源码
- 伪共享