1. 什么是闭锁
闭锁(latch)是一种 Synchronizer,他可以延迟线程的进度直到线程到达终止状态。
一个闭锁工作起来就像一道大门:直到闭锁达到终点状态之前,门一直是关闭的,没有线程通过,在终点状态到来的时候,门开了,允许所有线程都通过。一旦闭锁到达了终点状态,他就不能够在改变状态了,所以它会永远保持敞开的状态。
2. 闭锁的应用场景
-
确保一个计算不会执行,直到它需要的资源被初始化。
-
确保一个服务不会开始,直到它依赖的服务都已经开始。
-
等待直到活动的所有部分都为继续处理做好准备。比如王者荣耀需要等待所有玩家准备才能开始
Jmeter模拟高并发也是这个场景
,如果我想测试10个线程同时工作对cpu的影响,那么如果线程执行的快慢程度不一样,可能第10个线程刚创建,第一个线程就执行结束了,此时就只有9个线程在同时执行,和我预期不一致,此时,就可以用CountDownLatch 控制,在下文5.2复杂例子,就有这样的例子!
3. 闭锁的实现
CountDownLatch是一个同步辅助类,存在于java.util.concurrent包下,灵活的实现了闭锁,它允许一个或多个线程等待一个事件集的发生。
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数的值就会减1。当计数器值到达0时,它所表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
4. CountDownLatch原理
CountDownLatch构造函数:
CountDownLatch(int count);
构造器中计数值(count)就是闭锁需要等待的线程数量,这个值只能被设置一次。
CountDownLatch类的方法:
-
void await()
:使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。 -
boolean await(long timeout, TimeUnit unit)
:使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。其实就是
void await()
,额外多了一个超时时间,避免某个因素导致锁长久未归零,程序可以提前做出处理。 -
void countDown()
:递减锁存器的计数,如果计数到达零,则释放所有等待的线程。 -
long getCount()
:返回当前计数。 -
String toString()
:返回标识此锁存器及其状态的字符串。
常规用法是这样:线程A调用锁B的await()方法后,线程A会等待锁B归零,期间线程A会一直阻塞,如果锁B一直未归零,线程A也会一直阻塞下去;如果用await(timeout)的话,超时后,会自行唤醒,继续完成后续流程。
同样的,如果其他的N个线程也调用锁B的await()方法后象,这些线程会和线程A一样,都会阻塞,直至锁B归零。
5. 使用案例
5.1 入门案例
我们前面介绍了CountDownLatch 的使用场景,那就是等待资源就绪。
假设你是一个厨师,你有2个助手,你要做一道荤菜,那么需要肉和调味品,你可以吩咐2个助手分别去买肉和买盐,等到2个助手都回来后,你才能点火做饭,在此期间,你就一直在等待。如果某个助手先到了,你也不能做任何事情,继续等待另一个的到来。
买肉任务:
package com.test;
import java.util.concurrent.CountDownLatch;
/**
* 买肉任务
*/
public class BuyMeatTask implements Runnable {
private CountDownLatch countDownLatch;
public BuyMeatTask(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public void run() {
try {
System.out.println("出门去买肉了");
Thread.sleep(2000);
System.out.println("肉买来了!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (countDownLatch != null)
countDownLatch.countDown();
}
}
}
买盐的任务:
package com.test;
import java.util.concurrent.CountDownLatch;
/**
* 买盐的任务
*/
public class BuySaltTask implements Runnable {
private CountDownLatch countDownLatch;
public BuySaltTask(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
public void run() {
try {
System.out.println("出门买盐了");
Thread.sleep(5000);
System.out.println("盐买回来了!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (countDownLatch != null)
countDownLatch.countDown();
}
}
}
执行类:
package com.test;
import java.util.concurrent.CountDownLatch;
/**
* 我是厨师,等待配菜和调味品
*/
public class CountDownLaunchRunner {
static int sub = 0;
static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(new BuyMeatTask(countDownLatch)).start();
new Thread(new BuySaltTask(countDownLatch)).start();
// 等待线程池中的2个任务执行完毕,否则一直等待
countDownLatch.await();
System.out.println("准备点火炒菜");
}
}
执行结果:
出门去买肉了
出门买盐了
肉买来了!
盐买回来了!
准备点火炒菜
当子线程调用countDownLatch.countDown()
满足为0
时,会发通知消息,唤醒阻塞的主线程:
5.2 复杂案例
我们使用书上原文的例子,我们想统计所有子线程的运行时间,由于子线程运行时间有长有短,比如子线程A花费10s,子线程B花费2S,那么结果应该是最长的那个,即10s,因此,我们希望在最后一个线程(耗时最长的那个)执行完毕后,由主线程统计时间。
package net.jcip.examples;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class TestHarness {
public long timeTasks(int nThreads, final Runnable task) throws InterruptedException {
// 开始锁,当其值变为0时,表示等待的线程可以运行
final CountDownLatch startGate = new CountDownLatch(1);
// 结束锁,当其值变为0时,表示最后一个线程也执行完了,也就是说所有的线程都执行完了
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++) {
// 模拟不同线程的间隔一定的时间才创建好
TimeUnit.SECONDS.sleep(1);
Thread t = new Thread() {
public void run() {
try {
System.out.println(new Date() + " " + Thread.currentThread().getName()
+ " started");
startGate.await(); // <注1>每个子线程立即进入等待,直至开始锁释放,才一起运行
try {
task.run();
} finally {
endGate.countDown(); // <注2>每个子线程运行结束,通知结束锁计数器减一
}
} catch (InterruptedException ignored) {
}
}
};
t.start(); // 启动子线程
}
long start = System.nanoTime(); // 开始的时间
// 为了让结果更直观,主线程增加休眠,避免间隔太短,瞬时打印日志IO顺序不符合预期
// 确保所有的线程都达到阻塞点,再去唤醒它们
Thread.sleep(1000);
startGate.countDown(); // 开始锁减一,计数器归零,通知所有的子线程(卡在<注1>)处的)可以执行了
endGate.await(); // 主线程进入等待,直至最后一个线程运行结束(<注2>处发出通知),才会被唤醒,继续执行
long end = System.nanoTime();
return end - start; // 统计时间
}
public static void main(String[] args) throws InterruptedException {
Runnable task = new Runnable() {
@Override
public void run() {
// 模拟线程耗时不同
int counter = new Random().nextInt(100);
System.out.println(new Date() + " " + Thread.currentThread().getName()
+ " will run " + counter + " times");
for (int i = 0; i < counter; i++) {
try {
Thread.sleep(100); // 模拟耗时处理
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(new Date() + " " + Thread.currentThread().getName()
+ " finished");
}
};
TestHarness test = new TestHarness();
long time = test.timeTasks(3, task);
System.out.println("total time is : " + time);
}
}
执行结果,每次内容随机,但存在特定规律:
Fri Feb 12 17:03:05 CST 2021 Thread-0 started
Fri Feb 12 17:03:06 CST 2021 Thread-1 started
Fri Feb 12 17:03:07 CST 2021 Thread-2 started
Fri Feb 12 17:03:08 CST 2021 Thread-2 will run 88 times
Fri Feb 12 17:03:08 CST 2021 Thread-0 will run 71 times
Fri Feb 12 17:03:08 CST 2021 Thread-1 will run 39 times
Fri Feb 12 17:03:12 CST 2021 Thread-1 finished
Fri Feb 12 17:03:15 CST 2021 Thread-0 finished
Fri Feb 12 17:03:17 CST 2021 Thread-2 finished
total time is : 9853746100
分析:通过结果的注释,基本上就能看到规则了,主线程确实是在所有子线程结束后,才统计的运行时间。
我们之前列举了CountDownLatch 的3个应用场景,这个例子里面有2个锁,覆盖了其中的2种:
- 等待直到活动的所有部分都为继续处理做好准备
startGate锁保证所有的子线程都准备就绪后,才开始; - 确保一个计算不会执行,直到它需要的资源被初始化
endGate锁保证主线程依赖的所有子线程都完成后,才开始 ;对于主线程来说,子线程完成就是它依赖资源。