本文介绍J.U.C包中的Lock接口及其相关实现类。
本文的源码如无特别说明,都是基于JDK 1.8。
一、引入背景
在JDK 1.5之前,synchronized锁机制性能不好,且功能单一(“申请锁时不能响应中断”,“未提供‘尝试申请锁失败直接返回而不挂起’接口”,“不支持读写锁”,“不支持公平锁”,“不支持不可重入锁”等),故Java并发大神Doug Lea设计了Lock接口及其相关实现类——Lock接口,ReentrantLock类,ReentrantReadWriteLock类,它们在JDK 1.5被引入。
二、J.U.C包中的Lock接口及其相关实现类
Lock接口的所有方法描述如下:
lock()
,申请锁,如果失败则挂起直到申请成功,过程中不可响应中断lockInterruptibly()
,申请锁,如果失败则挂起直到申请成功,过程中可响应中断tryLock()
,尝试申请锁,立即返回锁申请结果——成功/失败tryLock(long time, TimeUnit unit)
,尝试申请锁,至多等待设定的超时时间,在该过程中如果申请到锁则立即返回“成功”,否则等待设定的超时时间到期返回“失败”,过程中可响应中断unlock()
,释放已申请到的锁newCondition()
,创建生成一个与本Lock实例对象关联的Condition对象,在其上可调用await()/awaitUninterruptibly()/await(long time, TimeUnit unit)/awaitNano(long nanosTimeout)/awaitUntil(Date deadline)
和signal()/signalAll()
方法,提供一个线程之间协作的机制(可类比于Object类的wait()/wait(long timeout)/wait(long timeout, int nanos)
和notify()/notifyAll()
方法)。需要注意的是,只有当实际锁是“排他锁”时,调用该方法才有意义。关于Condition的实现子类详见AQS中的ConditionObject
接下来主要介绍Lock接口的相关实现类。
2.1、ReentrantLock锁
2.1.1、基本介绍
ReentrantLock锁被设计作为synchronized锁的加强改进版。
最多加锁次数:2^31-1,因为是排他锁,所以该次数指的是最多重入次数。
其锁分类是:
- 悲观锁
- 阻塞锁
- 创建时可选是“公平锁”还是“非公平锁”,默认是“非公平锁”
- 可重入锁
- 排他锁
其常见使用示例如下:
1 | import java.util.concurrent.locks.Condition; |
有点需要注意:
tryLock()
方法的实现总是“非公平”的,不管创建ReentrantLock实例时选择的是“公平”还是“非公平”策略;lock()
,lockInterruptibly()
和tryLock(long time, TimeUnit unit)
的实现遵从创建ReentrantLock实例时选择的“公平/非公平”策略
2.1.2、源码实现
ReentrantLock内部有一个AQS继承体系如下:
1 | AQS |
ReentrantLock类内有一个ReentrantLock.Sync sync
实例成员变量,ReentrantLock类的所有具体实现都通过转发给sync
实现,在创建类实例对象时,可选择sync
具体是一个“ReentrantLock.FairSync”实例对象,还是一个“ReentrantLock.NonfairSync”实例对象,默认是一个“ReentrantLock.NonfairSync”实例对象,即“选择公平/非公平策略,默认是非公平策略”。
根据源码推导ReentrantLock锁的锁分类:
- 根据《AQS》,我们知道,基于AQS实现的锁,天然就是“悲观锁”和“阻塞锁”
- 如果
sync
具体是一个“ReentrantLock.FairSync”实例对象,查看“ReentrantLock.FairSync”源码,可知是“公平锁”,“可重入锁”和“排他锁”;如果sync
具体是一个“ReentrantLock.NonfairSync”实例对象,查看“ReentrantLock.NonfairSync”源码,可知是“非公平锁”,“可重入锁”和“排他锁”
我们知道synchronized锁存在一个happens-before规则——对一个锁的解锁,happens-before于随后对这个锁的加锁
,那ReentrantLock锁满足该happens-before规则吗?答案是:是的。
接下来进行证明:基于一个ReentrantLock锁,其sync
实例成员变量指向一个“ReentrantLock.FairSync”实例对象。假定有线程A和B(A和B不同线程,假如A和B是同一个线程,那么根据“程序顺序规则”该条happens-before规则,直接就能推导出目标结论),A执行“代码1”中的lockA
方法,B执行“代码1”中的lockB
方法,A已经获得锁正在执行解锁操作,B正在执行加锁操作。解锁操作细化见“代码2”,加锁操作细化见“代码3”。
代码1:
1 | import java.util.concurrent.locks.ReentrantLock; |
代码2:
1 | public final boolean release(int arg) { |
代码3:
1 | public final void acquire(int arg) { |
B想要获取锁,必须是“// 612处读取到// 513处的最新state写入值(值为0)”,而state是一个volatile变量,根据volatile变量happens-before规则,有“// 513 -> // 612”,再根据“程序顺序规则”和“传递性规则”,有“1 -> 2 -> 3 -> 4 -> 5 -> 511 -> 512 -> 513 -> 612 -> 7 -> 8 -> 9 -> 10”,其实在// 513后面还有其他语句,比如“// 52”,“// 53”,在// 612前面也有其他语句,比如“// 611”,“acquireQueued方法中的其他语句,因为获取到锁的tryAcquire方法调用可能是在acquireQueued方法中触发的”,但是// 512后面的语句和// 612前面的语句并不会影响业务代码,因此我们在叙述ReentrantLock锁的happens-before规则时(关注Lock接口的lock和unlock方法级粒度),可“宏观近似”地认为有“1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10”,到此目标结论得证。
2.1.3、与synchronized锁的比较
比较维度有4个:锁分类、使用、源码实现和性能。
2.1.3.1、锁分类
锁 | 悲观锁 vs 乐观锁 | 阻塞锁 vs 非阻塞锁 | 公平锁 vs 非公平锁 | 可重入锁 vs 非可重入锁 | 共享锁 vs 排他锁 |
---|---|---|---|---|---|
synchronized锁 | 悲观锁 | 阻塞锁 | 非公平锁 | 可重入锁 | 排他锁 |
ReentrantLock锁 | 悲观锁 | 阻塞锁 | 可选是“公平锁”和“非公平锁”,默认是“非公平锁” | 可重入锁 | 排他锁 |
2.1.3.2、使用
ReentrantLock锁的常见使用示例如上,synchronized锁的常见使用示例如下:
1 | public class SynchronizedUseExample { |
“使用”维度的比较描述:
- synchronized锁的使用形式相较更简单,自动进行锁释放;对于ReentrantLock锁,需要手动进行锁释放
- 从使用功能丰富的角度来说,ReentrantLock锁更胜一筹,除了以上常见接口,它还提供
lockInterruptibly()
,tryLock()
和tryLock(long time, TimeUnit unit)
等接口
2.1.3.3、源码实现
ReentrantLock锁的源码实现可参见“2.1.2、源码实现”小节,synchronized锁的源码实现可参见《synchronized锁内部实现》。
竞争申请锁的核心逻辑(假定现有T1和T2两个线程尝试竞争锁,两个线程足以代表两个以上的线程竞争情形):
- ReentrantLock非公平锁:非公平竞争锁,失败排队。当锁申请等待队列不为空时,最多可能的竞争选手有:
T1
,T2
和被唤醒队列头节点所指向线程
;当锁申请等待队列为空时,最多可能的竞争选手有:T1
和T2
- ReentrantLock公平锁:公平竞争锁,失败排队。当锁申请等待队列不为空时,最多可能的竞争选手有:
被唤醒队列头节点所指向线程
;当锁申请等待队列为空时,最多可能的竞争选手有:T1
和T2
- synchronized锁:
- 在轻度竞争环境中,竞争偏向锁或者轻量级锁
- 在重度竞争环境中,竞争重量级锁,非公平竞争重量级锁,失败排队。当锁申请等待队列不为空时,最多可能的竞争选手有:
T1
,T2
,被唤醒队列头节点所指向线程
和_Responsible线程
[1];当锁申请等待队列为空时,最多可能的竞争选手有:T1
和T2
2.1.3.4、性能
在JDK 1.5之前,synchronized锁的实现性能较差,这也是促进设计引入ReentrantLock锁的其中一个原因,但是随着JDK版本的发布提升,synchronized锁的实现性能不断得以提升改进,它与ReentrantLock锁的性能差距不断缩小,一个足以说明的例子是——“在JDK 1.8的ConcurrentHashMap实现中已经用synchronized替换ReentrantLock来完成分段加锁”。
性能实现的改进包括但不限于:
- synchronized锁的内部实现从“无锁、重量级锁”两种锁状态演进到“无锁、偏向锁、轻量级锁、重量级锁”4种锁状态
- 优化重量级锁的竞争策略
synchronized锁与ReentrantLock锁的性能对比(分成“ReentrantLock非公平锁”,“ReentrantLock公平锁”和“synchronized锁”3类。另外需要注意的是:不明确JDK版本和锁竞争程度的性能对比毫无意义):
- 在JDK 1.5之前(包括JDK 1.5):
- 轻度竞争,一般是
ReentrantLock非公平锁 > ReentrantLock公平锁 > synchronized锁
- 重度竞争,一般是
ReentrantLock非公平锁 > ReentrantLock公平锁 > synchronized锁
- 轻度竞争,一般是
- 在JDK 1.8中
- 轻度竞争,一般是
synchronized锁 > ReentrantLock非公平锁 > ReentrantLock公平锁
- 重度竞争,一般是
ReentrantLock非公平锁 > synchronized锁 > ReentrantLock公平锁
- 轻度竞争,一般是
针对以上情况,进行一些说明(自我理解):
- 在轻度竞争情形中,从“JDK 1.5之前(包括JDK 1.5)”到“JDK 1.8”,synchronized锁的性能提升,可能是由于在JDK 1.6中引入了偏向锁和轻量级锁
- 在重度竞争情形中,从“JDK 1.5之前(包括JDK 1.5)”到“JDK 1.8”,synchronized锁的性能提升,应该不是由于偏向锁和轻量级锁的引入,因为此时占主导作用的是synchronized重量级锁,故大概率是由于优化了重量级锁的竞争策略——根据“2.1.3.3、源码实现”小节的确可发现,在JDK 1.8中,synchronized重量级锁的竞争策略已经优化到跟ReentrantLock非公平锁/公平锁的竞争策略大同小异
2.2、ReentrantReadWriteLock锁
2.2.1、基本介绍
读写锁的设计目标应用场景是:读多写少,允许读读操作同时进行。
ReentrantReadWriteLock类其实本身并不继承实现Lock接口,其内部有两个Lock接口的继承实现类“ReentrantReadWriteLock.WriteLock”和“ReentrantReadWriteLock.ReadLock”,分别指代“写锁”和“读锁”。
“写锁”最多加锁次数:2^16-1,因为是排他锁,所以该次数指的是最多重入次数。
“读锁”最多加锁次数:2^16-1,因为是共享锁,所以该次数指的是最多“重入+非重入”次数。
上述“写锁”和“读锁”的锁分类如下表。
锁 | 悲观锁 vs 乐观锁 | 阻塞锁 vs 非阻塞锁 | 公平锁 vs 非公平锁 | 可重入锁 vs 非可重入锁 | 共享锁 vs 排他锁 |
---|---|---|---|---|---|
写锁 | 悲观锁 | 阻塞锁 | 创建时可选是“公平锁”和“非公平锁”,默认是“非公平锁” | 可重入锁 | 排他锁 |
读锁 | 悲观锁 | 阻塞锁 | 创建时可选是“公平锁”和“非公平锁”,默认是“非公平锁” | 可重入锁 | 共享锁 |
其常见使用示例如下:
1 | import java.util.concurrent.locks.ReentrantReadWriteLock; |
有几点补充说明:
- 读锁调用
newCondition()
方法直接抛出UnsupportedOperationException
异常,因为该方法只支持排他锁;写锁调用newCondition()
方法成功创建一个Condition实例 - 读锁和写锁的
tryLock()
方法的实现总是“非公平”的,不管创建ReentrantReadWriteLock实例时选择的是“公平”还是“非公平”策略;lock()
,lockInterruptibly()
和tryLock(long time, TimeUnit unit)
的实现遵从创建ReentrantReadWriteLock实例时选择的“公平/非公平”策略 - 线程T1获取写锁后,在不释放写锁的情形下能够再获取读锁;反之,线程T1获取读锁后,在不释放读锁的情形下不能够再获取写锁。可查阅“ReentrantReadWriteLock.ReadLock”和“ReentrantReadWriteLock.WriteLock”的
lock()
方法实现源码,另外有如下“代码4”实验代码 - 基于第3点,介绍“锁降级”和“锁升级”。锁降级:T1获取写锁,然后获取读锁,再释放掉写锁,完成“写锁 -> 读锁”的锁降级;不支持锁升级,因为获取读锁后不能再获取写锁
- 存在“写锁申请长时间饥饿”的情形,具体是
在非公平锁语境中,当前读锁被持有,“不断新产生的读锁申请”总是抢占“排队队列头节点的写锁申请”,导致该“写锁申请”长时间得不到满足
,解决方案是:针对上述情形,新产生的读锁申请不参与竞争,直接排队到末尾,详细可见“ReentrantReadWriteLock.NonfairSync”类的readerShouldBlock()
方法源码
代码4:
1 | import java.util.concurrent.locks.ReentrantReadWriteLock; |
结果如下(main线程在打印“C”字符后申请读锁失败挂起):
1 | A |
2.2.2、源码实现
ReentrantReadWriteLock内部有一个AQS继承体系如下:
1 | AQS |
ReentrantReadWriteLock内部有一个Lock继承体系如下:
1 | Lock |
ReentrantReadWriteLock类内有3个主要的实例成员变量:
Sync sync
,在创建类实例对象时,可选择sync
具体是一个“ReentrantReadWriteLock.FairSync”实例对象,还是一个“ReentrantReadWriteLock.NonfairSync”实例对象,默认是一个“ReentrantReadWriteLock.NonfairSync”实例对象,即“选择公平/非公平策略,默认是非公平策略”ReentrantReadWriteLock.ReadLock readerLock
,与writerLock
共用sync
,然后所有具体实现都通过转发给sync
实现,sync
中的state
字段高16位分配给readerLock
ReentrantReadWriteLock.WriteLock writerLock
,与readerLock
共用sync
,然后所有具体实现都通过转发给sync
实现,sync
中的state
字段低16位分配给writerLock
根据源码推导ReentrantReadWriteLock.WriteLock锁的锁分类:
- 根据《AQS》,我们知道,基于AQS实现的锁,天然就是“悲观锁”和“阻塞锁”
- 如果
sync
具体是一个“ReentrantReadWriteLock.FairSync”实例对象,查看“ReentrantReadWriteLock.FairSync”源码,可知是“公平锁”,“可重入锁”和“排他锁”;如果sync
具体是一个“ReentrantReadWriteLock.NonfairSync”实例对象,查看“ReentrantReadWriteLock.NonfairSync”源码,可知是“非公平锁”,“可重入锁”和“排他锁”
根据源码推导ReentrantReadWriteLock.ReadLock锁的锁分类:
- 根据《AQS》,我们知道,基于AQS实现的锁,天然就是“悲观锁”和“阻塞锁”
- 如果
sync
具体是一个“ReentrantReadWriteLock.FairSync”实例对象,查看“ReentrantReadWriteLock.FairSync”源码,可知是“公平锁”,“可重入锁”和“共享锁”;如果sync
具体是一个“ReentrantReadWriteLock.NonfairSync”实例对象,查看“ReentrantReadWriteLock.NonfairSync”源码,可知是“非公平锁”,“可重入锁”和“共享锁”
我们知道synchronized锁存在一个happens-before规则——对一个锁的解锁,happens-before于随后对这个锁的加锁
,那ReentrantReadWriteLock锁满足该happens-before规则吗?具体分为几种情况:
- 对一个ReentrantReadWriteLock.WriteLock锁的解锁,happens-before于随后对“同一个写锁”或者“关联ReentrantReadWriteLock.ReadLock锁”的加锁
- 对一个ReentrantReadWriteLock.ReadLock锁的解锁,happens-before于随后对“关联ReentrantReadWriteLock.WriteLock锁”的加锁
- “对一个ReentrantReadWriteLock.ReadLock锁的解锁”与“随后对同一个读锁的加锁”,如果是在同一线程,则根据“程序顺序规则”该条happens-before规则,有“对一个ReentrantReadWriteLock.ReadLock锁的解锁,happens-before于随后对同一个读锁的加锁”;否则,两者没有必然的happens-before关系
具体证明可参见“2.1.2、源码实现”小节中对于ReentrantLock锁的happens-before规则证明。
参考文献
[1]《synchronized锁内部实现》中的“3.6、重量级锁”小节