1. 问题描述
如题,现在接受到一个大任务,将这个任务交由计算机程序处理,计算密集型的,要求考虑适当的性能问题,并最终给出问题处理的报告。
注意,这个问题,描述中有几个非常重要的信息:
A. 是一个大任务 ---> 耗时不会短
B. 要由计算机程序处理 ---> 需要编写程序
C. 计算密集型 ---> 需要考虑任务划分策略
D. 最终给出问题处理报告 ---> 任务处理完前,是给不了的,这个报告动作只能在任务处理完后执行
2. 方案设计
结合问题描述,可以想到的是,我们可以采用多种方案:
1)、若有多个机器,可以将任务划分给多个机器一起处理,最终汇总,生成报告。这个是一个方案。
2)、若只有一台机器,我们最先要想到的是多线程进行处理,也可以用多进程。因为多进程涉及到进程上下文切换的问题,会有所影响性能,当然,不是主要的,要看是什么什么环境,这里,采用的是JAVA语言。可以考虑采用多线程方式,本案例将用多线程来解决这个问题。将大任务耗时长的作业,通过拆分,交由多线程进行处理,提高并发性。
3)、因为是计算密集型,所以,要重点考虑CPU时间片是否会被频繁的切换,频繁调度的话,也是会影响性能的;这里就采用和计算机核数一样多的线程数处理这个大任务。
4)、最终给出处理报告,必须要在任务处理完后,才做出报告,所以,程序设计的时候,要考虑报告事件是在多线程任务执行完毕后才执行的,这里就涉及到锁或者是等待同步问题。
综合上述分析过程,JAVA多线程实现,其实也有好多种方案,这里将重点讨论经典的两个JAVA多线程解决方案,分别是利用CountDownLatch和CyclicBarrier实现。
3. 代码实现
3.1 CountDownLatch方案
本方案,首先要搞清楚CountDownLatch的运行特点,他相当于是有一个同步点,通过倒计数的模式,每一个线程执行一次counDown(),促使CountDownLatch定义时指定的技术值减到0,即为满足到达同步点,此时,由CountDownLatch约束同步的后续事件可以进行。当然了,由CountDownLatch约束的任务,想要执行,必须得获取到了锁才行,即CountDownLatch的await()将会阻塞调用这个函数的线程T,直到线程T因为CountDownLatch的计数值减为0时获取锁后才能得以继续执行。
由此分析可以想到的是,依据CountDownLatch进行多任务同步约束,通常要设计两个CountDownLatch元素,一个相当于开关,另外一个相当于任务管理器,只有当开关打开,任务管理器里面的任务才开始运行,每一个任务执行完后,将任务管理器的计算值通过调用CountDownLatch的countDown()函数减1,直到计数器值为0,约束任务全部执行完后,即任务管理器CountDownLatch的await()阻塞解除,进入报告执行阶段。
下面是主程序:
package multiTask; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author shihuc * @date 2018年6月22日 上午9:23:31 * * 此处的任务描述: * * 有一个大的活,正好,当前计算机配置也很不错,多核, 需要考虑计算性能,高效的处理完,处理完后,告知一下客户端,任务已经搞定。 * */ public class SolutionCDL { /** * @author shihuc * @param args */ public static void main(String[] args) { int cpuCore = Runtime.getRuntime().availableProcessors(); CountDownLatch switchLatch = new CountDownLatch(1); CountDownLatch taskLatch = new CountDownLatch(cpuCore); /* * 构建任务线程,依据当前系统的核数,构建相应数量的线程量 */ ExecutorService executor = Executors.newFixedThreadPool(cpuCore); for(int i=0; i<cpuCore; i++){ executor.submit(new WorkerCDL(switchLatch, taskLatch, "task" + i + " works hard...")); } executor.shutdown(); /* * 启动全局开关,让任务开始干活 */ switchLatch.countDown(); /* * 这里进行收尾工作,即当所有的worker线程完成任务后,进行的工作。本案例中,就是告知所有的活都搞定了 */ try { //所有的worker线程都结束后,即相应的countDown()函数被执行后,这里的await将会被解锁,进入后续任务。 taskLatch.await(); finalWork("CountDownLathc, Yeah, all the task is finished perfect..."); } catch (InterruptedException e) { e.printStackTrace(); } } private static void finalWork(String inf) { System.out.println(inf); } }
下面是任务程序:
package multiTask; import java.util.concurrent.CountDownLatch; /** * @author shihuc * @date 2018年6月22日 上午9:24:01 */ public class WorkerCDL implements Runnable { private CountDownLatch switcher; private CountDownLatch worker; private String info; public WorkerCDL(CountDownLatch s, CountDownLatch w, String info){ this.switcher = s; this.worker = w; this.info = info; } /* (non-Javadoc) * @see java.lang.Runnable#run() */ @Override public void run() { try { /* * 首先将干活的开关进行关闭,相当于设置一道统一的门,只有门打开的时候,才能进行后续的工作。 * 当线程运行到此处时,即遇到await时,处于阻塞状态,只有该门打开时,也就是开关管理器(CountDownLatch)计数器降到0了,才会开门,释放锁。 */ switcher.await(); /* * 这里相当于具体的干活逻辑模块 */ doWork(info); /* * 活干完了,当前任务管理器(CountDownLatch的实例worker)要将计数器减1。 * 主任务管理器将依据此处的任务管理器是否全部完成来决定能否进入后续的逻辑。 */ worker.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } } private void doWork(String s) { //模拟这个任务耗时比较长时间,这里固定耗时2s try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(s); } }
3.2 CyclicBarrier方案
JDK7的官方文档描述CyclicBarrier的内容如下:
A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.A CyclicBarrier supports an optional
Runnable
command that is run once per barrier point, after the last thread in the party arrives, but before any threads are released. This barrier action is useful for updating shared-state before any of the parties continue.
其意思大概是:
一个同步的助手,他允许一批线程彼此等待,直到都到达一个统一的栅栏点。CyclicBarrier在涉及一个固定大小的线程组编程中非常有用,这个线程组的每个线程必须要相互等待对方(到达某一个point)。这个栅栏之所以成为Cyclic(循环)的,是因为它可以被重用,当等待的线程被释放后。
一个CyclicBarrier支持一个选项Runnable指令,也就是一个线程,支持一次性运行,当固定线程组中最后一个线程到达栅栏点时,且在任何线程释放前,对于每一个栅栏点(barrier point)到达,此时,这个Runnable线程即启动执行。这个栅栏行为非常重要,对于在任何一个线程继续执行前更新共享状态。
从这个描述来看,CyclicBarrier的工作逻辑,是针对一组固定大小的线程组,每一个线程必须相互等待,直到所有的线程都达到一个共同的状态后(每一个线程都调用了await(),表示达到了共同状态,即barrier point达到) ,才能进行后续约定的任务。对于本博文开篇提到的问题,最后给出问题处理报告。也就是说,在达到栅栏点即barrier point后,在Runnable这个线程里进行报告。
具体来说,CyclicBarrier的每一个线程,都执行一个任务,执行完后,调用await(),进行相互等待状态。从而可以实现,所有的线程调用await()后就相互等待了。最后一个任务线程执行了await()即进入报告执行阶段对应的Runnable线程。
主体程序:
package multiTask; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author shihuc * @date 2018年6月22日 上午10:53:49 * * 有一个大的活,正好,当前计算机配置也很不错,多核, 需要考虑计算性能,高效的处理完,处理完后,告知一下客户端,任务已经搞定。 */ public class SolutionCB { /** * @author shihuc * @param args */ public static void main(String[] args) { int cpuCore = Runtime.getRuntime().availableProcessors(); /* * cpuCore指定的任务都到达了await后,即触发CyclicBarrier定义时第2个参数指定的任务(Thread)的执行 */ CyclicBarrier barrier = new CyclicBarrier(cpuCore, new Runnable(){ @Override public void run() { finalWork("CyclicBarrier, Yeah, all the task is finished perfect..."); } }); ExecutorService executor = Executors.newFixedThreadPool(cpuCore); for(int i=0; i<cpuCore; i++){ executor.submit(new WorkerCB(barrier, "task" + i + " works hard...")); } executor.shutdown(); } private static void finalWork(String inf) { System.out.println(inf); } }
工作程序:
package multiTask; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; /** * @author shihuc * @date 2018年6月22日 上午10:54:56 */ public class WorkerCB implements Runnable { private CyclicBarrier barrier; private String info; public WorkerCB(CyclicBarrier br, String info){ this.barrier = br; this.info = info; } @Override public void run() { /** * 每个任务,在执行前都会在barrier这个形象的栅栏处等待,等待所有的参与者到齐了,再进行正式的多线程干活 */ try { /* * 干活 */ doWork(info); /* * 活干完后,就到达了barrier设定的栅栏处了 */ barrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } private void doWork(String inf) { //模拟这个任务耗时比较长时间,这里固定耗时2s try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(inf); } }
总结:
1. 一般涉及到大任务并发执行,都要考虑状态同步,也就是最终什么时候汇总结果的问题。
2. 计算密集型任务的多线程执行,CPU核数决定了线程数。尽量减少CPU时间片切换导致的性能损耗。 区别IO密集型任务的多线程模型,IO密集型的多线程问题,往往因为网络或者磁盘响应慢,CPU特别快,这个时候线程可以尽量比CPU核数多,性能就会相对更好。
3. CountDownLatch和CyclicBarrier都有一个类似的栅栏点,即共同等待的一个同步点,CountDownLatch模式通常要两个元素参与,而CyclicBarrier通常一个即可完成(充分利用CyclicBarrier的候选参数,即第二个参数Runnable指令线程)。