前面说了AQS,也说了ReentrantLock 加锁解锁的基本逻辑,现在就详细剖析源码
加锁源码分析
先看下,加锁的流程图,有个具体的印象,再看源码
假设有ABCD四个线程,同时执行到加锁这行代码。前文说过,state是0,代表可抢锁。
final void lock() {
if (compareAndSetState(0, 1)) // 假设A执行此代码成功
setExclusiveOwnerThread(Thread.currentThread()); // 标识哪个线程执有该锁
else
acquire(1);
}
// 这里compareAndSetState(0, 1),就是进行CAS操作
protected final boolean compareAndSetState(int expect, int update) {
// unsafe类调用的是native方法
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
// 线程A拿到了锁就直接返回了,那线程B C D 就进入 acquire(1) 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
}
- 先说 tryAcquire(arg) 就是抢锁
// tryAcquire 这个方法,非公平锁调用这个方法,传入参数是1,这个与可重入相关,待会再细说。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取 state状态,
if (c == 0) { // state是0,继续抢锁。刚进入lock 没抢到,现在可能锁已释放,有可能这次就抢成功了。
if (compareAndSetState(0, acquires)) { // B C D 线程再次竞争拿锁
setExclusiveOwnerThread(current);
return true; // 拿到了锁,lock 方法就结束了。
}
}
// 来抢锁的线程,本身就执有锁,说明是再次加锁,state 再加 1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow 内在溢出了,抛出异常
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 抢锁失败
}
这个方法没有难理解的逻辑,不过,setState(nextc); 这个方法没有用 CAS,这里会不会是bug
明确的告诉你,不是,因为这里不存在并发
current == getExclusiveOwnerThread()
即当前线程是执有锁的线程,只可能有一个线程进入这人条件分支,不需要用CAS
这里就是ReentrantLock可重入的体现,state=0,指锁不属于任何线程,
当某线程首次抢到锁,state=1,
此线程未释放锁的情况下,再次抢到锁,state=2,
这种情况下,只有连续释放两次锁,其它线程才可能抢到该锁。
这就是 ReentrantLock 锁的可重入。
- 详细说说 acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
addWaiter(Node.EXCLUSIVE),入队,自旋能保证,一定入队成功
addWaiter(Node.EXCLUSIVE) 这个方法就是用自旋的方式,保证线程入队
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 线程与Node绑定
Node pred = tail;
if (pred != null) { // 队列已经初始化,直接将new 出来的node 放到队尾
node.prev = pred;
if (compareAndSetTail(pred, node)) { // CAS 设置队尾
pred.next = node;
return node;
}
}
// 走到这里,说明队列未初始化,或者上面并发入队,入队失败了。
enq(node);
return node;
}
// 自旋方式入队
private Node enq(final Node node) {
for (;;) { // 死循环,保证入队一定成功
Node t = tail;
if (t == null) { // 队尾是null,说明得初始化队列
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
还记得上文画的那个AQS的图么
还是刚刚的4个线程,假设此刻 A 还没有释放锁,那state一定不等于0,exclusiveOwnerThread记录的就是线程A,
当B C D线程都进入addWaiter方法时,图中 head==null , tail == null,
想入队就要先初始化,即 t == null那个分支的代码。
从原码来看,队列用了哨兵,初始化就是new一个node不与任何线程绑定,
如下图所示,这个就是头节点,thread==null,之后入队的,thread一定有值
若 B C D线程中 B线程进入if (t == null) { } 这段代码,就完成了初始化,
那 C D 线程进入else分支,
它们俩个,谁执行compareAndSetTail()成功,谁就入队,
假设是C入队成功(如下图),那 B D 线程进入下一轮循环,
那 B D 线程进入下一轮循环,若存在并发,失败那个得再循环入队一次,
若不存在并发,就顺次入队。但最终大概都是这个样子。
这个方法里的逻辑是巨复杂的,不太好理解。
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; // 从队列中剔除,等待GC 回收
failed = false;
return interrupted; // lock 方法结束
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 设置头节点
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
先解释下这个条件 p == head && tryAcquire(arg) 前驱节点是head,且此节点抢锁成功,
结合上面那个图,B C D 三个节点,只有 C 节点才可以 抢锁,不要问为什么,这是规矩。
假设在 B C D 执行acquireQueued方法时,恰好A释放了锁,C 线程刚好抢到了锁,队列就变成上图的样子。
B D 线程进入shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt() 这两上方法。
原来代表C线程的node成为新的头节点(抢锁成功,就要出队)。
head 指向新的头节点,头节点thread 设置为null,
新旧头节点之间的指针去掉,旧的头节点等待GC回收。
在讲解shouldParkAfterFailedAcquire(p, node)
、 parkAndCheckInterrupt()
这两个方法之前,
说点题外话。
B C D 三个节点,只有 C 节点才可以 抢锁,Why ? 大神Doug Lea 就是这么写的,你想咋地!
个人认为,这样设计,代码实现简单,线程都已经进入等待队列了,说明并发比较高,
抢锁就派个代表出去就行了,别的继续在队列中等,出去抢锁的线程多了,CPU有意见。
再说,去的再多,也是只能是一个抢到锁。
派谁去,当然是头节点,去掉代码实现简单易懂,去其中任意一个,代码更加复杂。
不是说非公平锁么,那队列里,排在前面的先出队列去抢锁,很公平啊,哪里来的非公平?
这是个好问题,公平与非公平锁,不是在这里体现的。
不管公平锁还是非公平锁,队列里,都是前面那个出队抢锁,没区别,
公平与非公平,主要差别是tryAcquire() 这个方法上,
公平锁,若队列不为空,没入队的线程不得抢锁,
非公平锁,若队列不为空,没入队的线程却可以抢锁,
这时后到的线程可能先抢到锁,即不公平。
言归正传
- shouldParkAfterFailedAcquire(p, node) 判断是否要阻塞线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 前驱节点的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);
}
return false;
}
这段代码逻辑很复杂,一句话,前驱节点 waitStatus是-1,就返回true.
不过,今天讲 lock() unlock() ,waitStatus都是初始状态0。
之后的博客中,还会再次讲到这个方法。
结合上图,以线程 B为例 waitStatus状态都是 0,前驱节点是头节点,其waitStatus也是0
首次进入shouldParkAfterFailedAcquire方法,
执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL)这段代码之后,前驱节点waitStatus=-1,
之后走下一行 return false, compareAndSetWaitStatus 方法结束。
那在方法acquireQueued() 中 for (;;) {},第一次循环就结束了, B 没拿到锁
for (;;) {} 第二次循环开始,再次抢锁,
假设还没拿到,会第二次进入shouldParkAfterFailedAcquire()
此时B 线程的前驱节点,waitStatus=-1 该方法返回 true;(解锁时会讲到这一点)
那程序会走到 parkAndCheckInterrupt()这个方法里,阻塞线程
也就是说,经过两次循环,才会去阻塞线程,每次循环都会去抢锁的
Doug Lea
设计的真是好,发现这个线程需要阻塞,还要再给次抢锁的机会。
万一抢到了呢!真的是抢不到,再真正阻塞线程。
- parkAndCheckInterrupt() 阻塞线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞线程
return Thread.interrupted(); // 清除线程中断标记
}
题外话
park()阻塞了线程,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
Thread.interrupted()当且仅当 线程被阻断时返回true,它还会清除当前线程的中断标记位,
若要了解 unpark() interrupt() 唤醒有何不同,请参看我的另外一篇博客——park后是如何被唤醒的
至此,lock 调用的内层代码讲完了,再回头看下抢锁的总逻辑
// 线程A拿到了锁就直接返回了,那线程B C D 就进入 acquire(1) 方法
public final void acquire(int arg) {
// 抢锁,直到抢到为止,没抢到就在acquireQueued一直自旋转。
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt(); // 如果线程被中断过,给线程打个标记
}
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
上面这段代码很有意思,作为题外话说说,当然你可以不看,有点绕
。最好别看,看了会看糊涂的。
想想这段代码 Thread.currentThread().interrupt(); 什么时候会执行?
只有tryAcquire(arg)返回 false 且 acquireQueued() 返回 true 的时候才可以。
再看下,acquireQueued()什么时候才会返回 true 呢? 咱再看下原码
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);
}
}
接着上文说,B线程 在for (;;)中,第二次进入shouldParkAfterFailedAcquire(),返回true,
进入parkAndCheckInterrupt(),调用 LockSupport.park(this)后,
返回 return Thread.interrupted();
而 Thread.interrupted() 结果是false,因为目前 B线程 未调用过 interrupt(),继续自旋,
若在自旋过程中,其它的代码调用了interrupt(),那么 parkAndCheckInterrupt()会返回 true,
interrupted = true; 会被执行。
那 B线程在自旋过程中 parkAndCheckInterrupt() 会清除掉中断标记,但 interrupted = true是不会。
最终在acquire() 方法里,会明确的知道拿到锁的线程,曾经是否被中断过,若中断过,会重新在线程上做标记。
还有一点要说的
线程B 在自旋过程中,第二次 for 循环,会调用LockSupport.park(this)方法,程序就暂停了,不会再占用CPU资源了。
当被unpark()或interrupt()唤醒时,会接着自旋,要么拿到了锁,
要么重新调用LockSupport.park(this)方法,继续等待。
具体是被unpark()唤醒的,还是被interrupt()唤醒的,程序上做了区分。
区分不区分,lock(), unlock() 用不到,啥时候乃至,之后的博客会写。