在并发场景下,为了保证线程安全,需要进行加锁操作。
在使用ReentrantLock时需要手动加锁解锁,这一点区分于Synchronized的自动释放。
所以在使用ReentrantLock时一定要遵循java编码规范,在try块钱调用lock()方法获取锁,在finnally块中调用unlock()方法释放锁,这样保证锁一定能被正确释放。Synchronized代码块是通过monitorenter 和 monitorexit 指令来实现的,ReentrantLock的实现原理呢?
本文重点在于结合源码介绍ReentrantLock的实现原理。这个过程中会涉及到AQS,针对涉及到的点进行AQS相关的介绍。
下面进入ReentrantLock原码分析,ReentrantLock实现了Lock接口。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
包括了4个加锁接口,一个释放锁的接口,以及一个生成Condition对象的接口。源码分析就从这些加锁、解锁接口进入并深入,至于newCondition()在JAVASE-3 多线程体系2-Condition与ConditionObject中详细介绍。
先整体看一下ReentrantLock这个类的内部组成:
public class ReentrantLock implements Lock, Serializable {
private final ReentrantLock.Sync sync;
public ReentrantLock() {
this.sync = new ReentrantLock.NonfairSync();
}
public ReentrantLock(boolean isFair) {
this.sync = (ReentrantLock.Sync)(isFair? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
static final class FairSync extends ReentrantLock.Sync { // 省略FairSync逻辑}
static final class NonfairSync extends ReentrantLock.Sync { // 省略NonfairSync逻辑}
abstract static class Sync extends AbstractQueuedSynchronizer{ // 省略Sync逻辑}
// 此处省略ReentrantLock的其他方法,lock系列,unlock,...
}
内部定义了一个Sync类,该类继承自AQS;定义了两个Sync的子类:FairSync和NonfairSync,从名字可以看出这两个子类的区别在于获取锁的策略是否公平。从默认构造函数可以看出,ReentrantLock默认支持不公平的锁策略。
在简单看一下ReentrantLock关于Lock的实现:
private final ReentrantLock.Sync sync;
public void lock() {
this.sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
this.sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return this.sync.nonfairTryAcquire(1);
}
public boolean tryLock(long var1, TimeUnit var3) throws InterruptedException {
return this.sync.tryAcquireNanos(1, var3.toNanos(var1));
}
public void unlock() {
this.sync.release(1);
}
public Condition newCondition() {
return this.sync.newCondition();
}
可以看出,Lock中提供的方法最终都调用了sync的方法来实现,ReentrantLock就是提供了一层封装而已。ReentrantLock就是基于AQS来实现的,因此本文不可避免地需要接触AQS。
由于这部分涉及的概念较多,且代码量比较大,因此只对涉及内容进行简单讲解。本文涉及的ReentrantLock是一个独占锁,因此对AQS的介绍会有所聚焦;对AQS内容感兴趣,可以参考:JAVASE-3 多线程体系10-AQS源码。
ReentrantLock不仅是独占性质的锁,从名字可以看出来,还具有可重入性。这两点在AQS中体现在两个实例变量上:Thread类型的exclusiveOwnerThread 和 int类型的state。
ReentrantLock的sync继承自AQS,每个ReentrantLock实例都对应一个sync实例变量,因此每个ReentrantLock对象多对应一个exclusiveOwnerThread和state变量, 即每个ReentrantLock都有一把锁。
exclusiveOwnerThread 和 state 的使用方式如下:
当没有线程获取锁时,exclusiveOwnerThread为null,state等于1;
当锁被线程获取时,exclusiveOwnerThread指向获取锁的线程,state变成1;
当该线程再次获取锁时,state加1;释放锁的时候,也是没释放一次,state减1。
这里介绍的内容都会在下文的代码解析里体现。首先看一下NonfairSync和FairSync的继承体系:
从上图可以看出,Sync中定义了nonfairTryAcquire(int)
和tryRelease(int)
NonfairSync和FairSync中分别重写了lock()和tryAcquire(int)。
因此释放锁,没有公平与否的概念,走的都是Sync中的tryRelease(int)
逻辑。
接下来我们先看一下非公平锁是怎么实现的,再对比一下公平锁。
final void lock() {
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
lock() 直接调用AQS的acquire(1);
在acquire
中有一个条件!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这里的tryAcquire
就是上面提到的NonfairSync和FairSync自己重载的方法。
我们看一下NonfairSync的tryAcquire
方法:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
非公平锁的tryAcquire
直接调用了Sync类中定义的nonfairTryAcquire
方法。
因此非公平锁的lock()主要实现逻辑就在nonfairTryAcquire
方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0){
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
return false;
}
先获取state的值,如果是state的值等于,0表示没有被加锁:则使用CAS方式将state加1,
然后当前线程的值赋值给exclusiveOwnerThread。
如果不等于0,表示锁已经被线程占用,这时需要判断是不是自己占用了锁:current == getExclusiveOwnerThread()
;如果是该线程占用了锁,则是重入,对state加1,否则,尝试加锁失败-返回false。
=> tryAcquire(int args) 就是一次尝试加锁,如果加锁成功-返回true,加锁失败返回false;
继续回到!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,
tryAcquire(arg)加锁成功了,就不会执行后面的条件判断了,直接退出;
加锁失败后,进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,首先执行addWaiter(Node.EXCLUSIVE)
,将当前线程加入到等待队列中:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
第一步构造一个Node对象new Node(Thread.currentThread(), mode)
,mode是Node.EXCLUSIVE,为null,是一个标志位(在AQS中独占锁用null表示,new Node()表示共享锁,存在nextWaiter变量里);
接下来:
if (pred != null) {
Node pred = tail;
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
cas需要和自旋配合才可以起作用,这里只是进行了一次判断,如果可以使用cas将新创建的节点设置为尾节点,则操作成功,并退出。如果一次不成功呢?
没关系,还有enq()方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq()就是一个自旋+cas的组合,将新创建的节点加入到同步队列的尾部;
这里是不是有点疑惑,那为什么addWaiter上多此一举地加了一次cas判断?
其实是作了一层优化:一次判断如果可以退出,没必要再进自旋块。
=> addWaiter方法时使用当前线程构造一个Node对象,加入同步队列中,并返回该Node对象。
这个Node对象会传参给acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
主体内容在一个自旋块中,线程如果获取不到锁,就会一直阻塞在这个自旋逻辑中,直到成功获取锁。
在介绍这个逻辑之前,先说明一下AQS对于独占锁的唤醒机制:
当前节点用currentNode表示,前驱节点用preNode表示,因此有以下关系:currentNode.prev == preNode;
(1) currentNode由preNode唤醒;
(2) preNode必须时Signal的,才能唤醒currentNode;
(3) 只有当preNode位于同步队列的头结点时,才会触发唤醒currentNode操作;
接下来,我们继续看自旋的主体逻辑线:
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
interrupted = true;
}
}
该自旋逻辑只有一个退出执行点:return interrupted;
,且位于p == head && tryAcquire(arg)
条件逻辑中。说明只有node的前驱节点是头结点,然后调用tryAcquire获取锁成功时,才会退出该自旋逻辑(获取锁成功后,会把node设置为头结点)。
在看一下第二个条件语句:shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()
;注意:这个条件语句也是存在于自旋块中的。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire方法比较清晰,使用cas方式将node的前驱节点设置为SIGNAL状态;
同步队列的Node有4种状态:CANCELLED=1, SIGNAL=-1; CONDITION=-2; PROPAGATE=-3;
pred.waitStatus > 0
表示该节点所在的线程已经被cancel了;设置前驱节点的过程也会更新同步对类信息,剔除已经被cancel的节点。
在shouldParkAfterFailedAcquire配合着自旋块,将node的前驱节点状态设置为SIGNAL时;再次自旋进入shouldParkAfterFailedAcquire方法会返回true;此时会调用parkAndCheckInterrupt()方法:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
该方法逻辑简单,直接调用LockSupport.park(this);使得该线程休眠,等待被前驱节点唤醒。
=> 至此,非公平整个lock的解锁逻辑以及清楚了。
在此基础上了解公平锁lock就比较简单了。
从下面的图可以看出,NonfairSync和FairSync的区别仅在于lock()和tryAcquire(int),至于获取锁失败后怎么添加到同步队列是没有区别的。
// 公平锁
final void lock() {
acquire(1);
}
// 非公平锁
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
对比两个lock()方法:
公平锁按照获取锁的流程调用acquire(1);
来获取锁;
非公平锁,直接先试探一下能否获取锁成功,失败了再调用acquire(1);
获取锁。
acquire会调用tryAcquire,在前面介绍过,这里不再赘述。正如前文所述,在tryAcquire中非公平锁,直接进行尝试加锁。
我们再看一下公平锁下的加锁策略:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
在尝试加锁之前,先调用hasQueuedPredecessors
判断是否已经有其他线程在等待锁了,如果没有才会加锁,如果有了返回false表示加锁失败。其实在tryAcquire中已经可以实现公平和非公平加锁的区别了,在非公平锁的lock()里的一次尝试加锁是一层优化。
对比可以发现:
非公平锁,可以避免线程等待时间,省去了释放cpu和获取cpu的时间。
假如线程A准备加锁时,已经有线程B和C在同步队列中等待,这时锁正好被线程D释放;此时A就不需要先去队列排序,再被唤醒,而是直接获取锁;这样从整体的角度,省去了一次线程切换的时间开销,有助于提高程序运行效率。
这样也直接导致了一个问题,从A后面由A1,A2,A3……他们到来的时候,锁正好被释放,那么B和C就会一直等下去,出现线程饥饿线程。
unlock在公平锁和非公平锁中没有区分,调用的是同一个方法:
public void unlock() {
sync.release(1);
}
进入Sync的release方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release方法中涉及tryRelease和unparkSuccessor两个方法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
tryRelease方法直接操作state和exclusiveOwnerThread变量,先给state将去需要释放锁的数量,然后判断如果state为0,就将exclusiveOwnerThread置为null。
其中 if (Thread.currentThread() != getExclusiveOwnerThread())
是为了保证释放锁的操作需要由只有锁的线程来进行。
当线程占用的锁都释放完成后,即state变为0的时候,release(1)返回true,此时进入
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
这里调用unparkSuccessor(h)
唤醒头结点的一个有效的后继节点。因为独占锁只能被一个线程所占用,所以没必要唤醒很多线程,然后让他们去竞争-从而浪费资源。在后续文章中提及的共享锁,和这里有所区分,一次会唤醒多个线程,届时会详细介绍。
我们再看一下unparkSuccessor
方法的逻辑:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) {
compareAndSetWaitStatus(node, ws, 0);
}
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev){
if (t.waitStatus <= 0){
s = t;
}
}
}
if (s != null){
LockSupport.unpark(s.thread);
}
}
这里头结点的后继节点不为null,则调用LockSupport.unpark(s.thread);
激活该线程,否则Node t = tail; t != null && t != node; t = t.prev
:从尾结点开始便利,直到遇到一个没有被cancel的节点。
是不是有个疑问:为什么不是释放头结点,而是释放头结点的后继节点?
这里,我们需要结合前面的addWaiter方法一起介绍:
在addWaiter
方法中,调用enq
方法将线程加入到同步队列中,中有一个判断:判断头结点是否为空;
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
t == null表示头结点和尾结点都为null,此时调用compareAndSetHead(new Node())
创建了一个新的头结点,后续因获取锁失败而阻塞的线程都会添加在这个头结点之后。这就是为什么当锁完全被释放后,释放节点要从头结点的后继节点开始。所以,释放锁的过程是FIFO,是公平的。
除了lock()和unlock()之外,Lock接口中还定义了tryLock(),lockInterruptibly(),tryLock(long var1, TimeUnit var3)方法,是对功能的补充,本质的实现都是依赖于AQS。
tryLock()不是一个阻塞方法,如果获取成功返回true, 获去失败返回false:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
可以看出tryLock()
直接调用了上面介绍的nonfairTryAcquire()
方法尝试获取锁,是一次性判断(不是自旋),也是一个非公平性质的加锁操作。
至于tryLock(long times, TimeUnit unit)
无非是一个具有超时机制的加锁操作,在等待times时间,还没有获取到锁,就返回false;否则加锁成功-返回true。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
调用的是tryAcquireNanos方法:
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
逻辑和acquire方法比较相似,不同点是:在调用tryAcquireNanos过程中,如果线程被中断了会抛出中断异常。
再看一下具有延时功能的doAcquireNanos方法,由于代码与前面介绍的加锁过程极其相似,只挑出主线逻辑进行介绍:
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} // 省略部分逻辑,突出矛盾的主要方面
除了正常加锁之外,多了一层判断,如果事件超时了,if (nanosTimeout <= 0L)
,则返回false; 因为超时后,需要自动被唤醒,所以不能调用LockSupport.park
方法,此处调用的是LockSupport.parkNanos(this, nanosTimeout);
看到这个方法就知道这个方法时lock的copy版本,增加一个响应中断的能力。
在介绍lock时未曾提到会抛出InterruptedException异常,事实上,Lock接口提供的加锁操作只有lockInterruptibly()
和tryLock(long timeout, TimeUnit unit)
具有响应中断的能力。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
调用acquireInterruptibly加锁:
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted()){
throw new InterruptedException();
}
if (!tryAcquire(arg)){
doAcquireInterruptibly(arg);
}
}
如果此时线程被中段,就会抛出InterruptedException异常;
加锁成功,会直接返回;如果加锁失败,就会进入doAcquireInterruptibly()
private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
parkAndCheckInterrupt执行完后,会返回这个过程中线程是否被中断过,这里因为和lock加锁过程一致,不再详细介绍。
但是这里有一处需要看一下:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
相同的逻辑在lock()中是这样的:
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
之后因为返回的interrupted 是true, 而调用Thread.currentThread().interrupt();
使线程处于中断状态,而不是抛出异常。
除此之外,还有一个newCondition()方法,用于生成一个Condition对象,这里不再讲解了。放在ConditionObject文章中一起讲解。