在java.util.concurrent
中给我们提供了很多的并发工具,通过这些并发工具我们能很简单的实现许多强大的功能.这里我主要介绍一下比较常用的一些并发工具的使用场景.
CountDownLatch(闭锁)
简单的来说它可以在初始化时设定一个计数器,这个值只能设定一次后面无法再次修改.同时还提供一个await
方法,在计数器的值还未到达0之前调用该方法的线程将一直等待.通过每调用一次countDown
将会使该计数值减1,直到计数器的值减到0之后,等待在该对象上的线程才能继续运行.
如果上面的描述没看懂没关系,我们举一个生活中的例子来类比.假如旅行社要搞一个50个人的旅游团(闭锁中的计数器)需要等到名额满了之后才会开始出发(计算器归0).每当有一个人过来报名成功名额就减一(计数器减一),而且报完名的人是可以去干别的事情.当名额未满时旅行社只能等待或者去干别的事(线程调用await等待),直到名额满了旅行社就可以出发了(调用线程继续执行).
例如现在我们有一个数据统计页接口,这个接口里面需要返回来自不同平台的一些统计数据.例如我们要获取淘宝京东拼多多等多个平台的订单数据.按照我们的惯性逻辑来做,可能会查询淘宝平台的数据,然后接着查询京东平台的数据依次类推.这样的查询是可以完成任务,但是接口的响应时间不会很理想.因为这些任务都是串行执行的,最后所花费的时间就是所有的时间之和.我们可以使用多线程配合CountDownLatch让这些查询任务并行运行,大大的提高我们接口的响应时间.
public class App2 {
public static void main(String[] args) throws Exception {
//开始时间
long start = System.currentTimeMillis();
//创建闭锁
CountDownLatch cdl = new CountDownLatch(3);
//查询淘宝任务
QueryPlatformTask tb = new QueryPlatformTask("tb", cdl);
//查询京东任务
QueryPlatformTask jd = new QueryPlatformTask("jd", cdl);
//查询拼多多任务
QueryPlatformTask pdd = new QueryPlatformTask("pdd", cdl);
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(3);
//提交任务
Future f1 = service.submit(tb);
Future f2 = service.submit(jd);
Future f3 = service.submit(pdd);
//等待执行完
cdl.await();
//关闭线程池
service.shutdown();
//获取结果打印
System.out.println("f1.get() = " + f1.get());
System.out.println("f2.get() = " + f2.get());
System.out.println("f3.get() = " + f3.get());
long end = System.currentTimeMillis();
//打印耗时时间
System.out.println("耗时时间:"+(end-start)/1000+"s");
}
}
/**
* 平台查询任务
*/
class QueryPlatformTask implements Callable{
private String platform;
private CountDownLatch cdl;
public QueryPlatformTask(String platform, CountDownLatch cdl) {
this.platform = platform;
this.cdl = cdl;
}
@Override
public Integer call() throws Exception {
try {
switch (platform){
case "tb":{
return queryTb();
}
case "jd":{
return queryJD();
}
case "pdd":{
return queryPdd();
}
default:{
throw new UnsupportedOperationException("不支持的平台操作");
}
}
}finally {
//请将计数器减一操作放入finally,避免异常导致该操作失败变成死锁
cdl.countDown();
}
}
//查询淘宝平台数据
private Integer queryTb() throws InterruptedException {
//模拟查询耗时
Thread.sleep(2000);
System.out.println("淘宝平台数据查询完成");
return 100;
}
//查询京东平台数据
private Integer queryJD() throws InterruptedException {
//模拟查询耗时
Thread.sleep(3000);
System.out.println("京东平台数据查询完成");
return 80;
}
//查询拼多多平台数据
private Integer queryPdd() throws InterruptedException {
//模拟查询耗时
Thread.sleep(4000);
System.out.println("拼多多平台数据查询完成");
return 50;
}
}
上面代码简单的实现了查询需求,我们将查询任务封装成Callable类型的任务,且每个任务中包含了CountDownLatch
实例用来计数.每当完成一个任务时都会将计数器减1,需要特别注意的是在调用countDown
方法时一定要放在finally
中.因为如果在执行中出现异常,代码还未执行到countDown
时就已经因为异常退了,这将导致所有任务已执行完,但是因为异常的线程没有执行计数器减一的操作,最后导致等待的线程始终无法被唤醒.主线程阻塞在await
方法上,直到所有任务都完成计数器归0,主线程继续执行打印结果如下:
淘宝平台数据查询完成
京东平台数据查询完成
拼多多平台数据查询完成
f1.get() = 100
f2.get() = 80
f3.get() = 50
耗时时间:4s
从结果也能看出来,任务由串行运行变成并行运行.最后的耗时时间取决于任务中耗时时间最长的一个.
线程池本身也是提供一个invokeAll
方法用来执行批量任务,等任务全部执行完全之后等待的线程再继续执行.这两个方法都能实现同样的功能,但是他们有也有些许不同.不同之处主要在于提供的超时方法上有不同.
invokeAll
当到了过期时间之后,对于还在线程池队列中的任务会被取消,而正在执行的方法则会调用interrupt
来中断线程执行.但是使用CountDownLatch
过期时间到了,阻塞的线程会立即执行,并不会关心任务是否执行完成,也不会去调用interrupt
中断线程的执行.下面我们使用两段代码来验证:
invokeAll
public class App3 {
public static void main(String[] args) throws InterruptedException {
//创建任务
List> tasks = new ArrayList<>();
for (int i = 0; i < 5; i++) {
tasks.add(new Callable
CountDownLatch
public class App4 {
public static void main(String[] args) throws InterruptedException {
int taskNumber = 5;
final CountDownLatch cdl = new CountDownLatch(taskNumber);
//创建任务
List> tasks = new ArrayList<>();
for (int i = 0; i < taskNumber; i++) {
tasks.add(new Callable
- 结果
// 结果1
main:提交任务
pool-1-thread-1:开始运行
main:等待结束
pool-1-thread-1:线程中断退出
// 结果2
main:提交任务
pool-1-thread-1:开始运行
main:等待结束
上面的代码中需要注意的的点有两个:创建线程池和线程池shutdwon方法.这两个如果设置的不合理不太好说明invokeAll
和CountDownLatch
之间的区别.如果不懂为什么请看我之前写的关于线程池相关的笔记.
结果1中,在超时时间到了之后,线程池中正在执行的任务会被中断,还在队列中未执行的任务会被取消.最后调用shutdown
线程池关闭,主线程执行完毕整个程序结束执行.
结果2中,我们使用countDown
设置超时时间.超时时间到了之后主线程推出阻塞继续向下执行,但是可以发现线程池中的线程还在继续执行.因为countDown
这种方式它不会去关心线程是不是还在继续执行.而线程池中的线程也无法收到中断响应,这将导致线程池中的任务会继续执行导致无法执行完.最后主线程调用线程池shutdown
,不管你等多久程序还是无法终止.这是因为线程池中还有线程在执行,而这部分任务默认都不是守护线程,这将导致程序无法终止.
CyclicBarrier(循环栅栏)
它允许一组线程互相等待,直到所有线程到达某个公共集合点,然后所有的线程再继续执行.这里面强调的执行任务的线程相互等待.同时当所有的任务执行完成之后,Barrier
释放所有等待的线程后是可以重用的.
还是拿之前旅行社的例子来做个比喻.现在情况是旅行社团员人数够了,现在准备集合出发了.但是每个游客所在的地方都不一样,每个人到达集合点所用的时间也不一样(每个线程执行完任务到达屏障点的时间不一样).当一个游客到达集合点(线程到达屏障点)需要等待的人就少1(计数器减1),游客到了只能等待其他游客不能干别的事(先到达的线程只能在屏障点等待).直到所有人都已经到了旅行团就可以出发了.
上面的例子使用代码描述为下:
public class App5 {
public static void main(String[] args) {
//游客人数
int touristCount = 5;
//创建集合点
CyclicBarrier barrier = new CyclicBarrier(touristCount, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":人员集合完毕开始出发");
}
});
//创建线程池
ExecutorService service = Executors.newCachedThreadPool();
//游客开始出发
for (int i = 0; i < touristCount; i++) {
service.execute(new Tourist("游客["+i+"]",(i+1)*1000,barrier));
}
//关闭线程池
service.shutdown();
}
}
/**
* 游客
*/
class Tourist implements Runnable{
//游客的名字
private String name;
//赶路花费的时间
private Integer time;
private CyclicBarrier barrier;
public Tourist(String name, Integer time, CyclicBarrier barrier) {
this.name = name;
this.time = time;
this.barrier = barrier;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+name+"出发");
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+name+"到达集合点,等待其他游客到达");
try {
//当前线程在这个地方等待其他线程到达屏障点
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
最后的打印结果如下:
pool-1-thread-2:游客[1]出发
pool-1-thread-1:游客[0]出发
pool-1-thread-4:游客[3]出发
pool-1-thread-5:游客[4]出发
pool-1-thread-3:游客[2]出发
pool-1-thread-1:游客[0]到达集合点,等待其他游客到达
pool-1-thread-2:游客[1]到达集合点,等待其他游客到达
pool-1-thread-3:游客[2]到达集合点,等待其他游客到达
pool-1-thread-4:游客[3]到达集合点,等待其他游客到达
pool-1-thread-5:游客[4]到达集合点,等待其他游客到达
pool-1-thread-5:人员集合完毕开始出发
创建CyclicBarrier
中的barrierAction
参数是一个Runnable
类型.这个代表当所有的任务执行完成之后才执行这个任务.而且是由最后一个到达屏障点(即完成)的线程来执行.从我们的打印结果可以看出是pool-1-thread-5
,因为它是最后一个执行的.
-
CyclicBarrier
是可以循环使用的.它提供了一个reset
方法用来重置并将它设置回初始状态.但是需要如果在屏障点上已经有线程在上面等待的话,等待的线程将会抛出BrokenBarrierException
异常.这将导致还未到达屏障点的其他线程在到达屏障点后一直无限等待.
public class App6 {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"barrierAction");
}
});
//
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+":在屏障点等待");
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
});
t1.setName("t1");
t1.start();
//
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName()+":在屏障点等待");
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
});
t2.setName("t2");
t2.start();
Thread.sleep(2000L);
barrier.reset();
}
}
上面代码执行结果如下:
t1:在屏障点等待
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
at com.buydeem.App6$2.run(App6.java:26)
at java.lang.Thread.run(Thread.java:745)
t2:在屏障点等待
线程t1先到达屏障点,然后过了2s之后main
线程调用reset
将CyclicBarrier
实例重置,这直接导致线程t1
抛出BrokenBarrierException
异常,然后线程t2
到达屏障点将在该点上一个值无限等待下去,导致程序无法执行结束.
- 如果一个线程等待在屏障点,然后将等待中的线程中断.这个操作将会导致等待中的线程抛出
InterruptedException
异常,这个异常还会传播到其他线程,这将导致其他等待在这个屏障点上的线程会收到BrokenBarrierException
异常.这个里面的等待线程包括已经到达等待点的线程和还未到达屏障点的线程,这个操作也将导致屏障点损坏.
public class App7 {
public static void main(String[] args) throws InterruptedException {
//创建CyclicBarrier
CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println("barrierAction");
}
});
//创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
barrier.await();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
} catch (BrokenBarrierException e) {
System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
}
}
};
//任务1
Thread t1 = new Thread(runnable);
t1.setName("t1");
t1.start();
//任务2
Thread t2 = new Thread(runnable);
t2.setName("t2");
t2.start();
//任务3
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
barrier.await();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
} catch (BrokenBarrierException e) {
System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
}
}
});
t3.setName("t3");
t3.start();
//2s后中断线程1
Thread.sleep(1000L);
t1.interrupt();
//2s打印是否损坏
Thread.sleep(2000L);
System.out.println("屏障点是否损坏:"+barrier.isBroken());
}
}
打印结果如下:
t1:InterruptedException
t2:BrokenBarrierException
t3:BrokenBarrierException
屏障点是否损坏:true
程序运行线程t1
和t2
都到达了阻塞点,然后过了1s之后,线程t1
被中断.这个中断导致线程t1
和t2
分别抛出InterruptedException
异常和BrokenBarrierException
.而线程t3
在此之后到达屏障点,也同样抛出BrokenBarrierException
异常.最后通过isBroken
打印显示屏障点已经被损坏.
- 如果当前等待在屏障点上的线程超出指定的等待时间,当前线程会抛出
TimeoutException
异常,而其他线程会抛出BrokenBarrierException
异常,同样也会导致屏障点被损坏
public class App8 {
public static void main(String[] args) throws InterruptedException {
//创建CyclicBarrier
CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println("barrierAction");
}
});
//任务1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
barrier.await();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
} catch (BrokenBarrierException e) {
System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
}
}
});
t1.setName("t1");
t1.start();
//任务2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
barrier.await(1,TimeUnit.SECONDS);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + ":InterruptedException");
} catch (BrokenBarrierException e) {
System.out.println(Thread.currentThread().getName() + ":BrokenBarrierException");
} catch (TimeoutException e) {
System.out.println(Thread.currentThread().getName() + ":TimeoutException");
}
}
});
t2.setName("t2");
t2.start();
//2s打印是否损坏
Thread.sleep(2000L);
System.out.println("屏障点是否损坏:"+barrier.isBroken());
}
}
t2:TimeoutException
屏障点是否损坏:true
t1:BrokenBarrierException
线程t1
先到达屏障点等待超时时间为1s
,而线程t2
需要2s
才到达屏障点.所以线程t1
抛出TimeoutException
异常,同时导致线程t2
同样抛出BrokenBarrierException
异常并且还导致屏障点损坏.
CountDownLatch与CyclicBarrier的差异
网上说他们之间的差异主要在于复用和不复用
.其实我不太认同这种说法,我觉得这个并不是他们之间最大的差别.
-
CountDownLatch
强调的是主线程
等待所有子线程
完成任务然后继续执行.而CyclicBarrier
强调的是所有的子线程到达一个集合点再继续执行. -
CountDownLatch
中的子线程完成任务后它是可以去干别的事情的,而CyclicBarrier
中的子线程只能等待在屏障点是不能去干别的事情的.
以上两点我觉得才是他们之间最大的区别.我用一个例子说明:
- CountDownLatch
public class App9 {
public static void main(String[] args) throws InterruptedException {
//创建一个固定大小为2的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//创建闭锁
int count = 5;
final CountDownLatch cdl = new CountDownLatch(count);
//创建子任务
for (int i = 0; i < count; i++) {
service.execute(new NumberTask(i,cdl));
}
//等待任务执行结束
cdl.await();
//关闭线程池
service.shutdown();
}
}
/**
* 任务
*/
class NumberTask implements Runnable{
private Integer id;
private CountDownLatch cdl;
public NumberTask(Integer id, CountDownLatch cdl) {
this.id = id;
this.cdl = cdl;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+":执行完任务["+id+"]");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
cdl.countDown();
}
}
}
- CyclicBarrier
public class App10 {
public static void main(String[] args) throws InterruptedException {
//创建一个固定大小为2的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
//创建闭锁
int count = 5;
final CyclicBarrier barrier = new CyclicBarrier(count);
//创建子任务
for (int i = 0; i < count; i++) {
service.execute(new NumberTask2(i,barrier));
}
//关闭线程池
service.shutdown();
}
}
/**
* 任务
*/
class NumberTask2 implements Runnable{
private Integer id;
private CyclicBarrier barrier;
public NumberTask2(Integer id, CyclicBarrier barrier) {
this.id = id;
this.barrier = barrier;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+":执行完任务["+id+"]");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
打印结果如下:
//结果1
pool-1-thread-1:执行完任务[0]
pool-1-thread-2:执行完任务[1]
pool-1-thread-1:执行完任务[2]
pool-1-thread-2:执行完任务[3]
pool-1-thread-1:执行完任务[4]
//结果2
pool-1-thread-1:执行完任务[0]
pool-1-thread-2:执行完任务[1]
结果1中我们使用的线程池只有2个线程,当子线程完成自己的任务后,会继续去处理其他子任务.而结果2只会打印出两条记录,而且线程不会结束一直阻塞.这是因为在屏障点的线程需要等待其他线程都到达了屏障点才回继续执行,它们只能在屏障点等待.