根据ReentrantLock源码,一步一步探究它的主要功能实现原理,在这个过程之中也可以了解到aqs同步器的一些知识点。
本文需要前置知识:
cas
aqs的state属性
aqs的等待队列
aqs的条件队列
park / unpark
如何加锁
从lock方法入手,我们主要观察非公平锁。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
主要步骤:
尝试通过cas设置aqs同步器为1,如果成功则直接设置owner线程为当前线程,表示加锁成功。
如果cas失败,进入aquire方法.
观察aquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在这里会再次尝试获取锁(tryAcquire) ,如果获取失败则添加到waiter队列,也就是等待队列。
观察一下添加到等待队列这一步具体是怎样做的。
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);
}
}
可以看到进来之后有一个很显眼的死循环,在这里面会再次尝试获取锁(tryAcquire),如果成功了自然就获取到了锁。这里面具体逻辑在后面解锁会讲到。
如果失败了,则会让当前线程park住(暂停当前线程运行)
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
现在总结一下加锁主要流程:
尝试cas改变同步器state的值,从0->1 如果成功代表加锁成功,同时aqs的owner线程为设置为当前线程,否则进入尝试阶段。
尝试阶段主要是将当前线程作为一个节点加入等待队列(尾插法),并且尝试再次获取锁,会尝试多次,(应该是3次),如果还是尝试失败那么当前线程会被park住。
这里面有一个值得注意的问题,当前线程是被park住了,那么以后由谁来unpark它呢?
事实上,在尝试阶段,有一个动作会将当前线程的前一节点的status设置为-1,表示将来由它来唤醒当前线程。
这个动作就是 shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) //Node.SIGNAL==-1
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将前驱节点的waitStatus设置成-1
}
return false;
}
如何解锁
从unlock方法开始观察。
public void unlock() {
sync.release(1);
}
//进入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;
}
//tryRelease方法
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;
//设置owner线程为null
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可以看到release方法比较简单,首先尝试释放,如果失败则返回false
如果成功,则设置owner线程为null,并且唤醒等待队列中的某个节点。
这里主要观察它如何唤醒,也就是如何选择等待队列里的节点
private void unparkSuccessor(Node node) {
//拿到头节点的waitStatus
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);//cas设置为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;
}
//如果后继节点不为空,则由当前线程来unpark它。
if (s != null)
LockSupport.unpark(s.thread);
}
可以看到它唤醒的是头节点(哨兵节点)后面的第一个节点。
那么此时将该线程唤醒之后,我们可以回到前面的代码看看了。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//2.经过循环来到这里,再次尝试竞争获取锁,如果竞争失败了再次回到队列
if (p == head && tryAcquire(arg)) {
//3.竞争成功之后,把前面的哨兵节点废掉,GC回收掉。此时由当前节点作为哨兵节点
setHead(node);//这一步会将node里面的thread清空,作为哨兵节点
p.next = null; // help GC
failed = false;
//4.返回true退出,获取锁成功。
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//1.先是在这里被park住,此时被唤醒就从这里开始执行。
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
解锁流程总结:
首先尝试解锁,如果成功了则将同步器的owner线程设为null
然后开始唤醒等待队列中的节点,唤醒的是哨兵节点后面的第一个节点,也就是离哨兵最近的节点。
这一过程中,需要注意的是原来的哨兵节点会被废掉,新的哨兵节点就是唤醒人所处的节点,作为哨兵节点,它内部的thread也会被清空。
如何实现可重入
如果前面的加锁,解锁看懂了,锁重入其实也很好理解。
我们在看前面代码的时候,有一地方就是每次加锁,解锁传入的参数都是1
acquire(1); //获取锁
sync.release(1);//释放锁
现在再回头看看这俩方法,找到它们的具体逻辑,先看加锁。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//c==0,表示初次加锁,这时的acquires也是1
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//不是0,表示要么有竞争,要么是发生了锁重入
else if (current == getExclusiveOwnerThread()) {
//如果当前线程是owner线程,那么就设置state为当前值+acquires
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
加锁流程里面,其实就涉及到state值的自增,当发现当前线程持有锁的时候,这时会将state值++
再来看看解锁,不出意外的话应该是自减。
protected final boolean tryRelease(int releases) {
//获取当前state值并且减去releases 其实就是--
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//注意,只有当状态值被减为0的时候才会真正释放锁,即清空owner线程
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可以看到,解锁流程果然不出意外的是将state值自减,直到减为0表示当前线程不再持有这把锁,此时设置owner线程为null。
如何使它可打断
我们知道,park方法是会被interrupt打断的,ReentrantLock内部也是通过park来控制线程的执行。但是它却提供了可打断锁和不可打断锁,默认情况下都是不可打断锁。
先来看看默认情况下,它如何控制成不可打断。
//这是竞争不到锁的时候,park住当前线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//返回Thread.interrupted(),注意此种方法会清除打断标记!
return Thread.interrupted();
}
//再回来看看这个方法,注意顺序
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;
//2.此时的interrupted为true,直接返回
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//1.这里假如被打断了,我们进入到这里,会将interrupted设置为true,
//它仍然会进入下一轮循环!!
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//上面方法出来之后,将true返回到aquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//在这里拿到了true
selfInterrupt();//进入selfInterrupt
}
static void selfInterrupt() {
Thread.currentThread().interrupt();//通知线程被打断了
}
缕一缕上面的代码可以发现,不可打断模式下,我们调用interrupt通知它中断,它的park是不会受到影响的,因为就算当前park被打断,它检测到之后仍然会进入死循环,再次park.
但是它在最后,也就是获取到锁的时候,还是被通知了线程它被打断了(selfInterrupt)
总结: 不可打断模式下,对目标线程发起的interrupt在拿不到锁的时候不会收到中断通知,只有当目标线程拿到锁了之后才能收到中断通知。
再来看看可打断模式吧,从加锁方法开始。
//入口方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//找到这个实际的加锁逻辑
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);
}
}
结果一目了然了,可打断模式下,在park并且检测中断方法中,如果被通知中断,则会立刻抛出中断异常。
如何实现条件等待/唤醒
首先要明确,await / signal / signalAll都是ConditionObject类里面的方法,而ConditionObject是定义在aqs的内部类。
我们每次申请一个条件condition其实就是new 了一个ConditionObject而已。
final ConditionObject newCondition() {
return new ConditionObject();
}
先看await方法。
public final void await() throws InterruptedException {
//如果检测到被打断,抛出中断异常。表示await可打断
if (Thread.interrupted())
throw new InterruptedException();
//这里既是用当前线程new一个新的节点,也是添加到该条件队列里
Node node = addConditionWaiter();
//释放当前线程上的锁,之所以要用fully是为了释放重入锁。
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果还没有在aqs队列里面
while (!isOnSyncQueue(node)) {
//将当前线程park住。
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//重新竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//所有被取消的节点断开waiter链表
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
//打断模式应用
reportInterruptAfterWait(interruptMode);
}
总结一下await流程:
首先创建当前线程为节点并添加到条件队列里面。
释放当前节点持有的锁。
将当前线程park住。
这一过程中,释放当前锁使用的是fullyRelease方法,是为了可以释放掉当前重入锁(假设有重入)
再来看看唤醒方法signall
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//拿到等待头节点
Node first = firstWaiter;
if (first != null)
//进入doSignal
doSignal(first);
}
private void doSignal(Node first) {
do {
//非空判断
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//断开链表连接
first.nextWaiter = null;
//将等待头节点转移至aqs阻塞队列
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
//如果cas条件状态失败,返回false,表示节点被取消了
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//加入aqs队列尾部
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//unpark唤醒节点
LockSupport.unpark(node.thread);
return true;
}
总结一下唤醒方法signal的流程:
拿到条件队列里的首节点
将其添加到aqs队列尾部
唤醒该节点的线程
这里要注意:
唤醒的目标是条件队列也就是await队列里的首个等待节点
需要将其转移到aqs队列也就是阻塞队列里面,并且添加到尾部。
唤醒不代表目标节点马上就能获取到锁,对方仍然需要竞争。
看完了signal方法,再来看看signalAll方法(唤醒全部)就一目了然了。
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}
signalAll的作用是唤醒目标条件的全部线程,在源码上看到跟signal的区别就是在于,signal里面只进行了一次转移等待队列-> aqs队列,而signalAll则是将整条等待队列转移完毕。
private void doSignal(Node first) {
do {
//....
//这里仅会转移一次,如果转移成功了就跳出循环了
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
private void doSignalAll(Node first) {
//...
do {
//...
//这里把转移这段逻辑放在了循环内部
transferForSignal(first);
} while (first != null);
}