我们都知道, 在使用多线程编程的时候,每个线程运行的顺序都是随机的, 它由CPU的线程调度机制决定执行哪个线程;
我们可以看看正常使用多线程编程时程序的运行顺序:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
//CountDownLatch countDownLatch = new CountDownLatch(3);
try {
for (int i = 0; i < 3; i++) {
executorService.submit(new Callable>() {
@Override
public List call() throws Exception {
try {
//doSomeThing()
//Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "线程开始执行");
} catch (Exception e) {
} finally {
//countDownLatch.countDown();
}
return new ArrayList<>();
}
});
}
//countDownLatch.await();
//System.out.println("主线程执行");
executorService.submit(new Callable>() {
@Override
public List call() throws Exception {
try {
//doSomeThing()
//countDownLatch.await();
System.out.println("测试线程开始执行");
} catch (Exception e) {
} finally {
}
return new ArrayList<>();
}
});
} catch (Exception e) {
}finally {
executorService.shutdown();
}
}
}
执行结果:
可以看到,线程的执行顺序是随机的,他们执行的顺序并不固定.
那么现在有一个需求, 我想让测试线程必须执行在其他三个线程执行完毕之后才执行, 也就是测试线程必须执行在其他三个线程的后面. 该怎么实现呢?
有一个通俗一点的例子: 有三个工人在为老板干活,这个老板有一个习惯,就是当三个工人把一天的活都干完了的时候,他就来检查所有工人所干的活。记住这个条件:三个工人先全部干完活,老板才检查。
如何实现以上的需求呢?
CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。
CountDownLatch有两个核心的方法, countDown()和await()方法.在使用CountDownLatch的时候, 必须要传入一个数值型参数作为计数器的值. 调用countDown()方法可以将值减1. 而调用了await()方法的线程,在计数器的值为0之前, 会被一直阻塞. 直到计数器的值为0时才会被唤醒. 因此.可以通过CountDownLatch来实现某个线程在其他线程执行完毕之后再执行.
那么使用CountDownLatch之后会怎么样呢?
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CountDownLatchTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
//初始化CountDownLatch 计数器的值
CountDownLatch countDownLatch = new CountDownLatch(3);
try {
for (int i = 0; i < 3; i++) {
//创建三个线程, 每个线程执行完后调用countDownLatch 对象的countDown()方法将计数器减1
executorService.submit(new Callable>() {
@Override
public List call() throws Exception {
try {
//doSomeThing()
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "线程开始执行");
} catch (Exception e) {
} finally {
countDownLatch.countDown();
}
return new ArrayList<>();
}
});
}
executorService.submit(new Callable>() {
@Override
public List call() throws Exception {
try {
//doSomeThing()
//当计数器的值变为0之后,调用同一个countDownLatch对象的await()方法的主线程将会被唤醒
countDownLatch.await();
System.out.println("测试线程开始执行");
} catch (Exception e) {
} finally {
}
return new ArrayList<>();
}
});
} catch (Exception e) {
}finally {
executorService.shutdown();
}
}
}
执行结果:
可以看到, 测试线程都是在前三个线程执行完毕之后才执行的
如上面的代码中所示. 测试线程在计数器的值变为0之前, 会一直处于阻塞状态.只有当计数器值变为0时才会被唤醒. 也就是说, 测试线程是必须要运行在前三个线程之后的.
了解到CountDownLatch的作用以及使用机制之后, 我们可以看看源码,看看CountDownLatch是如何实现阻塞测试线程, 并且在计数器为0时再唤醒测试线程的.
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) //计数器state不等于0时
doAcquireSharedInterruptibly(arg); //阻塞线程
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);//1. 将该线程加入阻塞队列中
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();//获取前一个节点
if (p == head) {
int r = tryAcquireShared(arg);//2. 判断state是否为0; r>=0说明为0
if (r >= 0) {
//把当前节点设置成head节点,并传播 唤醒 所有被await()方法阻塞的节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//2. 到此处,说明state != 0
//检查是否应该阻塞节点
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞节点
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
await()流程:
判断state(即计数器的数量是否等于0)
不等于0则将线程加入阻塞队列中
等于0则唤醒所有被await()方法阻塞的线程
我们来看看CountDownLatch类的结构:
可以看到CountDownLatch类中有一个继承自AQS抽象类的Sync类. AQS中实现了对同步代码中的各类操作.
先从await()方法看起:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
可以看到,await()方法中调用了Sync的acquireSharedInterruptibly(1)方法;
acquireSharedInterruptibly()方法:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) //计数器state不等于0时
doAcquireSharedInterruptibly(arg); //阻塞线程
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
acquireSharedInterruptibly方法中,先调用了tryAcquireShared方法来判断state(在CountDownLatch中代表的是计数器的值)的值是否为0 ,不为0时, 调用doAcquireSharedInterruptibly方法阻塞线程;
那么,doAcquireSharedInterruptibly是怎么实现的阻塞线程的呢?
/**
* 在共享可中断模式下获取。
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);//1. 将该线程加入阻塞队列队尾
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();//获取前一个节点
if (p == head) {
int r = tryAcquireShared(arg);//2. 判断state是否为0; r>=0说明为0
if (r >= 0) {
//把当前节点设置成head节点,并传播 唤醒 所有被await()方法阻塞的节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//2. 到此处,说明state != 0
//检查是否应该阻塞节点
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞节点
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
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;
//采用CAS将当前节点设为队尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);//向队列队尾处插入节点,此处暂不赘述
return node;
}
//挂起线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
从代码中可以看出, AQS中有一个先进先出的队列.而在CountDownLatch类中, 该队列保存的就是被await()方法阻塞的线程. 在执行await()方法之前, 程序会先判断计数器的值是否为0,如果为0则直接执行该线程,反之会将该线程封装到Node节点中,并通过CAS操作将其保存到队列的队尾.然后阻塞该线程. 从这里你是不是猜到了, 既然将阻塞的线程都保存到队列中了,是不是就是为了在计数器值变为0时将其唤醒呢. 没错, 在接下来的countDown()方法中就能找到答案.
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//state(计数器值)减1,且当state为0时返回true
//唤醒线程
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;
//CAS将计数器state的值减1
if (compareAndSetState(c, nextc))
//当state为0时返回true
return nextc == 0;
}
}
可以看到,countDown()方法是利用CAS操作将计数器的值减1,减1成功之后,自然就是判断计数器值是否为0 ,为0时唤醒队列中阻塞的线程了. 那么我们接着看doReleaseShared()方法;
private void doReleaseShared() {
/*
* 确保释放传播,即使有其他进行中的获取/释放。如果它需要信号,则以通常的方式尝试解除头的处理器。但如果不是,状态设置为PROPAGATE以确保在释放后,传播继续。
* 另外,我们必须循环,以便在我们这样做的时候添加一个新的节点。此外,与unparkSuccessor的其他用法不同,我们需要知道CAS是否重置状态失败,如果这样重新检查。
*
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 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;
}
}
唤醒队列中的线程:unparkSuccessor():
private void unparkSuccessor(Node node) {
//如果状态为负(即,可能需要信号)尝试在预期的信令中清除。如果这失败或者如果状态通过等待线程而改变,则是OK。
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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);
}
从代码中可以看到, 当计数器值为0 时,会根据队列中的头结点向队尾依次遍历获取阻塞中的节点并唤醒对应节点中的线程. 当下一个节点不存在或者已经取消等待时,会从队尾开始向头结点回溯遍历来唤醒对应的线程.