系列文章目录 java
上一篇文章 咱们逐行分析了独占锁的获取操做, 本篇文章咱们来看看独占锁的释放。若是前面的锁的获取流程你已经趟过一遍了, 那锁的释放部分就很简单了, 这篇文章咱们直接开始看源码.node
开始以前先提一句, JAVA的内置锁在退出临界区以后是会自动释放锁的, 可是ReentrantLock这样的显式锁是须要本身显式的释放的, 因此在加锁以后必定不要忘记在finally块中进行显式的锁释放:segmentfault
Lock lock = new ReentrantLock(); ... lock.lock(); try { // 更新对象 //捕获异常 } finally { lock.unlock(); }
必定要记得在 finally
块中释放锁! ! !
必定要记得在 finally
块中释放锁! ! !
必定要记得在 finally
块中释放锁! ! !安全
因为锁的释放操做对于公平锁和非公平锁都是同样的, 因此, unlock
的逻辑并无放在 FairSync
或 NonfairSync
里面, 而是直接定义在 ReentrantLock
类中:多线程
public void unlock() { sync.release(1); }
因为释放锁的逻辑很简单, 这里就不画流程图了, 咱们直接看源码:并发
release方法定义在AQS类中,描述了释放锁的流程函数
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
能够看出, 相比获取锁的acquire
方法, 释放锁的过程要简单不少, 它只涉及到两个子函数的调用:ui
tryRelease(arg)this
unparkSuccessor(h)线程
下面咱们分别分析这两个子函数
tryRelease
方法由ReentrantLock的静态类Sync
实现:
多嘴提醒一下, 能执行到释放锁的线程, 必定是已经获取了锁的线程(这不废话嘛!)
另外, 相比获取锁的操做, 这里并无使用任何CAS操做, 也是由于当前线程已经持有了锁, 因此能够直接安全的操做, 不会产生竞争.
protected final boolean tryRelease(int releases) { // 首先将当前持有锁的线程个数减1(回溯到调用源头sync.release(1)可知, releases的值为1) // 这里的操做主要是针对可重入锁的状况下, c可能大于1 int c = getState() - releases; // 释放锁的线程当前必须是持有锁的线程 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 若是c为0了, 说明锁已经彻底释放了 boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
是否是很简单? 代码都是自解释的, LZ就很少嘴了.
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
锁成功释放以后, 接下来就是唤醒后继节点了, 这个方法一样定义在AQS中.
值得注意的是, 在成功释放锁以后(tryRelease
返回 true
以后), 唤醒后继节点只是一个 "附加操做", 不管该操做结果怎样, 最后 release
操做都会返回 true
.
事实上, unparkSuccessor 函数也不会返回任何值
接下来咱们就看看unparkSuccessor的源码:
private void unparkSuccessor(Node node) { int ws = node.waitStatus; // 若是head节点的ws比0小, 则直接将它设为0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); // 一般状况下, 要唤醒的节点就是本身的后继节点 // 若是后继节点存在且也在等待锁, 那就直接唤醒它 // 可是有可能存在 后继节点取消等待锁 的状况 // 此时从尾节点开始向前找起, 直到找到距离head节点最近的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; // 注意! 这里找到了之并有return, 而是继续向前找 } // 若是找到了还在等待锁的节点,则唤醒它 if (s != null) LockSupport.unpark(s.thread); }
在上一篇文章分析 shouldParkAfterFailedAcquire
方法的时候, 咱们重点提到了当前节点的前驱节点的 waitStatus
属性, 该属性决定了咱们是否要挂起当前线程, 而且咱们知道, 若是一个线程被挂起了, 它的前驱节点的 waitStatus
值必然是Node.SIGNAL
.
在唤醒后继节点的操做中, 咱们也须要依赖于节点的waitStatus
值.
下面咱们仔细分析 unparkSuccessor
函数:
首先, 传入该函数的参数node就是头节点head, 而且条件是
h != null && h.waitStatus != 0
h!=null
咱们容易理解, h.waitStatus != 0
是个什么意思呢?
我不妨逆向来思考一下, waitStatus在什么条件下等于0? 从上一篇文章到如今, 咱们发现以前给 waitStatus赋值过的地方只有一处, 那就是shouldParkAfterFailedAcquire
函数中将前驱节点的 waitStatus
设为Node.SIGNAL
, 除此以外, 就没有了.
然而, 真的没有了吗???
其实还有一处, 那就是新建一个节点的时候, 在addWaiter
函数中, 当咱们将一个新的节点添加进队列或者初始化空队列的时候, 都会新建节点 而新建的节点的waitStatus
在没有赋值的状况下都会初始化为0.
因此当一个head节点的waitStatus
为0说明什么呢, 说明这个head节点后面没有在挂起等待中的后继节点了(若是有的话, head的ws就会被后继节点设为Node.SIGNAL
了), 天然也就不要执行 unparkSuccessor
操做了.
另一个有趣的问题是, 为何要从尾节点开始逆向查找, 而不是直接从head节点日后正向查找, 这样只要正向找到第一个, 不就能够中止查找了吗?
首先咱们要看到,从后往前找是基于必定条件的:
if (s == null || s.waitStatus > 0)
即后继节点不存在,或者后继节点取消了排队,这一条件大多数条件下是不知足的。由于虽而后继节点取消排队很正常,可是经过上一篇咱们介绍的shouldParkAfterFailedAcquire方法可知,节点在挂起前,都会给本身找一个waitStatus状态为SIGNAL的前驱节点,而跳过那些已经cancel掉的节点。
因此,这个从后往前找的目的实际上是为了照顾刚刚加入到队列中的节点,这就牵涉到咱们上一篇特别介绍的“尾分叉”了:
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); //将当前线程包装成Node Node pred = tail; // 若是队列不为空, 则用CAS方式将当前节点设为尾节点 if (pred != null) { node.prev = pred; //step 1, 设置前驱节点 if (compareAndSetTail(pred, node)) { // step2, 将当前节点设置成新的尾节点 pred.next = node; // step 3, 将前驱节点的next属性指向本身 return node; } } enq(node); return node; }
若是你仔细看上面这段代码, 能够发现节点入队不是一个原子操做, 虽然用了compareAndSetTail
操做保证了当前节点被设置成尾节点,可是只能保证,此时step1和step2是执行完成的,有可能在step3尚未来的及执行到的时候,咱们的unparkSuccessor方法就开始执行了,此时pred.next的值尚未被设置成node,因此从前日后遍历的话是遍历不到尾节点的,可是由于尾节点此时已经设置完成,node.prev = pred
操做也被执行过了,也就是说,若是从后往前遍历的话,新加的尾节点就能够遍历到了,而且能够经过它一直往前找。
因此总结来讲,之因此从后往前遍历是由于,咱们是处于多线程并发的条件下的,若是一个节点的next属性为null, 并不能保证它就是尾节点(多是由于新加的尾节点还没来得及执行pred.next = node
), 可是一个节点若是能入队, 则它的prev属性必定是有值的,因此反向查找必定是最精确的。
最后, 在调用了 LockSupport.unpark(s.thread)
也就是唤醒了线程以后, 会发生什么呢?
固然是回到最初的原点啦, 从哪里跌倒(被挂起)就从哪里站起来(唤醒)呗:
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 喏, 就是在这里被挂起了, 唤醒以后就能继续往下执行了 return Thread.interrupted(); }
那接下来作什么呢?
还记得咱们上一篇在讲“锁的获取”的时候留的问题吗? 若是线程从这里唤醒了,它将接着往下执行。
注意,这里有两个线程:
一个是咱们这篇讲的线程,它正在释放锁,并调用了LockSupport.unpark(s.thread)
唤醒了另一个线程;
而这个另一个线程
,就是咱们上一节讲的由于抢锁失败而被阻塞在LockSupport.park(this)
处的线程。
咱们再倒回上一篇结束的地方,看看这个被阻塞的线程被唤醒后,会发生什么。从上面的代码能够看出,他将调用 Thread.interrupted()
并返回。
咱们知道,Thread.interrupted()
这个函数将返回当前正在执行的线程的中断状态,并清除它。接着,咱们再返回到parkAndCheckInterrupt
被调用的地方:
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); } }
具体来讲,就是这个if语句
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;
可见,若是Thread.interrupted()
返回true
,则 parkAndCheckInterrupt()
就返回true, if条件成立,interrupted
状态将设为true
;
若是Thread.interrupted()
返回false
, 则 interrupted
仍为false
。
再接下来咱们又回到了for (;;)
死循环的开头,进行新一轮的抢锁。
假设此次咱们抢到了,咱们将从 return interrupted
处返回,返回到哪里呢? 固然是acquireQueued
的调用处啦:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
咱们看到,若是acquireQueued
的返回值为true
, 咱们将执行 selfInterrupt()
:
static void selfInterrupt() { Thread.currentThread().interrupt(); }
而它的做用,就是中断当前线程。
绕了这么一大圈,到最后仍是中断了当前线程,究竟是在干吗呢?
其实这一切的缘由都在于:
咱们并不知道线程被唤醒的缘由。
具体来讲,当咱们从LockSupport.park(this)
处被唤醒,咱们并不知道是由于什么缘由被唤醒,多是由于别的线程释放了锁,调用了 LockSupport.unpark(s.thread)
,也有多是由于当前线程在等待中被中断了,所以咱们经过Thread.interrupted()
方法检查了当前线程的中断标志,并将它记录下来,在咱们最后返回acquire
方法后,若是发现当前线程曾经被中断过,那咱们就把当前线程再中断一次。
为何要这么作呢?
从上面的代码中咱们知道,即便线程在等待资源的过程当中被中断唤醒,它仍是会不依不饶的再抢锁,直到它抢到锁为止。也就是说,它是不响应这个中断的,仅仅是记录下本身被人中断过。
最后,当它抢到锁返回了,若是它发现本身曾经被中断过,它就再中断本身一次,将这个中断补上。
注意,中断对线程来讲只是一个建议,一个线程被中断只是其中断状态被设为true
, 线程能够选择忽略这个中断,中断一个线程并不会影响线程的执行。
线程中断是一个很重要的概念,这个咱们之后有机会再细讲。(已成文,参见Thread类源码解读(3)——线程中断interrupt)
最后再小小的插一句,事实上在咱们从return interrupted;
处返回时并非直接返回的,由于还有一个finally代码块:
finally { if (failed) cancelAcquire(node); }
它作了一些善后工做,可是条件是failed为true,而从前面的分析中咱们知道,要从for(;;)中跳出来,只有一种可能,那就是当前线程已经拿到了锁,由于整个争锁过程咱们都是不响应中断的,因此不可能有异常抛出,既然是拿到了锁,failed就必定是true,因此这个finally块在这里实际上并无什么用,它是为响应中断式的抢锁所服务的,这一点咱们之后有机会再讲。
(完)
查看更多系列文章:系列文章目录