CountDownLatch倒数计时器
我们所熟知的ReentrantLock、CountDownLatch、CyclicBarrier等同步器,其实都是通过内部类实现了AQS框架暴露的API,以此实现各类同步器功能。这些同步器的主要区别其实就是对同步状态(synchronization state)的定义不同。
同步器 | 资源的定义 |
---|---|
ReentrantLock | 资源表示独占锁。State为0表示锁可用;为1表示被占用;为N表示重入的次数 |
CountDownLatch | 资源表示倒数计数器。State为0表示计数器归零,所有线程都可以访问资源;为N表示计数器未归零,所有线程都需要阻塞。 |
Semaphore | 资源表示信号量或者令牌。State≤0表示没有令牌可用,所有线程都需要阻塞;大于0表示由令牌可用,线程每获取一个令牌,State减1,线程没释放一个令牌,State加1。 |
ReentrantReadWriteLock | 资源表示共享的读锁和独占的写锁。state逻辑上被分成两个16位的unsigned short,分别记录读锁被多少线程使用和写锁被重入的次数。 |
AQS的共享功能,通过钩子方法tryAcquireShared暴露,与独占功能最主要的区别就是:
共享功能的结点,一旦被唤醒,会向队列后部传播(Propagate)状态,以实现共享结点的连续唤醒。这也是共享的含义,当锁被释放时,所有持有该锁的共享线程都会被唤醒,并从等待队列移除。
CountDownLatch 允许一个或多个线程等待直到在其他线程中执行的一组操作完成的同步辅助。
CountDownLatch 内部维护了一个计数器,调用 await() 会阻塞当前线程,每个线程完成自己的操作之后调用countDown() 都会将计数器减一,然后会在计数器的值变为 0 之前一直阻塞,直到计数器的值变为 0,释放所有等待的线程。
CountDownLatch的初始计数值一旦降到0,无法重置。如果需要重置,可以考虑使用CyclicBarrier。
countDown()方法
private void doReleaseShared() {
for (;;) {
Node h = head;
//不是尾节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
//避免两次unpark
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)){
continue;
}
//唤醒头节点的后继节点
unparkSuccessor(h);
}else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)){
//头节点的等待状态为0
//使用CAS把当前节点状态设置为PROPAGATE,确保后面可以传递下去
continue;
}
}
//头结点无变更,退出循环
if (h == head)
break;
}
}
LockSupport
//java.util.concurrent.locks.LockSupport
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
U.park(false, 0L);
setBlocker(t, null);
}
public static void park() {
U.park(false, 0L);
}
/**
* 通过反射机制获取Thread类的parkBlocker字段对象。
* 然后通过sun.misc.Unsafe对象的objectFieldOffset方法,
* 获取到parkBlocker在内存里的偏移量
*/
private static void setBlocker(Thread t, Object arg) {
U.putObject(t, PARKBLOCKER, arg);
}
static {
PARKBLOCKER = U.objectFieldOffset
(Thread.class.getDeclaredField("parkBlocker"));
}
parkBlocker对象是用来记录线程被阻塞是被谁阻塞的,用于线程监控和分析工具来定位原因的。
LockSupport通过getBlocker获取到阻塞的对象,主要用于监控和分析线程。