ReentrantLock原理探究(二)
来源:互联网 发布:怎么在淘宝上买东西 编辑:程序博客网 时间:2024/05/17 22:19
前言
上篇ReentrantLock原理探究(一)介绍了ReentrantLock类的使用说明,详细解析了关于非公平锁的lock()
过程。这篇我们继续分析。
本文所有测试代码可在github.learn-sample找到.
三、源码解析
2. unlock()方法
公平锁与非公平锁的unlock()
方法相同,就不用区别了。unlock()
方法调用了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()
,这个方法实现在ReentrantLock
的内部类Sync
而不是分别留在FairSync
或NonfairSync
中,这也说明了释放锁的过程与锁的公平性无关。
protected final boolean tryRelease(int releases) { //每次释放锁状态减1,若为0说明锁已释放完毕 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //若锁已释放,将表示占有锁的线程变量设为null if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
这个方法比较简单,仅仅是检查并设置状态。不过仍需注意的是,释放锁的方法必须由占有锁的线程调用,否则,则会抛出IllegalMonitorStateException
异常。
假设线程1已执行完,调用unlock()
执行到此,会将表示阻塞线程个数状态的state
设置0。返回true
,进if语句。
Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true;
此时head
节点是没有线程的空节点,其后跟着表示线程2的线程节点。而head
节点的waitStatus
为SIGNAL
,值为-1。
进入unparkSuccessor(h)
方法,h
是队列的头节点。
private void unparkSuccessor(Node node) { /* 若节点`waitStatus`为负值,设置0 */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* 这里有JDK注释,大意是,解锁阻塞队列中的线程。一般情况下是下一个节点(head后面的节点),但若这个节点表示的线程被取消了,则往后遍历直到找到需释放的(waitStatus为负值的)。 */ 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; } //解锁head节点后的第一个等待节点 if (s != null) LockSupport.unpark(s.thread); }
LockSupport.unpark(s.thread)
方法应该不陌生,上篇中就是用LockSupport.park(this)
方法将当前线程锁住的,底层调用的是Unsafe
类方法。这里暂不研究。
这里和lock()
的过程对接一下,当线程2完成unlock()
过程。唤起线程1。
此时线程1在acquireQueued()
的循环中,
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; }
走循环的第一个if,执行完tryAcquire()
后线程2已获取锁。这里进if,设置头节点
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
经过设置,阻塞队列重新恢复成只有一个空的头节点的状态。这样获取、释放锁的流程就走完了。其他类似。
3. 公平锁的获取方式
公平锁与非公平锁的解锁流程是一致的,区别只在锁的过程,即FairSync
的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()
方法。此方法会判阻塞队列前面是否有其他线程在等待。
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
其他步骤与非公平锁一致。
4. 线程取消
前面一直提到线程取消,这里解析一下
在acquireQueued()
方法中finally
代码段是处理异常发送后取消线程的,主要调用了cancelAcquire(node)
。node为要取消的线程表示的节点。
private void cancelAcquire(Node node) { //节点为空直接返回 if (node == null) return; node.thread = null; // 跳过被取消的(即waitStatus大于0)节点 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; //node前置节点的后置节点,不一定是node Node predNext = pred.next; // 将节点状态改为取消状态 node.waitStatus = Node.CANCELLED; //若节点是尾节点,直接设为null if (node == tail && compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { //前置节点不是head,表明该节点前有阻塞的线程 int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { Node next = node.next; if (next != null && next.waitStatus <= 0) //连接该节点的前置节点和后续节点,即去掉该节点 compareAndSetNext(pred, predNext, next); } else { //前置节点是head unparkSuccessor(node); } node.next = node; // help GC } }
注释都说差不多了,最后调用的unparkSuccessor
前面解锁过程已经提过。可以发现,如果外界有异常导致当前线程被取消时,会设置状态,并根据该节点所在位置恰当退出队列。
四、Condition研究
上篇示例中演示了Condition()
的用法,这里简要分析一下它的底层。
1. 类结构
AQS内部类ConditionObject
实现了Condition
接口,主要有下列两个字段.
//条件队列第一个节点 private transient Node firstWaiter; //条件队列最后一个节点 private transient Node lastWaiter;
2.await()方法
await()
方法需结合Lock类使用,上篇示例中已经提过,此方法与Object.wait()
功能相近,阻塞当前线程,直到调用相同锁的signal()
方法。
await()
方法有众多版本,如awaitNanos(long nanosTimeout)、awaitUntil(Date date)、await(long time, TimeUnit unit)
。这里以await()
为例。
为表述方便,我们以Condition测试用例 为实例,有线程A、B两个线程。线程A遍历到20调用
await()
阻塞,线程B休眠2秒唤醒线程A。
public final void await() throws InterruptedException { //显式检查中断 if (Thread.interrupted()) throw new InterruptedException(); //用当前线程构造等待条件节点 Node node = addConditionWaiter(); //当前线程释放锁 int savedState = fullyRelease(node); int interruptMode = 0; //如果不在阻塞队列中,将自己锁住 while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } //以下是被唤醒后重新竞争锁,与lock过程类似 if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
await()
方法为暂停该线程,等待唤醒。线程A调用后会阻塞在上面的while
循环中。等待唤醒。
首先会检查中断标志,这也是await()
方法会响应中断的实现点。addConditionWaiter()
以当前线程为节点,fullyRelease()
释放当前线程持有的锁,内部调用的是release()
。再判断节点是否在队列中,若不在,在循环中阻塞该线程。循环后面的语句是经signal()
唤醒后竞争锁的过程。与lock
就关联起来了。
3.signal()方法
signal()
主要作用就是唤醒被await
的线程。与await()
一样,该方法必须在获取锁的情况下使用。
public final void signal() { //当前线程是否是锁的持有者 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); }
主要处理逻辑在doSignal(first)
,first
是条件队列的首节点。
private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; //遍历找到第一个可以释放的节点(不能是被取消的) } while (!transferForSignal(first) && (first = firstWaiter) != null); }
看看transferForSignal(first)
final boolean transferForSignal(Node node) { // if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; // 将要唤醒的线程节点加入阻塞队列等待唤醒 Node p = enq(node); int ws = p.waitStatus; //该节点被取消,直接释放解锁 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }
总结一下signal()
的流程。
线程A已被阻塞(处于while
循环),此时在等待队列中(由Condition维护,不同于AQS的同步阻塞队列)。线程B调用signal()
后线程A从等待队列取出,放到同步阻塞队列。此时正好破解了线程A的while循环。
// await()方法 while (!isOnSyncQueue(node)) //是否处于阻塞队列
这样一来流程就清晰了。等线程B执行完后(执行完unlock()
释放锁),由于同步阻塞队列没有其他阻塞线程,线程A就可获取锁继续执行了。
相反,假设阻塞队列有排队的线程,即不止一个线程(这里只有线程A)等待释放,走await()
后面的竞争锁过程,其实是lock
阻塞的过程,这里就不提了。
参考文章
- 怎么理解Condition
- ReentrantLock实现原理深入探究
- AbstractQueuedSynchronizer的介绍和原理分析
- Java线程(九):Condition-线程通信更高效的方式
- ReentrantLock原理探究(二)
- ReentrantLock原理探究(一)
- ReentrantLock实现原理深入探究
- ReentrantLock实现原理深入探究
- ReentrantLock 实现原理深入探究
- ReentrantLock实现原理深入探究
- ReentrantLock实现原理深入探究
- ReentrantLock实现原理深入探究
- ReentrantLock实现探究
- Annotation 探究 (二)
- 尝试阅读ReentrantLock、AbstractQueuedSynchronizer源码(二)
- Zookeeper(一)原理探究
- 由浅入深探究 MySQL索引结构原理、性能分析与优化(二)
- 【SLAM】(二)Cartographer的原理探究——GraphSLAM理论基础
- ReentrantLock实现原理
- java可重入锁ReentrantLock原理
- ReentrantLock实现原理分析
- ReentrantLock实现原理
- 绿色无捆绑大白菜U盘启动盘制作
- 全排列
- 1316(简单) self number
- linux安装文件
- lnmp的缓存
- ReentrantLock原理探究(二)
- 挑战程序竞赛系列(7):2.1一往直前!贪心法(区间)
- STM32 调试记录
- 最近很是迷茫啊
- POJ
- 分享一个gif截图工具
- ajax的data偶然数据溢出。。。
- 循环双向链表
- 网络异常模拟测试方法