章节目录:
一、CountDownLatch
用来进行线程同步协作,等待所有线程完成倒计时。其中构造参数用来初始化等待计数值,
await()
用来等待计数归零,countDown()
用来让计数减一。
1.1 基本使用
@Slf4j
public class CountDownLatchSample {
public static void doTask(ExecutorService pool, CountDownLatch latch, int nSeconds) {
pool.submit(() -> {
log.debug("begin...");
sleep(nSeconds);
// 计数减一。
latch.countDown();
log.debug("end...{}", latch.getCount());
});
}
public static void waitEnd(ExecutorService pool, CountDownLatch latch) {
pool.submit(() -> {
try {
log.debug("waiting...");
// 等待计数归零。
latch.await();
log.debug("wait end");
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
});
}
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(3);
ExecutorService pool = Executors.newFixedThreadPool(4);
// 模拟三个线程执行任务。
doTask(pool, latch, 1);
doTask(pool, latch, 2);
doTask(pool, latch, 3);
// 模拟一个线程等待任务执行结束。
waitEnd(pool, latch);
pool.shutdown();
// begin...
// waiting...
// begin...
// begin...
// end...2
// end...1
// end...0
// wait end
}
}
1.2 应用之同步等待多线程准备完毕
需求:使用
CountdownLatch
模拟十位玩家等待加载完成后,游戏开始。
@Slf4j
public class SyncWaitSample {
public static void main(String[] args) throws InterruptedException {
// 原子整数用于给线程命名。
AtomicInteger num = new AtomicInteger(0);
ExecutorService pool = Executors.newFixedThreadPool(
10,
r -> new Thread(r, "t_" + num.getAndIncrement())
);
CountDownLatch latch = new CountDownLatch(10);
String[] all = new String[10];
SecureRandom random = new SecureRandom();
for (int i = 0; i < 10; i++) {
int x = i;
pool.submit(() -> {
// 从 0% 加载 到 100%。
for (int j = 0; j <= 100; j++) {
// 模拟网络加载的等待时间。
sleep(random.nextInt(2), TimeUnit.SECONDS);
all[x] = Thread.currentThread().getName() + "(" + (j + "%") + ")";
System.out.print("\r" + Arrays.toString(all));
}
latch.countDown();
});
}
latch.await();
System.out.println("\n游戏开始...");
pool.shutdown();
// 开始:[t_0(8%), t_1(8%), t_2(15%), t_3(27%), t_4(18%), t_5(12%), t_6(17%), t_7(12%), t_8(23%), t_9(10%)]
// 中间:[t_0(75%), t_1(58%), t_2(69%), t_3(78%), t_4(68%), t_5(62%), t_6(68%), t_7(77%), t_8(84%), t_9(62%)]
// 结束:[t_0(100%), t_1(100%), t_2(100%), t_3(100%), t_4(100%), t_5(100%), t_6(100%), t_7(100%), t_8(100%), t_9(100%)]
// 游戏开始...
}
}
1.3 应用之同步等待多个远程调用结束
需求:模拟调用不同微服务,并等待执行结束。
- Controller:
@RestController
public class TestCountDownLatchController {
@GetMapping("/order/{id}")
public Map<String, Object> order(@PathVariable int id) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("total", "2300.00");
sleep(2, TimeUnit.SECONDS);
return map;
}
@GetMapping("/product/{id}")
public Map<String, Object> product(@PathVariable int id) {
Map<String, Object> map = new HashMap<>();
if (id == 1) {
map.put("name", "小爱音箱");
map.put("price", 300);
} else if (id == 2) {
map.put("name", "小米手机");
map.put("price", 2000);
}
map.put("id", id);
sleep(1, TimeUnit.SECONDS);
return map;
}
@GetMapping("/logistics/{id}")
public Map<String, Object> logistics(@PathVariable int id) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("name", "中通快递");
sleep(3, TimeUnit.SECONDS);
return map;
}
}
- Tests:
@SpringBootTest
@Slf4j
class DemoApplicationTests {
/**
* 模拟不使用任何手段,直接进行远程调用。
*/
@Test
void mockRemoteCall() {
RestTemplate restTemplate = new RestTemplate();
log.info("begin");
Instant start = Instant.now();
log.info(restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1) + "");
log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1) + "");
log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2) + "");
log.info(restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1) + "");
Instant end = Instant.now();
long cost = Duration.between(start, end).toMillis();
log.info("end... cost=[{}]ms", cost);
// begin
// {total=2300.00, id=1}
// {price=300, name=小爱音箱, id=1}
// {price=2000, name=小米手机, id=2}
// {name=中通快递, id=1}
// end... cost=[7132]ms
}
/**
* 使用 CountDownLatch 模拟等待远程调用结束。
*
* @throws InterruptedException 中断异常
*/
@Test
void mockRemoteCallByCountDownLatch() throws InterruptedException {
RestTemplate restTemplate = new RestTemplate();
log.info("begin");
Instant start = Instant.now();
CountDownLatch latch = new CountDownLatch(4);
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(() -> {
log.info(restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1) + "");
latch.countDown();
});
pool.submit(() -> {
log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1) + "");
latch.countDown();
});
pool.submit(() -> {
log.info(restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2) + "");
latch.countDown();
});
pool.submit(() -> {
log.info(restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1) + "");
latch.countDown();
});
latch.await();
Instant end = Instant.now();
long cost = Duration.between(start, end).toMillis();
log.info("end... cost=[{}]ms", cost);
pool.shutdown();
// [ main] begin
// [pool-1-thread-2] {price=300, name=小爱音箱, id=1}
// [pool-1-thread-3] {price=2000, name=小米手机, id=2}
// [pool-1-thread-1] {total=2300.00, id=1}
// [pool-1-thread-4] {name=中通快递, id=1}
// end... cost=[3098]ms
}
/**
* 在有返回值的情况下,使用 Future 模拟等待远程调用结束。
*
* @throws ExecutionException 执行异常
* @throws InterruptedException 中断异常
*/
@Test
@SuppressWarnings("unchecked")
void mockRemoteCallByFuture() throws ExecutionException, InterruptedException {
RestTemplate restTemplate = new RestTemplate();
log.info("begin");
Instant start = Instant.now();
ExecutorService pool = Executors.newCachedThreadPool();
Future<Map<String, Object>> f1 = pool.submit(() ->
restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1));
Future<Map<String, Object>> f2 = pool.submit(() ->
restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1));
Future<Map<String, Object>> f3 = pool.submit(() ->
restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2));
Future<Map<String, Object>> f4 = pool.submit(() ->
restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1));
log.info(String.valueOf(f1.get()));
log.info(String.valueOf(f2.get()));
log.info(String.valueOf(f3.get()));
log.info(String.valueOf(f4.get()));
Instant end = Instant.now();
long cost = Duration.between(start, end).toMillis();
log.info("end... cost=[{}]ms", cost);
pool.shutdown();
// begin
// {total=2300.00, id=1}
// {price=300, name=小爱音箱, id=1}
// {price=2000, name=小米手机, id=2}
// {name=中通快递, id=1}
// end... cost=[3102]ms
}
}
二、CyclicBarrier
[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用
await()
方法进行等待,当等待的线程数满足『计数个数』时,继续执行。
2.1 场景引出
v1需求:主线程需要等待其它线程执行任务结束,才开始执行后面的逻辑。
- 代码示例(使用 CountDownLatch 实现):
@Test
public void test01() {
ExecutorService pool = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(2);
pool.submit(() -> {
log.debug("task1 begin...");
sleep(1, TimeUnit.SECONDS);
latch.countDown();
});
pool.submit(() -> {
log.debug("task2 begin...");
sleep(2, TimeUnit.SECONDS);
latch.countDown();
});
try {
latch.await();
log.debug("task1 and task2 finish.");
pool.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 21:04.627 [pool-1-thread-1] task1 begin...
// 21:04.627 [pool-1-thread-2] task2 begin...
// 21:06.634 [main] task1 and task2 finish.
}
v2需求:现在需要反复执行3次上面的操作。
- 代码示例(使用 CountDownLatch 实现):
@Test
public void test02() {
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 3; i++) {
CountDownLatch latch = new CountDownLatch(2);
pool.submit(() -> {
log.debug("task1 begin...");
sleep(1, TimeUnit.SECONDS);
latch.countDown();
});
pool.submit(() -> {
log.debug("task2 begin...");
sleep(2, TimeUnit.SECONDS);
latch.countDown();
});
try {
latch.await();
log.debug("task1 and task2 finish.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
pool.shutdown();
// 27:28.277 [pool-1-thread-1] task1 begin...
// 27:28.277 [pool-1-thread-2] task2 begin...
// 27:30.289 [main] task1 and task2 finish.
// * 3 ...
}
-
问题说明:我们会很自然想到
CountDownLatch
放到循环体中去创建并执行,但是这会存在一个问题,那就是倒计时锁对象在这个过程中被反复的创建了。基于这一点,我们接下来使用CyclicBarrier
来进行优化。 -
代码示例(使用 CyclicBarrier 实现):
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
// 设置屏障拦截的线程数量。
CyclicBarrier barrier = new CyclicBarrier(2, () -> log.debug("task1, task2 finish..."));
for (int i = 0; i < 3; i++) { // task1 task2 task1
pool.submit(() -> {
log.debug("task1 begin...");
sleep(1, TimeUnit.SECONDS);
try {
barrier.await(); // 2-1=1
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
pool.submit(() -> {
log.debug("task2 begin...");
sleep(2, TimeUnit.SECONDS);
try {
// `await()`:已经到达了屏障,则当前线程会被阻塞。
barrier.await(); // 1-1=0
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
pool.shutdown();
// 41:53.609 [pool-1-thread-1] task1 begin...
// 41:53.609 [pool-1-thread-3] task2 begin...
// 41:53.609 [pool-1-thread-1] task1 and task2 finish.
// * 3 ...
}
2.2 CyclicBarrier与CountDownLatch的区别
CountDownLatch
的计数器只能使用一次,而CyclicBarrier
的计数器可以使用reset()
方法重置,可以使用多次,所以CyclicBarrier
能够处理更为复杂的场景;CountDownLatch
:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;CyclicBarrier
:多个线程互相等待,直到到达同一个同步点,再继续一起执行;- 对于
CountDownLatch
来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier
,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待; CountDownLatch
是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier
更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
三、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。