您的位置:首页 > 编程语言 > Java开发

【Java】多线程系列(二)之CountDownLatch的使用

2017-04-17 23:36 543 查看

前言

在多线程环境下,很多时候在主线程中需要等待子线程完成之后,再继续执行后面的代码。那么这种应用场景下可以利用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也提供了相应的解决方案。但是个人觉得还是第一个方案比较好用。

教程推荐:

多线程教程视频地址
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 多线程 计数器