一、本章概述
AQS系列的前三个章节,我们通过ReentrantLock的示例,分析了AQS的独占功能。
本章将以CountDownLatch
为例,分析AQS的共享功能。CountDownLatch,是J.U.C中的一个同步器类,可作为倒数计数器使用。
CountDownLatch示例
假设现在有3个线程,ThreadA、ThreadB、mainThread,CountDownLatch初始计数为1:
CountDownLatch switcher = new CountDownLatch(1);
线程的调用时序如下:
//ThreadA调用await()方法等待
//ThreadB调用await()方法等待
//主线程main调用countDown()放行
二、AQS共享功能的原理
1. 创建CountDownLatch
CountDownLatch的创建没什么特殊,调用唯一的构造器,传入一个初始计数值,内部实例化一个AQS子类:
CountDownLatch switcher = new CountDownLatch(1);
public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }
private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; Sync(int count) { setState(count); }
可以看到,初始计数值count其实就是同步状态值,在CountDownLatch中,同步状态State表示CountDownLatch的计数器的初始大小。
2. ThreadA调用await()方法等待
CountDownLatch的await方法是响应中断的,该方法其实是调用了AQS的acquireSharedInterruptibly方法:
public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1); }
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0)//该方法尝试获取锁,由AQS子类实现 doAcquireSharedInterruptibly(arg); }
注意tryAcquireShared方法,该方法尝试获取锁,由AQS子类实现,其返回值的含义如下:
State | 资源的定义 |
---|---|
小于0 | 表示获取失败 |
等于0 | 表示获取成功 |
大于0 | 表示获取成功,且后继争用线程可能成功 |
CountDownLatch中的tryAcquireShared实现相当简单,当State值为0时,永远返回成功:
protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; }
我们之前说了在 CountDownLatch中,同步状态State表示CountDownLatch的计数器的初始值, 当State==0
时,表示无锁状态,且一旦State变为0,就永远处于无锁状态了, 此时所有线程在await上等待的线程都可以继续执行。这就是 共享锁的含义。
而在 ReentrantLock中,State==0
时,虽然也表示无锁状态,但是只有一个线程可以重置State的值。这就是独占锁的含义。
好了,继续向下执行,ThreadA尝试获取锁失败后,会调用doAcquireSharedInterruptibly:注意:这里是自旋操作,后面或再次执行这段代码逻辑
1.doAcquireSharedInterruptibly方法分析开始
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED);//重点:包装成共享节点,插入到等待队列 boolean failed = true; try { for (;;) { //自旋阻塞线程或尝试获取锁 final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg);//尝试获取锁 if (r >= 0) { //大于等于0,表示获取成功 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //检查是否需阻塞当前结点 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
1.1 分析上述代码:首先通过addWaiter方法,将ThreadA包装成共享结点,插入等待队列,插入完成后队列结构如下:
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; }
1.2 然后会进入自旋操作,先尝试获取一次锁,显然此时是获取失败的(主线程main还未调用countDown,同步状态State还是1)。
然后判断是否要进入阻塞(shouldParkAfterFailedAcquire):
//判断是否需要阻塞当前线程,注意:CLH队列的一个特点是:将当前结点的状态保存在它的前驱中 //前驱状态是【-1:等待唤醒】时,才会阻塞当前线程 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; //前驱结点的状态 if (ws == Node.SIGNAL) //SIGNAL:后续结点需要被唤醒(这个状态说明当前结点的前驱结点将来会来唤醒我。我可以安心睡大觉了,哈哈(阻塞)) return true; if (ws > 0) { //CANCELED:取消(说明前驱结点(线程)因意外被中断/取消,需要将其从等待队列移除) do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //对应独占功能来说,这里表示结点初始状态为0 } return false; }
1.3 好了,至此,ThreadA进入阻塞态,最终队列结构如下:
3. ThreadB调用await()方法等待
流程和步骤2完全相同,调用后ThreadB也被加入到等待队列中:
4. 主线程main调用countDown()放行
ThreadA和ThreadB调用了await()
方法后都在等待了,现在主线程main开始调用countDown()
方法,该方法调用后,ThreadA和ThreadB都会被唤醒,并继续往下执行,达到类似门栓的作用。
来看下countDown方法的内部:
public void countDown() { sync.releaseShared(1); //将计数器值减少1(最小为0),为0,则释放所有等待线程 }
该方法内部调用了AQS的releaseShared方法,先尝试一次释放锁,tryReleaseShared方法是一个钩子方法,由CountDownLatch实现,当同步State状态值首次变为0时,会返回true:
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c-1;//将计数器值减少1 if (compareAndSetState(c, nextc))//CAS设置状态值 return nextc == 0;//如果state为0则返回true,否则返回false } }
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //将头结点的等待状态置为0,表示将要唤醒后继节点 continue; // loop to recheck cases unparkSuccessor(h); //唤醒后继节点 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
先调用compareAndSetWaitStatus将头结点的等待状态置为0,表示将唤醒后续结点(ThreadA),成功后的等待队列结构如下:
private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) { return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update); }
然后调用unparkSuccessor唤醒后继结点(ThreadA被唤醒后会从原阻塞处继续往下执行,这个在步骤5再讲):
//唤醒当前节点的后继节点(线程) private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0)//SIGNAL=-1 compareAndSetWaitStatus(node, ws, 0); //预置当前节点的状态为0,表示后续节点即将被唤醒 Node s = node.next; //后继节点 //正常情况下,会直接唤醒后继节点 //但是如果后继节点处于1:CANCELLED状态时(说明被取消了),会从队尾开始,向前找到第一个未被CANCELLED的结点 if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) //从tail开始向前查找是为了考虑并发入队(enq)的情况 if (t.waitStatus <= 0) s = t; } if (s != null)//唤醒结点 LockSupport.unpark(s.thread); }
此时,等待队列结构如下:
5. ThreadA从原阻塞处继续向下执行
ThreadA被唤醒后,会从原来的阻塞处继续向下执行:
由于是一个自旋操作,ThreadA会再次尝试获取锁,由于此时State同步状态值为0(无锁状态),所以获取成功。然后调用setHeadAndPropagate方法:注意:这里执行之前的自旋操作
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { //包装成共享结点,插入等待队列 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //自旋阻塞线程,或尝试获取锁 final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); //尝试获取锁 if (r >= 0) { //大于等于0表示获取成功 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //检查释放需要阻塞当前结点 throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
setHeadAndPropagate方法把ThreadA结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:
//将当前结点 node设置为头结点,并尝试唤醒并释放后继共享结点 private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node); //判断是否需要唤醒后继节点 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; //将当前结点设置为头结点 if (s == null || s.isShared()) doReleaseShared(); //释放并唤醒后继节点 } }
①将ThreadA变成头结点
②调用doReleaseShared方法,释放并唤醒ThreadB结点
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //将头结点的等待状态置为0,表示将要唤醒后继节点 continue; // loop to recheck cases unparkSuccessor(h); //唤醒后继节点 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
6. ThreadB从原阻塞处继续向下执行
ThreadB被唤醒后,从原阻塞处继续向下执行,这个过程和步骤5(ThreadA唤醒后继续执行)完全一样。
setHeadAndPropagate方法把ThreadB结点变为头结点,并根据传播状态判断是否要唤醒并释放后继结点:
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below setHead(node);//将当前节点设置为头结点 //判断是否需要唤醒后继节点 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; if (s == null || s.isShared()) doReleaseShared();//释放并唤醒后继节点 } }
①将ThreadB变成头结点
②调用doReleaseShared方法,释放并唤醒后继结点(此时没有后继结点了,则直接break):
private void doReleaseShared() { for (;;) { Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //将头结点的等待状态置为0,表示将要唤醒后继节点 continue; // loop to recheck cases unparkSuccessor(h); //唤醒后继节点 } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
最终队列状态如下:
三、总结
AQS的共享功能,通过钩子方法tryAcquireShared暴露,与独占功能最主要的区别就是:
共享功能的结点,一旦被唤醒,会向队列后部传播(Propagate)状态,以实现共享结点的连续唤醒。这也是共享的含义,当锁被释放时,所有持有该锁的共享线程都会被唤醒,并从等待队列移除。
参考链接