Java并发56:ForkJoin并发框架的原理、2种ForkJoinTask的用法以及ForkJoinPool的常用方法
本章主要对ForkJoin并发框架进行学习,主要内容分为三个部分:
ForkJoin并发框架的浅谈
ForkJoin并发编程的两个实例
ForkJoinPool线程池的常用方法说明
1.ForkJoin并发框架的浅谈
1.1.Fork和Join
ForkJoin并发框架:Fork=分解 + Join=合并
ForkJoin并发框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割(Fork)成若干个小任务,最终汇总(Join)每个小任务结果后得到大任务结果的框架。
例如:计算1+2+…1000000000,可以将其分割(Fork)为100000个小任务,每个任务计算10000个数据的相加,最终汇总(Join)这100000个小任务的计算结果进行合并,得到计算结果。
1.2.工作窃取算法
ForkJoin并发框架是采取工作窃取(Work-Stealing)算法实现的。
工作窃取算法:某个线程从其他线程的工作队列中窃取任务来执行。可以形象的用下面的图表示:
下面来详细说明工作窃取算法(模拟):
有一个较大的任务划分成了10个小任务。
这10个小任务在一个大小为2的线程池中执行。
线程池中的2个核心线程,每个线程的队列中有5个任务。
线程1的任务都很简单,所以它很快就将5个任务执行完毕。
线程2的任务都很复杂,当线程1执行完5个任务时,他才执行了3个任务。
这时,线程1不会空闲,而且窃取线程2的等待队列中的任务(从末端开始窃取)来执行。
当线程2的队列中也没有了任务之后,线程1和线程2才空闲。
优缺点
整体上,这种窃取算法,提高了线程利用率。
为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列。
存在两个线程共同竞争同一个任务的可能,例如双端队列中只有一个任务时。
1.3.编程思想
ForkJoin并发框架应用了两种十分重要的编程思想:
分而治之
递归
1.4.ForkJoin的主要类
ForkJoin并发框架的主要类包括:
ForkJoinPool:ForkJoin线程池,实现了ExecutorService接口和工作窃取算法,用于线程调度与管理。
ForkJoinTask:ForkJoin任务,提供了fork()方法和join()方法。通常不直接使用,而是使用以下子类:
RecursiveAction:无返回值的任务,通常用于只fork不join的情形。
RecursiveTask:有返回值的任务,通常用于fork+join的情形。
1.5.ForkJoin的两类用法
根据ForkJoinTask的两种类型,可以将ForkJoin并发框架划分为两种用法:
only fork:递归划分子任务,分别执行,但是并不需要合并各自的执行结果。
fork+join:递归划分子任务,分别执行,然后递归合并计算结果。
only fork的示意图:
fork+join的示意图:
2.实例编码
2.2.RecursiveAction实例编码
场景说明:
真实场景:专网A内的数据库DB1上存储着100万条数据,需要通过数据交换服务发送到专网B的数据库DB2上。
原来的古老做法:由于带宽和服务器性能等限制,每次发送的数据不能超过5000条。所以将这100万数据按照5000条一组进行分组,然后每组都通过一个线程进行发送。但是不知道什么原因,总之DB2中会经常出现重复数据。
新的做法:根据ForkJoin框架编程思想,将这100万数据按照阈值THRESHOLD进行子任务划分,然后依次发送。
重点分析:
这个场景只是将100万数据分组进行分发,并不需要再将分组合并,所以属于上述的第一种only fork用法。
为了模拟对数据的接收,定义了一个ConcurrentLinkedQueue用于存储接收的数据。
如果DB2最终的数据量与DB1的数据量一直,则表明数据发送成功。
注意如何根据阈值THRESHOLD计算分组(fork())。
注意递归分组的用法。
代码:
/** * <p>ForkJoin框架实例1-RecursiveAction-无返回值-数据交换</p> * <p>数据交换:专网A内的数据库DB1上有100万数据,需要通过数据交换服务发送到专网B的数据库DB2上。 * 1.原来的做法:将这100万数据按照5000条一组进行分组,然后每组都通过一个线程进行发送。不知道什么原因,总之经常会出现重复发送的数据。 * 2.新的做法:根据ForkJoin框架编程思想,将这100万数据按照阈值THRESHOLD进行子任务划分,然后依次发送。</p> * * @author hanchao 2018/4/15 19:26 **/ public class RecursiveActionDemo { private static final Logger LOGGER = Logger.getLogger(RecursiveActionDemo.class); //模拟数据库DB2 static ConcurrentLinkedQueue DB2 = new ConcurrentLinkedQueue(); /** * <p>定义一个数据交换任务,继承自RecursiveAction,用于发送数据交换的JSON数据</p> * * @author hanchao 2018/4/15 19:28 **/ static class DataExchangeTask extends RecursiveAction { //阈值=5000 private static final int THRESHOLD = 5000; //开始索引 private int start; //结束索引 private int end; //交换的数据 List<String> list; public DataExchangeTask(int start, int end, List<String> list) { this.start = start; this.end = end; this.list = list; } @Override protected void compute() { //如果当前任务数量在阈值范围内,则发送数据 if (end - start < THRESHOLD) { //发送Json数据 try { sendJsonDate(this.list); } catch (InterruptedException e) { e.printStackTrace(); } } else { //如果当前任务数量超出阈值,则进行任务拆分 int middle = (start + end) / 2; //左边的子任务 DataExchangeTask left = new DataExchangeTask(start, middle, list); //右边的子任务 DataExchangeTask right = new DataExchangeTask(middle, end, list); //并行执行两个“小任务” left.fork(); right.fork(); } } /** * <p>发送数据</p> * * @author hanchao 2018/4/15 20:04 **/ private void sendJsonDate(List<String> list) throws InterruptedException { //遍历 for (int i = start; i < end; i++) { //每个元素都插入到DB2中 ==> 模拟数据发送到DB2 DB2.add(list.get(i)); } //假定每次发送耗时1ms Thread.sleep(1); } } /** * <p>模拟从数据库中查询数据并形成JSON个是的数据</p> * * @author hanchao 2018/4/15 20:21 **/ static void queryDataToJson(List list) { //随机获取100万~110万个数据 int count = RandomUtils.nextInt(1000000, 1100000); for (int i = 0; i < count; i++) { list.add("{\"id\":\"" + UUID.randomUUID() + "\"}"); } } /** * <p>RecursiveAction-无返回值:可以看成只有fork没有join</p> * * @author hanchao 2018/4/15 19:26 **/ public static void main(String[] args) throws InterruptedException { //从数据库中获取所有需要交换的数据 List dataList = new ArrayList<String>(); queryDataToJson(dataList); int count = dataList.size(); LOGGER.info("1.从DB1中读取数据并存放到List中,共计读取了" + count + "条数据."); //DB2的数据量 LOGGER.info("2.开始时,DB2中的数据量:" + DB2.size()); LOGGER.info("3.通过ForkJoin框架进行子任务划分,并发送数据"); //定义一个ForkJoin线程池 ForkJoinPool forkJoinPool = new ForkJoinPool(); //定义一个可分解的任务 DataExchangeTask dataExchangeTask = new DataExchangeTask(0, count, dataList); //向ForkJoin线程池提交任务 forkJoinPool.submit(dataExchangeTask); //线程阻塞,等待所有任务完成 forkJoinPool.awaitTermination(5, TimeUnit.SECONDS); //任务完成之后关闭线程池 forkJoinPool.shutdown(); //查询最终传输的数据量 LOGGER.info("4.结束时,DB2中的数据量:" + DB2.size()); //查询其中一条数据 LOGGER.info("5.查询其中一条数据:" + DB2.peek()); } }
运行结果:
2018-04-17 23:38:05 INFO - 1.从DB1中读取数据并存放到List中,共计读取了1037606条数据. 2018-04-17 23:38:05 INFO - 2.开始时,DB2中的数据量:0 2018-04-17 23:38:05 INFO - 3.通过ForkJoin框架进行子任务划分,并发送数据 2018-04-17 23:38:10 INFO - 4.结束时,DB2中的数据量:1037606 2018-04-17 23:38:10 INFO - 5.查询其中一条数据:{"id":"85bc8085-4836-41e3-b7f4-d80f38d8f0fe"}