实现上一篇文章的未尽事宜:
- ReentrantLock的lock、unlock源码剖析
- Condition的await、signal源码剖析
ReentrantLock#lock
lock办法最终是由sync实现的,偏心锁的sync是FairSync,非偏心锁是UnfairSync。
两者lock办法的区别是,偏心锁FairSync间接调用acquire(1)办法,非偏心锁UnfairSync则首先尝试取得锁资源(间接尝试批改锁状态)、获取不到才调用acquire(1)办法。
acquire办法由AQS实现。
AbstractQueuedSynchronizer#acquire
首先调用tryAcquire办法尝试取得锁资源,如果获取不胜利的话,调用acquireQueued办法进入队列排队期待。
如果是通过acquireQueued办法通过排队获取到锁资源、并且办法返回true的话,阐明在线程排队期待锁资源的过程中收到了中断信号,然而因为线程处于挂起状态、尚未取得锁资源,不能对CLH队列做操作,所以须要等到获取到锁资源之后、再调用selfInterrupt()中断。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
上面咱们看一下tryAcquire及acquireQueued办法。
FairSync#tryAcquire以及UnFairSync#tryAcquire
偏心锁与非偏心锁的tryAcquire办法的实现逻辑不同。
偏心锁FairSync#tryAcquire办法判断锁状态处于闲暇的话,首先调用hasQueuedPredecessors办法,判断以后CLH队列中是否存在比以后线程等待时间更久的线程,没有的话才尝试获取锁资源(CAS形式批改锁状态)。
非偏心锁UnFairSync#tryAcquire办法在判断锁处于闲暇状态的话,间接尝试获取锁资源(CAS形式批改锁状态)。
不论是FairSync还是UnFairSync的tryAcquire办法,如果判断锁资源是被以后线程独占,则能够间接再次获取锁资源,锁状态state在以后值的根底上加1。这也就是可重入锁的意思,同一线程能够屡次取得锁资源。
AbstractQueuedSynchronizer#acquireQueued
在tryAcquire获取锁资源失败后,调用acquireQueued办法,办法名很好的阐明了办法的作用:通过排队取得锁资源。
办法的参数addWaiter(Node.EXCLUSIVE), arg),咱们先看一下这个办法。
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
代码其实很简略,首先将以后线程封装成Node,因为ReentrantLock是独占锁,所以Node的mode是独占。
而后如果队列不空的话,将以后节点的prev设置为尾结点(留神这个时候的操作不是CAS的,也没有取得锁),之后才利用CAS操作将尾结点设置为以后节点(到这一步以后节点才正式退出队列),返回。
否则如果队列空的话,调用enq办法把以后节点退出队列,enq办法通过自旋+CAS的形式将以后节点入队(退出到尾结点),如果以后队列空的话,为CLH队列创立一个空的头节点后再将以后节点作为尾结点退出到队列中。
值得关注的是enq办法的操作形式,与addWaiter在队列不空的时候尝试间接将以后节点退出队列尾部的操作逻辑统一(关注点1):操作是在没有获取到锁资源的前提下进行的,在不应用CAS的状况下首先将以后节点的prev指向尾结点,而后再尝试CAS扭转队列的尾结点为以后节点。如果本次CAS操作失败(有其余线程曾经退出队列,队列的尾结点有变动),则自旋再来一次,如此重复直到胜利退出队列。
而后再来看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); } }
定义了一个interrupted变量,表明以后线程排队挂起期待的过程中是否收到了中断信号。
判断以后节点的前一个节点如果是头节点的话,阐明以后节点排队排到了,该出队获取锁资源了,所以就调用tryAcquire尝试,如果的确拿到锁资源了,则头节点出队,以后节点变成头节点,线程胜利拿到了锁资源,返回interrupted。
否则,前一节点不是头节点的话,阐明还得排队期待,所以首先调用shouldParkAfterFailedAcquire:办法名真的是太好了,倡议大家学习一下,不要胆怯办法名称太长,相比之下可能表白意思更重要!这个办法是判断在没有拿到锁资源后是否须要通过park挂起以后线程。
shouldParkAfterFailedAcquire的代码逻辑:
上一节点的waitStatus=Node.SIGNAL的话,阐明上一节点处于期待状态,则返回true,能够挂起。
如果上一节点曾经被勾销(waitStatus>0)则循环查看所有勾销状态的上一节点,将所有勾销状态的上一节点全副出队列。
如果上一节点状态为0,则尝试CAS批改上一节点状态为Node.SIGNAL,返回false(还不能挂起,始终要等到上一节点状态为Node.SIGNAL才能够挂起),返回false之后调用方不挂起线程、在下次自旋过程中挂起。
批改上一节点状态为SIGNAL后才挂起是一种“负责任的挂起”态度(关注点2),是为了挂起之后可能顺利的被唤醒,因为锁占用线程在实现操作调用unlock开释锁资源之后,是针对队列中状态为Node.SIGNAL的节点做唤醒。
接下来,线程能够被挂起了,调用parkAndCheckInterrupt办法:
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
很简略,挂起线程,(关注点3)被唤醒后返回线程的中断状态(挂起过程中如果收到中断信号则返回true,否则,没有收到中断信号则返回false)。
所以咱们能够看到,如果线程排队、排队挂起期待锁资源的过程中如果收到了中断信号,则acquireQueued办法会返回true。
所以咱们也就应该可能了解acquire办法中,调用acquireQueued办法返回为true的话,通过selfInterrupt()办法补发一个中断的起因了。
如果没有中断的话,持续自旋,再次判断以后节点的上一个节点是否是头节点,是的话就tryAcquire,不是的话就持续shouldParkAfterFailedAcquire、parkAndCheckInterrupt......如此重复,直到tryAcquire胜利,获取到锁资源!
lock办法源码剖析结束!
ReentrantLock#unlock
unlock办法间接调用sync的release:
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; }
首先调用ReentranceLock的tryRelease办法。之后查看CLH队列不空、且首节点waitStatus!=0的话(参见关注点2)则调用unparkSuccessor(h)。
先看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; setExclusiveOwnerThread(null); } setState(c); return free; }
tryRelease办法更新锁状态,如果更新后锁状态为0则开释锁setExclusiveOwnerThread(null),返回true。
开释锁资源胜利,调用AQS的unparkSuccessor(h):
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); }
如果以后节点(调用时传入的是头节点)waitStatus<0,CANCELLED、SIGNAL、CONDITION或PROPAGATE都是小于0的状态,然而这里可能的也就是SIGNAL,则批改状态为0。
一般来讲须要唤醒的节点就是以后节点的下一节点,然而如果下一节点被勾销了,就只能找前面的没有被勾销的节点。不过unparkSuccessor采纳的算法是从尾结点向前找,始终找到以后节点,最初被找到的那个节点就是间隔以后节点最近的那个未被勾销的节点。
有一个疑难是:既然是双向队列,这里为什么要从尾结点向前查找,为什么不从以后节点向后查找?两者相比显然向后查找效率更高而且更容易了解。
倡议联合关注点1来了解,因为节点退出队列的时候蕴含3步:
- 首先是设置以后节点的prev为原尾结点
- CAS批改尾结点为以后节点
- 批改原尾结点的next为以后节点
这3步操作并没有锁爱护,3步一起看不是原子操作,只有独自看第2步是原子操作。所以执行完第1步、执行完第2步以后线程都有可能被cpu挂起。
为了不便阐明问题,咱们假如以后队列有H头节点,3个节点N1、N2、N3,N3是尾结点:
H -> N1 -> N2 -> N3(T)
假如线程1申请获取锁资源,然而锁资源被线程2独占了,线程1只能封装为N4申请加入队列。假如线程1的退出队列操作执行到上述第2步实现之后被挂起了。队列变成:
H -> N1 -> N2 -> N3 -> N4(T)
因为线程1还没有执行完退出队列的第3步,所以N4变成了尾结点,N4的prev是N3,然而N3的next是null。
假如这个时候碰巧,N1、N2、N3都被勾销了。
好了,场景筹备实现了。
线程2开释锁资源之后,该调用unparkSuccessor唤醒期待线程了。
如果向后查找,从头节点H开始,N1、N2、N3都被勾销所以就被跳过了,N3的next是null,找不到尾结点N4......就出问题了。
推导一下向前查找应该是不存在这个问题的,这个算法应该是和退出队列算法(关注点1)匹配的。
好了,unparkSuccessor找到了应该被唤醒的排队节点后,唤醒该节点的线程,被唤醒的线程会从关注点3继续执行。
unlock剖析结束!
Condition#await
Conditon提供了await()、awaitUninterruptibly()、awaitNanos(long nanosTimeout)、await(long time, TimeUnit unit)、awaitUntil(Date deadline)等期待办法,其实次要也就是期待时长、可中断期待或不可中断期待的区别,代码逻辑大同小异。所以咱们就以await()作为例子来剖析。
AQS中蕴含了Condition接口的一个默认实现ConditionObject,await()办法是ConditionObject实现的:
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); long savedState = fullyRelease(node); int interruptMode = 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
第一步,以后节点入Condition队列排队:调用addConditionWaiter办法,addConditionWaiter办法负责将以后线程封装为mode为CONDITION的Node,并将该Node放入Condition的队列中排队。
入Condition队列的逻辑也很简略,由lastWaiter指定的队尾节点的nextWaiter指定为以后节点,以后节点变为lastWaiter。addConditionWaiter同时也会清理队列中被勾销的节点。
须要留神的是,Condition队列中的首节点不是空架子,是实实在在排队期待的线程!
第二步,用刚创立的节点node调用fullyRelease办法。
fullRelease办法的逻辑是:以后线程放弃对ReentrantLock的锁定,开释曾经获取的锁资源。放弃锁资源的起因是,以后线程须要挂起期待,必须等其余线程唤醒之后能力继续执行操作,持有锁资源去睡觉显然是不适合的,是对锁资源的节约,或者会造成死锁,所以就肯定要开释锁资源,只有把锁资源交给其余线程,其余线程有机会获取锁资源,才有可能会对以后线程收回signal信号、从新唤醒以后线程。
fullRelease办法最终会调用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办法,胜利则意味着以后线程开释掉了锁资源,间接查看CLH队列头节点如果不空、状态为期待唤醒的话,唤醒该节点。
第三步,循环查看刚刚退出到Condtion队列中的节点node如果不在CLH队列中,则挂起以后线程(关注点4)。直到以后线程被其余线程通过signal操作唤醒之后,则该节点node会被放入到CLH队列中(满足循环完结条件),完结循环。
第四步,曾经完结Condition排队了,节点曾经在CLH队列中了。咱们晓得这个时候以后线程必须要再次获取到锁资源能力持续。所以,应用以后节点node调用acquireQueued办法(这个办法后面剖析过,咱们曾经很相熟了)
第五步,如果Condition队列中以后节点node的下一节点nextWaiter不空的话,调用unlinkCancelledWaiters()革除被勾销的节点。
第六步,挂起期待过程中如果产生中断的话,调用reportInterruptAfterWait解决中断。
await办法剖析实现,接下来看signal办法。
Condition#signal
signal办法的调用场景是:以后业务实现操作之后,可能会导致在Condition对象的队列中排队的线程满足相应条件,因而须要调用该COndition对象的signal办法唤醒期待线程。
public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); }
signal办法首先获取到队列中的第一个节点,而后调用doSignal办法:
private void doSignal(Node first) { do { if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; } while (!transferForSignal(first) && (first = firstWaiter) != null); }
首先解决好排队队列,首节点出队列,如果队列空的话,做相应的解决。
而后应用以后节点调用transferForSignal办法,如果transferForSignal返回false,则获取fisrt为firstWaiter(其实就是用队列中的下一个节点持续循环)。
transferForSignal办法
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; }
如果将以后节点从CONDITION状态批改为0失败,*表明以后节点状态不正确,可能被勾销,或者曾经被其余线程唤醒中(曾经批改状态为0了),则返回false。
否则,如果状态批改胜利,该节点入CLH队列,并查看CLH队列中该节点的上一节点如果曾经被勾销、或者批改上一节点状态为SIGNAL失败的话(非凡状况),间接唤醒以后节点的线程从新同步(关注点4),返回true。
下面所说的非凡状况下,上一节点可能曾经被勾销,或者曾经被其余线程批改了状态,所以以后节点可能取得了出队列从而取得锁资源的机会,所以就唤醒线程做一次尝试。
否则,失常批改上一节点状态为SIGNAL后,以后节点退出CLH队列中失常排队,期待被ReentranceLock的unlock办法唤醒(唤醒后程序逻辑仍然会到关注点4)!
小结
Finally,ReentranceLock以及Condition剖析完了,欢送斧正谬误!
Thanks a lot!
上一篇 Java并发编程 Lock Condition & ReentrantLock(一)