java并发编程(5):ReentrantLock源码分析
这一章主要分析可重入锁ReentrantLock的实现细节
首先展示ReentrantLock实现涉及到的类图
1.AQS简介
AQS即图中的AbstractQueuedSynchronizer类,该类是并发工具类的基础,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
(1)AQS中的成员volatile int state代表共享资源,例如ReentrantLock在加锁时,会将state以CAS方式从0修改为1,代表当前ReentrantLock对象已经被一个线程持有,另外AbstractOwnableSynchronizer类则是会记录当前资源的持有线程引用,
AQS提供了相应的方法来获取和修改state以及资源持有线程:
protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } protected final boolean compareAndSetState(int expect, int update) { // unsafe是jdk提供的原子修改工具类(CAS) return unsafe.compareAndSwapInt(this, stateOffset, expect, update); } protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; } protected final Thread getExclusiveOwnerThread() { return exclusiveOwnerThread; }
(2) AQS通过一个内置的FIFO双向队列来完成线程的排队工作(内部通过结点head和tail记录队首和队尾元素,元素的结点类型为Node类型)
/** * Head of the wait queue, lazily initialized. Except for * initialization, it is modified only via method setHead. Note: * If head exists, its waitStatus is guaranteed not to be * CANCELLED. */ private transient volatile Node head; /** * Tail of the wait queue, lazily initialized. Modified only via * method enq to add new wait node. */ private transient volatile Node tail;
(3)Node节点中thread保存当前排队的线程引用,Node结点内部的SHARED表示标记线程是因为获取共享资源失败被阻塞添加到队列中的;Node中的EXCLUSIVE表示线程因为获取独占资源失败被阻塞添加到队列中的。waitStatus表示当前线程的等待状态:
①CANCELLED=1:表示线程因为中断或者等待超时,需要从等待队列中取消等待;
②SIGNAL=-1:代表当前节点的后驱节点因竞争共享资源失败而陷入阻塞,在释放锁时应当唤醒该后续节点代表的线程。
③CONDITION=-2:表示结点在等待队列中(这里指的是等待在某个lock的condition上,关于Condition的原理下面会写到),当持有锁的线程调用了Condition的signal()方法之后,结点会从该condition的等待队列转移到该lock的同步队列上,去竞争lock。(注意:这里的同步队列就是我们说的AQS维护的FIFO队列,等待队列则是每个condition关联的队列)
④PROPAGTE=-3:表示下一次共享状态获取将会传递给后继结点获取这个共享同步状态。
2.分析ReentrantLock(非公平模式)的加锁和解锁过程
(1)加锁
假设现有线程t1,t2,t3,
①首先t1线程调用ReentrantLock的加锁方法:
ReentrantLock: public void lock() { sync.lock(); } public ReentrantLock() { sync = new NonfairSync(); }
可以看到ReentrantLock的lock()方法实质是调用其静态内部类NonfairSync的lock()方法,而NonfairSync是AQS的子类,继续分析NonfairSync的lock()方法:
final void lock() { (1)使用CAS方法修改state,成功则拿到锁,失败则锁已经被占用 if (compareAndSetState(0, 1)) (2)拿到锁,将占用线程修改为当前线程 setExclusiveOwnerThread(Thread.currentThread()); (3)获取锁失败 else acquire(1); }
因为t1是第一个获取锁的线程,所以CAS方法一定会成功,成功获取锁,当前state修改为1,占用线程引用设置为t1
AQS状态如图:
②假设此时t2线程调用ReentrantLock的加锁方法:
此时因为t1线程已经占有锁,所以CAS方法设置state失败,此时代码进入acquire(1)方法,该方法是AQS定义的模板方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法中首先调用tryAcquire方法,该方法要由AQS子类实现,在NofairSync中实现了该方法:
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
在NofairSync中则是调用了nonfairTryAcquire方法,该方法定义在Sync中:
final boolean nonfairTryAcquire(int acquires) { 1.获取当前线程 final Thread current = Thread.currentThread(); 2.获取当前锁的状态 int c = getState(); 3.c为0,代表当前没有线程占用锁 if (c == 0) { 3-1. 使用CAS方法设置state,代表已有线程占用锁 if (compareAndSetState(0, acquires)) { 3-2.设置占用线程应用 setExclusiveOwnerThread(current); 3-3。 加锁成功,返回true return true; } } 4. 判断占用锁的线程是否是当前线程(可重入锁) else if (current == getExclusiveOwnerThread()) { 4-1.计算state的新值 int nextc = c + acquires; 4-2.异常处理 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); 4-3.设置新值 setState(nextc); 4-4.加锁成功,返回true return true; } 5.加锁失败,返回false return false; }
因为t1线程已经占有锁,所以t2线程在3处会判断为false,4是可重入锁的加锁校验,如果t1线程再次加锁,则会将state值更新为2,但t2!=t1,因此最终加锁失败,返回false.
返回false之后,会调用addWaiter(Node.EXCLUSIVE)方法,该方法则是在AQS中的FIFO队列中构建一个包含t2线程引用的独占模式Node节点:
private Node addWaiter(Node mode) { 1.将当前线程以及阻塞原因(是因为SHARED模式获取state失败还是EXCLUSIVE获取失败)构造为Node结点 Node node = new Node(Thread.currentThread(), mode); 2.快速插入当前节点到队尾 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } 3.入队 enq(node); 4.返回当前节点 return node; }
AQS中FIFO采用懒加载方式,在节点入队前,tail和head一直都为null,因此此处pred将为null,将调用enq方法进行入队:
private Node enq(final Node node) { 1.自旋,直到节点入队成功 for (;;) { 2.获取尾结点 Node t = tail; 3. 队列为null,初始化队列 if (t == null) { // Must initialize 3-1。 设置哨兵节点 if (compareAndSetHead(new Node())) tail = head; } else { 4.队列已初始化 4-1.将当前节点的前驱节点设置为tail node.prev = t; 4-2.CAS方式设置当前节点为新的尾结点 if (compareAndSetTail(t, node)) { 4-3. 设置原尾结点的后续节点为当前节点 t.next = node; 4-4. 返回原尾结点 return t; } } } }
进入enq方法,因为队列还未初始化,所以进入3-1分支,设置哨兵节点,随后进入第二次循环,将当前节点设置成新的尾结点,并返回原尾结点。
再次回顾acquire方法:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
这时已将t2线程放入到阻塞队列中,接下来将调用acquireQueued方法,此时整个队列情况如图:
acquireQueued方法如下,其入参则是新加入的t2线程节点:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; 1.自旋,只有获取锁成功或者被中断才能退出循环 for (;;) { 2.获取当前节点前驱节点 final Node p = node.predecessor(); 3.当前节点前驱节点为头节点,则尝试获取锁 if (p == head && tryAcquire(arg)) { 3-1.获取锁成功,则设置当前节点为新的头节点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } 4.前驱节点不是头节点或者获取锁失败,则确认是否park()自己 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //park()自己 interrupted = true; } } finally { //若当前前程因为中断或超时而取消排队, if (failed) cancelAcquire(node); } }
根据acquireQueued方法,t2节点的前驱节点确实是头节点,但因为t1线程持有锁,所有tryAcquire方法一定失败,此时应当调用shouldParkAfterFailedAcquire方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 1.获取前驱节点状态 int ws = pred.waitStatus; 2.如果前驱节点状态为SIGNAL,则返回true,代表可以阻塞该线程 if (ws == Node.SIGNAL) return true; 3.从队列中删除该节点前所有状态为取消的节点 if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { 4.设置前驱节点状态为SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } 5.返回false,代表不可以阻塞该线程 return false; }
分析shouldParkAfterFailedAcquire方法,根据前面绘制的队列示意图,pred节点的waitStatus为0,则将调用CAS方法将waitStatus设置为SIGNAL,即-1,并返回false,新的队列如图所示:
这是会进入acquireQueued方法的第二次循环,由于t1线程当前还持有锁,所以还是会进入shouldParkAfterFailedAcquire方法,但此时pred节点的waitStatus已经为SIGNAL,所以会直接返回true,这里我们可以看到AQS中Node节点的waitState为SIGNAL时,
代表其后驱节点因竞争互斥资源失败而陷入阻塞。
shouldParkAfterFailedAcquire返回true后,就会执行parkAndCheckInterrupt方法:
private final boolean parkAndCheckInterrupt() { 1.jdk提供的阻塞线程方法,只有调用unpark方法或者被中断,才能唤醒 LockSupport.park(this); 2.返回线程中断状态 return Thread.interrupted(); }
在parkAndCheckInterrupt方法里我们可以看到t2线程竞争互斥资源失败后,首先进入AQS的等待队列,并设置好前驱节点的状态,此时就会调用park方法阻塞自己,直到被唤醒。
那么何时会被唤醒呢?不考虑中断,那就只能是持有锁的线程在释放锁之后,唤醒等待队列中的线程了。
③假设此时t3线程调用ReentrantLock的加锁方法:
t3线程和t2线程的加锁流程一样,这里就不详细介绍,只给出最后AQS中等待队列的状态图.
(2)解锁
假设此时t1线程调用ReentrantLock的释放锁方法。
public void unlock() { sync.release(1); }
unlock方法直接调用AQS的子类Sync的release方法,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; }
release方法首先调用了tryRelease方法,该方法在Sync类中定义。
protected final boolean tryRelease(int releases) {
1. 计算state最终值 int c = getState() - releases;
2. 校验当前线程是否是加锁线程 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false;
3.state为0,代表锁已经被释放,可以唤醒等待线程 if (c == 0) { free = true; setExclusiveOwnerThread(null); }
4.修改state setState(c); return free; }
tryRelease方法主要在修改state变量,如果修改之后,state为0,则返回true,否则返回false。
因为t1线程只加锁一次,此时state值为1,所有调用tryRelase方法之后,state为0,返回true,此时调用unparkSuccessor方法,该方法定义在AQS中。
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ 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); }
unparkSuccessor方法主要在唤醒头节点的后继节点,如果后继节点已经被取消,则从队列最后开始寻找最接近队首的可唤醒节点。
调用LockSupport.unpark方法唤醒指定线程后,释放锁的整个流程就结束了。
释放锁的主要流程总结:
1.修改AQS的state
2.唤醒等待队列中头节点的后继节点,如果后继节点被取消,则从后往前,唤醒满足要求的最接近队首的节点对应的线程。
这里t2线程便被唤醒,回顾t2加锁时的流程,t2被阻塞在parkAndCheckInterrupt方法中,被唤醒后,继续执行自旋方法。
因为t2线程的前驱节点就是头节点,因此t2线程调用tryAcquire方法,因为t1线程已经释放锁,因此t2线程能够加锁成功,接着将自己设置为头节点。此时整个加锁和解锁流程完全结束。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; 1.自旋,只有获取锁成功或者被中断才能退出循环 for (;;) { 2.获取当前节点前驱节点 final Node p = node.predecessor(); 3.当前节点前驱节点为头节点,则尝试获取锁 if (p == head && tryAcquire(arg)) { 3-1.获取锁成功,则设置当前节点为新的头节点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } 4.前驱节点不是头节点或者获取锁失败,则确认是否park()自己 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //park()自己 interrupted = true; } } finally { //若当前前程因为中断或超时而取消排队, if (failed) cancelAcquire(node); } }