CountdownLatch源码解析(附带结合aqs)
CountdownLatch案例
注意:我用的jdk是15,如果源码有出入那么请参照具体版本的jdk源码
希望我的解析能帮助大家理解Countdownlatch
我们开发中有一个很现实的问题,具体来说:
我们写线程的时候需要等一些其他线程结束才能继续进行这个线程,(比如我们早上起来煮一杯牛奶,然后去洗脸刷牙,这两个任务是可以同时发生的。但是我们要喝牛奶的时候一定是已经把牛奶煮好了并且刷完牙洗完脸了。)而我们都知道java线程的分配是抢占式的,那么怎么让某一些线程在一些线程之前执行呢。当然我们可以使用join()函数。但是Countdownlatch比join更加灵活!
众所周知,Countdownlatch种有两个关键方法,第一个是await()。第二个是countdown();
这边为了更容易让大家理解Countdownlatch,写了一个main方法。
public static void main(String[] args) throws InterruptedException {
//这里设置了一个初始stat容量为5的countdownlatch,对应5个线程。在这5个线程执行完成之后才开始执行“main线程结束这句话”。
//为了理解我们把这个5暂时称为count
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int j = 0; j < 5 ; j++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"执行了");
countDownLatch.countDown();
},j+"线程").start();
}
//等待所有线程结束
countDownLatch.await();
System.out.println("main线程结束");
//等待所有线程结束
new Thread(()->{
try {
countDownLatch.await();
System.out.println("sub线程结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
/*下面是执行最终结果
1线程执行了
0线程执行了
4线程执行了
2线程执行了
3线程执行了
main线程结束
sub线程结束
*/
这边的每个线程start后进入就绪状态。之后main线程执行到countDownLatch发现countDownLatch的值还是5(有可能是4,3,2,1,这里只是为了说明问题),还不是0。那么线程就在这里挂起,阻塞,实际上它会进入AQS的阻塞队列中不停的cas请求启动线程,这个下面会分析到。
之后执行我们new的五个线程。执行完后就再次回到countDownLatch的地方,发现这个值为0了。那么就可以往下执行了。
我们来深度的分析一下countDownLatch的实现
CountDownLatch的源码分析
我们打开class结构看到:
这个结构相当简单明了。我们能看到的就是有一个Sync的内部类,以及CountDownLatch的await方法和countDown方法。
而熟悉aqs的人知道CountDownLatch其实是基于AQS(AbstractQueuedSynchronizer)实现的,AQS是很多锁的底层实现,可以说是相当重要。我们翻开Sync这个内部类:
这个类继承了aqs,实现了tryAcquireShared方法和tryReleaseShared方法。暂且把这个类放在这里不去分析,我们过会再来仔细分析这个类。
现在我们看countDown方法
我们发现的是,这个countDown实际上是调用了Sync这个内部类的releaseShared方法。然而这个Sync没有这个releaseShared方法,那么我们得去它的父类(即aqs)去找这个方法。
这个releaseShared方法先调用了tryReleaseShared方法,这个方法其实在Sync类中是有的。我们回到上面那张Sync图片看。
这个方法实际上是死循环的CAS(compareAndSwap),这个cas的任务就是设置stat的值
//死循环
for (;;) {
//得到AQS中的state,这个state在每一个类的具体含义都不同,在countDownLatch这个类的具体实现中,读者可以把他具体理解为countdownlatch中初始化的那个5的参数
int c = getState();
//如果这个参数已经是0的时候那么就返回false。意思就是我们执行countdown方法的时候,如果这个count已经是0了,那么就返回错误。
if (c == 0)
return false;
//nextc就是countdown后的值,即把count-1。
int nextc = c - 1;
//通过cas直接在内存设置count的值,也就是在aqs中state的值
if (compareAndSetState(c, nextc))
//如果count是0了,就返回true
//如果不是0,就返回false。
//这一点相当重要!!!后面会讲到
return nextc == 0;
}
await()
我们可以看到实际上调用了AcquireSharedInterruptibly方法。这个方法是AQS类中的(AbstractQueuedSynchronizer)
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//判断是否中断,如果不中断就去tryAcquiredShared方法,判断返回值是否小于0,实际上我们的返回值是-1;也就是这个时候count不为0,具体参见下面的tryAcquiredShared方法。
//然后就去找acquire方法,这个方法是aqs中的核心方法
if (Thread.interrupted() ||
(tryAcquireShared(arg) < 0 &&
acquire(null, arg, true, true, false, 0L) < 0))
throw new InterruptedException();
}
//判断count值是否等于0 ,如果不等于就返回-1。实际上这个时候count可能等于1,2,3,4,5。
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
我们最终去找acquire方法,这个方法在jdk15中非常的长,主要任务就是使调用了countdownlatch.await()方法的线程阻塞,加入aqs队列.
for (;;) {
if (!first && (pred = (node == null) ? null : node.prev) != null &&
!(first = (head == pred))) {
if (pred.status < 0) {
cleanQueue(); // predecessor cancelled
continue;
} else if (pred.prev == null) {
Thread.onSpinWait(); // ensure serialization
continue;
}
}
if (first || pred == null) {
boolean acquired;
try {
if (shared)
acquired = (tryAcquireShared(arg) >= 0);
else
acquired = tryAcquire(arg);
} catch (Throwable ex) {
cancelAcquire(node, interrupted, false);
throw ex;
}
if (acquired) {
if (first) {
node.prev = null;
head = node;
pred.next = null;
node.waiter = null;
if (shared)
signalNextIfShared(node);
if (interrupted)
current.interrupt();
}
return 1;
}
}
if (node == null) { // allocate; retry before enqueue
if (shared)
node = new SharedNode();
else
node = new ExclusiveNode();
} else if (pred == null) { // try to enqueue
node.waiter = current;
Node t = tail;
node.setPrevRelaxed(t); // avoid unnecessary fence
if (t == null)
tryInitializeHead();
else if (!casTail(t, node))
node.setPrevRelaxed(null); // back out
else
t.next = node;
} else if (first && spins != 0) {
--spins; // reduce unfairness on rewaits
Thread.onSpinWait();
} else if (node.status == 0) {
node.status = WAITING; // enable signal and recheck
} else {
long nanos;
spins = postSpins = (byte)((postSpins << 1) | 1);
if (!timed)
LockSupport.park(this);
else if ((nanos = time - System.nanoTime()) > 0L)
LockSupport.parkNanos(this, nanos);
else
break;
node.clearStatus();
if ((interrupted |= Thread.interrupted()) && interruptible)
break;
}
}
return cancelAcquire(node, interrupted, interruptible);
}
其次,示例中的sub线程的thread的await()其实根本没有生效。因为当执行到
countDownLatch.await();
System.out.println("sub线程结束");
这一句的时候,发现当前的stat值为0.那么直接返回了
AQS
实际上aqs就是一个fifo的双向队列。countdownlatch就基于这个实现。aqs有一个aqs阻塞队列和很多的condition队列。以及关键的stat的int变量。这个stat变量在具体的实现中的意义不同,在countdownlatch中就代表count的值。而在ReentrantReadWriteLock中的高16位表示读状态,低16位表示写状态,在semaphore中代表的是可用信号的个数。在ReentrantLock中代表的就是重入的次数。
其中有acquire的独占方法和acquireshared的共享方法。这个独占和共享并不是说这个方法独占和共享。而是线程独占和共享。
这个也基于具体的实现。像ReetrantLock一般是用独占(也有共享)。而countdownlatch只有共享。
还有release方法和releaseShared方法,同上,是对应的独占和共享。
acquire方法可以具体理解为进入aqs队列,请求去添加这个线程。同时改变具体的stat值。
release方法可以具体理解为出aqs队列,执行线程,同时改变具体的stat值。以及执行的线程会被gc回收。
在执行这方法acquire方法之前要先实现tryAcquire。这个待具体的继承类实现,如果条件成立,也就是具体的类允许这个线程去入队,那么再去执行acquire,去入队。
而condtion用于挂起线程的,具体的线程会进一个挂起队列,意思是,这边队列的线程暂时不能执行,要等对应的人去唤醒我们才能把他放到aqs队列中去。
最后再回到countdownlatch,实际上await方法就是使得线程进入了aqs队列不断的去cas请求执行。
注意 :aqs队列不等同于condition队列,我第一次看的时候弄错了。aqs队列是活跃的线程,有线程释放锁,aqs队列中的node就会争抢线程。而condition队列是阻塞的线程(可以认为是死的)这些线程不参与锁的争抢。aqs队列中的线程经过newcondition的await方法(不同于CountDownLatch的await方法,CountDownLatch中的await是进aqs队列)就会进入conditon队列,通过singal方法就会出condition队列。
而countDown方法的具体作用就是等到stat值变为0 时,去把这个aqs队列全部的线程执行一遍。