前言
在多线程环境下,很多时候在主线程中需要等待子线程完成之后,再继续执行后面的代码。那么这种应用场景下可以利用CountDownLatch类来实现上面的功能。
下面假设一种场景,现在有一个任务执行时间很长,前端需要请求数据时的响应速度很快。那么可以考虑把该任务计算之后的结果放在内存中(这里使用的是ServletContextListener),每次隔一段时间更新一次。假设这个任务可以拆成多个子任务,那么就可以考虑使用多线程来实现。那么问题来了,如果直接不等线程执行完毕,前端就请求内存中的数据,那么这样的话,可能就会出现一个问题:这个时候线程还没执行完毕,结果还没计算出来。
所以怎么解决这个问题呢?本篇博文利用CountDownLatch来实现上述功能。同时还给出了其他三种方案以供讨论。
代码结构:
下面放测试代码:
测试代码地址:
https://github.com/KingWang93/websleep_test
SleepListener.java
package test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/*
* 本方案解决一个问题,在服务器启动之前保证每个任务至少执行过一遍
* 应用场景:例如系统里需要展示轨迹数据,一打开系统页面,就得有轨迹数据。
* 这个时候每种轨迹数据可以用一个线程计算,最后汇总计算的结果放在内存里面。
* 因此这个时候前端直接请求内存中的数据即可
* 代码中提供了多种方案,并进行方法效果上的对比
*/
public class SleepListener implements ServletContextListener {
ExecutorService executor1 = Executors.newFixedThreadPool(3);
static CountDownLatch cdl = new CountDownLatch(3);
@Override
public void contextDestroyed(ServletContextEvent sce) {
executor1.shutdownNow();
// executor1.shutdown();
System.out.println("正在关闭线程池");
}
@Override
public void contextInitialized(ServletContextEvent sce) {
/*
* method1:利用CountDownLatch来等待子线程全部执行完毕
*/
task1 task1=new task1();
task2 task2=new task2();
task3 task3=new task3();
executor1.submit(task1);
executor1.submit(task2);
executor1.submit(task3);
long start,end;
start=System.currentTimeMillis();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
end=System.currentTimeMillis();
System.out.println("耗时:"+(end-start));//耗时取决于耗时最长的线程
System.out.println("全部至少执行一遍!");
/*
* method2:不需要使用CountDownLatch,相应的在各个task里面不需要使用CountDownLatch里的东西
* 其实就是先执行一遍各个方法,但是这种方式在第一遍执行的时候需要完成
*/
// task1 task1=new task1();
// task2 task2=new task2();
// task3 task3=new task3();
// long start,end;
// start=System.currentTimeMillis();
// task1.exectask();
// task2.exectask();
// task3.exectask();
// end=System.currentTimeMillis();
// System.out.println("耗时:"+(end-start));//耗时取决于所有任务执行完毕的时间总和
// System.out.println("全部至少执行一遍!");
// executor1.submit(task1);
// executor1.submit(task2);
// executor1.submit(task3);
/*
* method3:利用线程的join方法来实现,这个需要利用Runnable接口。 Task1不是像上面的方法一样循环执行,而是单次执行
*/
// task1 task1 = new task1();
// Thread t1 = new Thread(task1);
// task2 task2 = new task2();
// Thread t2 = new Thread(task2);
// task3 task3 = new task3();
// Thread t3 = new Thread(task3);
// t1.start();
// t2.start();
// t3.start();
// long start, end;
// start = System.currentTimeMillis();
// try {
// t1.join();
// t2.join();
// t3.join();
// end = System.currentTimeMillis();
// System.out.println("耗时:" + (end - start));//取决于耗时最长的那个线程
// System.out.println("全部至少执行一遍!");
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// ScheduledExecutorService exec = Executors.newScheduledThreadPool(3);
// exec.scheduleWithFixedDelay(new task1(), 0,3, TimeUnit.SECONDS);
// exec.scheduleWithFixedDelay(new task2(), 0,5, TimeUnit.SECONDS);
// exec.scheduleWithFixedDelay(new task3(), 0,1, TimeUnit.SECONDS);
/*
* method4:利用ExecutorService的involeAll()方法来完成
*/
//下面的task需要以Callable接口实现,目前代码里并没有作相应修改,因此编译会有错误,使用时需要自己实现Callable接口,本文只提供此方法的思路
// task1 task1=new task1();
// task2 task2=new task2();
// task3 task3=new task3();
// ArrayList<Callable<String>> list=new ArrayList<Callable<String>>();
// list.add(task1);
// list.add(task2);
// list.add(task3);
// executor1.invokeAll(list);
//后面利用ScheduledExecutorService的scheduleWithFixedDelay()函数来执行定时任务,与上面的method3类似,代码实现下面省略
//....
/*
* method4:每个线程单独写一个Listener,也可以解决这个问题,但是这样编码冗余,这里不展开
*/
}
}
task1.java
package test;
public class task1 implements Runnable{
boolean is=false;
@Override
public void run() {
while(true){
exectask();//在执行SleepListener类的method3时,run()方法内部仅保留该行代码
if(!is){
SleepListener.cdl.countDown();
is=true;
}
}
}
public void exectask(){
try {
System.out.println("正在执行task1...");
Thread.sleep(3000);
System.out.println("task1耗时3秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
task2.java
package test;
public class task2 implements Runnable{
boolean is=false;
@Override
public void run() {
while(true){
exectask();//在执行SleepListener类的method3时,run()方法内部仅保留该行代码
if(!is){
SleepListener.cdl.countDown();
is=true;
}
}
}
public void exectask(){
try {
System.out.println("正在执行task2...");
Thread.sleep(5000);
System.out.println("task2执行耗时5秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
task3.java
package test;
public class task3 implements Runnable{
boolean is=false;
@Override
public void run() {
while(true){
exectask();//在执行SleepListener类的method3时,run()方法内部仅保留该行代码
if(!is){
SleepListener.cdl.countDown();
is=true;
}
}
}
public void exectask(){
try {
System.out.println("正在执行task3...");
Thread.sleep(1000);
System.out.println("task3耗时1秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
另外两个Listener文件,主要是用来测试tomcat在加载监听器的时候的顺序(其实是按照web.xml里面配置的监听器的顺序决定的)。
这个问题已经在之前的一篇博文中已经介绍过
【Java】问题总结集锦
CountDownLatch的使用:
CountDownLatch类是一个同步计数器,构造时传入int参数,该参数就是计数器的初始值,每调用一次countDown()方法,计数器减1,计数器大于0 时,await()方法会阻塞程序继续执行。
因此在SleepListener里面可以使用CountDownLatch来对线程进行阻塞,等待所有线程执行完毕。
上面的method1即是这种做法,后面的method2、3、4、5也提供了相应的解决方案。但是个人觉得还是第一个方案比较好用。