【Java】多线程系列(二)之CountDownLatch的使用
2017-04-17 23:36
543 查看
前言
在多线程环境下,很多时候在主线程中需要等待子线程完成之后,再继续执行后面的代码。那么这种应用场景下可以利用CountDownLatch类来实现上面的功能。下面假设一种场景,现在有一个任务执行时间很长,前端需要请求数据时的响应速度很快。那么可以考虑把该任务计算之后的结果放在内存中(这里使用的是ServletContextListener),每次隔一段时间更新一次。假设这个任务可以拆成多个子任务,那么就可以考虑使用多线程来实现。那么问题来了,如果直接不等线程执行完毕,前端就请求内存中的数据,那么这样的话,可能就会出现一个问题:这个时候线程还没执行完毕,结果还没计算出来。
所以怎么解决这个问题呢?本篇博文利用CountDownLatch来实现上述功能。同时还给出了其他三种方案以供讨论。
代码结构:
下面放测试代码:
测试代码地址:
https://github.com/KingWang93/websleep_test
SleepListener.javapackage 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多线程组件一:CountDownLatch使用方法的总结
- [Java并发]使用CountDownLatch和CyclicBarrier等待多线程完成
- Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例
- Java多线程之CountDownLatch同步器的使用(六)
- Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例 (r)
- Java中CountDownLatch的使用和求多线程的运行时间
- Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例
- Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例
- Java多线程之~~~使用CountDownLatch来同步多个线程实现一个任务
- 【Java多线程】CountDownLatch、CyclicBarrier和Semaphore使用
- Java多线程系列--“JUC锁”09之 CountDownLatch原理和示例
- Java多线程编程之CountDownLatch同步工具使用实例
- Java多线程系列-多线程下的其他组件之CountDownLatch、Semaphore、Exchanger
- Java之CountDownLatch使用
- Java多线程与并发库高级应用之倒计时计数器CountDownLatch
- Java之CountDownLatch使用
- Java并发之CountDownLatch的使用和源码解析
- Java多线程20:多线程下的其他组件之CountDownLatch、Semaphore、Exchanger
- Java5 多线程(六)--CountDownLatch 同步工具类
- java多线程系列(六)---线程池原理及其使用