tryAcquire() 一旦返回 false,就会则进入 acquireQueued() 流程,也就是基于CLH队列的抢占模式,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用 addWaiter() 实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点。
代码如下:
//AbstractQueuedSynchronizer.addWaiter() private Node addWaiter(Node mode) { // 初始化一个节点,用于保存当前线程 Node node = new Node(Thread.currentThread(), mode); // 当CLH队列不为空的视乎,直接在队列尾部插入一个节点 Node pred = tail; if (pred != null) { node.prev = pred; //如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾) if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 当CLH队列为空的时候,调用enq方法初始化队列 enq(node); return node; } private Node enq(final Node node) { //在一个循环里不停的尝试将node节点插入到队尾里 for (;;) { Node t = tail; if (t == null) { // 初始化节点,头尾都指向一个空节点 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
- 将节点增加到CLH队列后,进入 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)) {// 通过tryAcquire获得锁,如果获取到锁,说明头节点已经释放了锁 setHead(node);//将当前节点设置成头节点 p.next = null; // help GC//将上一个节点的next变量被设置为null,在下次GC的时候会清理掉 failed = false;//将failed标记设置成false return interrupted; } //中断 if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞 parkAndCheckInterrupt())// 阻塞,返回线程是否被中断 interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
- 如果尝试获取锁失败,就会进入 shouldParkAfterFailedAcquire() 方法,会判断当前线程是否阻塞
/** * 确保当前结点的前驱结点的状态为SIGNAL * SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程 * 只有确保能够被唤醒,当前线程才能放心的阻塞。 */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果前驱节点状态为SIGNAL //表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点 return true; if (ws > 0) {//ws > 0 代表前驱节点取消了 do { node.prev = pred = pred.prev;//不断的把前驱取消了的节点移除队列 } while (pred.waitStatus > 0); pred.next = node; } else { //初始化状态,将前驱节点的状态设置成SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
当进入阻塞阶段,会进入 parkAndCheckInterrupt() 方法,则会调用 LockSupport.park(this) 将当前线程挂起。代码如下:
// 从方法名可以看出这个方法做了两件事 private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//挂起当前的线程 // 如果当前线程已经被中断了,返回true,否则返回false // 有可能在挂起阶段被中断了 return Thread.interrupted(); }
4.2 非公平锁 NonfairSync.unlock()
2.1 unlock()方法的示意图
2.1 unlock()方法详解
调用 unlock() 方法,其实是直接调用 AbstractQueuedSynchronizer.release() 操作。
进入 release() 方法,内部先尝试 tryRelease() 操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。
如果 tryRelease 成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。
一旦下一个节点的线程被唤醒,被唤醒的线程就会进入 acquireQueued() 代码流程中,去获取锁。
代码如下:
public void unlock() { sync.release(1); } public final boolean release(int arg) { //尝试在当前锁的锁定计数(state)值上减1, if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者处于CANCEL状态,或者是SIGNAL表示下一个线程在等待其唤醒。也就是说waitStatus不为零表示它的后继在等待唤醒。 unparkSuccessor(h); //成功返回true return true; } //否则返回false return false; } private void unparkSuccessor(Node node) { int ws = node.waitStatus; //如果waitStatus < 0 则将当前节点清零 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点 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); }
当然在 release() 方法中不仅仅只是将 state - 1 这么简单,-1 之后还需要进行一番处理,如果 -1 之后的 新state = 0 ,则表示当前锁已经被线程释放了,同时会唤醒线程等待队列中的下一个线程。
protected final boolean tryRelease(int releases) { int c = getState() - releases; //判断是否为当前线程在调用,不是抛出IllegalMonitorStateException异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //c == 0,释放该锁,同时将当前所持有线程设置为null if (c == 0) { free = true; setExclusiveOwnerThread(null); } //设置state setState(c); return free; } 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; // 从后往前找到离head最近,而且waitStatus <= 0 的节点 // 其实在ReentrantLock中,waitStatus应该只能为0和-1,需要唤醒的都是-1(Node.SIGNAL) for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread);// 唤醒挂起线程 }
重点:unlock最好放在finally中,因为如果没有使用finally来释放Lock,那么相当于启动了一个定时炸弹,如果发生错误,我们很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间,这也就是 ReentrantLock 不能完全替代 synchronized 的原因,因为当程序执行控制离开被保护的代码块时,不会自动清除锁。