ReentrantLock那些事儿(二)
接着上篇写,了解了什么是公平锁和非公平锁的概念后,继续分析 ReentrantLock,它使用一个静态抽象类 Sync来管理公平锁和非公平锁:
1 | abstract static class Sync extends AbstractQueuedSynchronizer { |
Sync 有两个子类,一个是FairSync,实现了公平锁,另一个是NonfairSync,它是非公平锁的实现方式。
加锁
FairSync
1 | static final class FairSync extends Sync { |
我画了一张公平锁获取锁的流程图:
NoFairSync
1 | static final class NonfairSync extends Sync { |
nonfairTryAcquire是其父类 Sync 的方法:
1 | final boolean nonfairTryAcquire(int acquires) { |
我们发现非公平锁的源码更容易理解,公平锁在获取锁时还要判断有没有轮到自己获得锁,非公平锁去掉这部判断直接去竞争锁。
注:非公平锁可能会造成线程饥饿,但它的吞吐量要远大于公平锁,所以 ReentrantLock 默认使用非公平锁,其构造方法可以传入一个布尔值来标识是否使用公平锁:
1 | public ReentrantLock() { |
CAS 方法
1 | protected final boolean compareAndSetState(int expect, int update) { |
compareAndSetState是从AQS 继承而来,使用的是unsafe 的 CAS 方法,后面还会分析 Atomic包下的原子类,其实现原子操作都是基于 unsafe的 CAS 方法。
CAS (Compare And Swap)是并发系统中,实现原子操作和锁(乐观锁)的常见手段。
比较并交换,通过传入旧值与新值,保证只有在待修改值与旧值相等时才执行赋值为新值,只要保证了整个过程的原子性,则使用者可以返回值判断并重试的方式保证并发环境下的安全性。
扩展
CAS存在ABA 问题:
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,CAS成功。但实际上这时的A 已经不是原来的 A 了,可能会引发一些问题。
ABA 解决办法:
每次更新记录版本号。
解锁
1 | public void unlock() { |
1 | protected final boolean tryRelease(int releases) { |
总结
两个线程 t1 和 t2,t1和t2并发去执行lock()方法获得锁,假设t1先获取锁(state=0),state加1,如果t1在执行的过程中又执行了lock()方法,t1会直接获取锁,这就是锁重入,state再加1,ReentrantLock判断锁重入是根据当前线程是否是当前获取锁的线程,如果是则重入。
t2去获得锁时,如果t1释放锁,如果是公平锁则需要检查同步阻塞队列中是否有符合获取锁条件的其他线程,如果有则继续阻塞,如果没有则获取锁;如果是非公平锁则直接 CAS 获取锁。
如果t1还没释放锁,则将t2线程封装到 Node 节点插入同步阻塞队列的尾部,并阻塞t2,等待唤醒,当t1释放后,开始从同步等待队列的头节点开始唤醒阻塞线程t2,t2获取锁。
线程每次执行unlock()方法,state减1,直至 state等于0时锁才真正释放。
- Title: ReentrantLock那些事儿(二)
- Author: 薛定谔的汪
- Created at : 2018-08-15 18:01:54
- Updated at : 2023-11-17 19:37:37
- Link: https://www.zhengyk.cn/2018/08/15/java/ReentrantLock-2/
- License: This work is licensed under CC BY-NC-SA 4.0.