多线程之Fork/Join使用

Fork/Join介绍

         Fork/Join框架是Java 7提供的用于并行执行任务的框架。具体是把大任务切分为小任务,再把小任务的结果汇总为大任务的结果。核心思想是工作窃取算法,工作窃取算法是指线程从其他任务队列中窃取任务执行。

如何使用Fork/Join

  • 分割任务:首先需要创建一个ForkJoin任务,执行该类的fork方法可以对任务不断切割,直到分割的子任务足够小
  • 合并任务执行结果:子任务执行的结果同一放在一个队列中,通过启动一个线程从队列中取执行结果。

常见使用场景

  • 大数据计算

简单的实例代码

 

 
  1. public class Test {
  2.  
  3. private Integer num;
  4.  
  5. private String name;
  6.  
  7. public Integer getNum() {
  8. return num;
  9. }
  10.  
  11. public void setNum(Integer num) {
  12. this.num = num;
  13. }
  14.  
  15. public String getName() {
  16. return name;
  17. }
  18.  
  19. public void setName(String name) {
  20. this.name = name;
  21. }
  22.  
  23. @Override
  24. public String toString() {
  25. return "Test{" +
  26. "num=" + num +
  27. ", name='" + name + '\'' +
  28. '}';
  29. }
  30. }
 
 
  1. import java.util.ArrayList;
  2. import java.util.List;
  3. import java.util.concurrent.ExecutionException;
  4. import java.util.concurrent.ForkJoinPool;
  5. import java.util.concurrent.ForkJoinTask;
  6. import java.util.concurrent.RecursiveTask;
  7.  
  8. /**
  9. * 如何使用 forkjoin
  10. * 1.创建任务类 继承RecursiveTask<返回对象>
  11. * 2。ForkJoinPool pool = new ForkJoinPool();
  12. * forkjoinPool 通过它来执行任务
  13. * ForkJoinTask<返回对象> submit = pool.submit(task);
  14. * submit.get();获取返回的对象
  15. * */
  16. public class CountTask extends RecursiveTask<List<Test>> {
  17.  
  18. // 临界值
  19. private static final int THRESHOLD = 10;
  20.  
  21. private List<Integer> integers ;
  22.  
  23. public CountTask(List<Integer> integers) {
  24. this.integers = integers;
  25. }
  26.  
  27. @Override
  28. protected List<Test> compute() {
  29. List<Test> tests = new ArrayList<>();
  30. boolean compute = integers.size() <= THRESHOLD;
  31. if (compute) {
  32. // 真正执行的任务,分割好的最小任务
  33. for (int i = 0; i < integers.size(); i++) {
  34. Test test = new Test();
  35. test.setName("name"+i);
  36. test.setNum(i);
  37. tests.add(test);
  38. }
  39. System.out.println("执行方法任务中");
  40. } else {
  41. System.out.println("执行拆分任务开始");
  42. List<List<Integer>> lists = CountTask.averageAssign(integers, 2);
  43. // 递归
  44. CountTask task1 = new CountTask(lists.get(0));
  45. CountTask task2 = new CountTask(lists.get(1));
  46. // 拆分任务,把任务压入线程队列
  47. invokeAll(task1, task2);
  48. //得到小任务的值
  49. List<Test> task1Res = task1.join();
  50. List<Test> task2Res = task2.join();
  51. task1Res.addAll(task2Res);
  52. tests = task1Res;
  53. System.out.println("执行任务结束");
  54. }
  55. return tests;
  56. }
  57.  
  58. /**
  59. * 将一组数据平均分成n组
  60. *
  61. * @param source 要分组的数据源
  62. * @param n 平均分成n组
  63. * @param <T>
  64. * @return
  65. */
  66. public static <T> List<List<T>> averageAssign(List<T> source, int n) {
  67. List<List<T>> result = new ArrayList<>();
  68. int remainder = source.size() % n; //(先计算出余数)
  69. int number = source.size() / n; //然后是商
  70. int offset = 0;//偏移量
  71. for (int i = 0; i < n; i++) {
  72. List<T> value;
  73. if (remainder > 0) {
  74. value = source.subList(i * number + offset, (i + 1) * number + offset + 1);
  75. remainder--;
  76. offset++;
  77. } else {
  78. value = source.subList(i * number + offset, (i + 1) * number + offset);
  79. }
  80. result.add(value);
  81. }
  82. return result;
  83. }
  84.  
  85.  
  86.  
  87. public static void main(String[] args) throws ExecutionException, InterruptedException {
  88. List<Integer> integers = new ArrayList<>();
  89. for (int i = 0; i < 100; i++) {
  90. integers.add(i);
  91. }
  92. ForkJoinPool pool = new ForkJoinPool();
  93. CountTask task = new CountTask(integers);
  94. ForkJoinTask<List<Test>> submit = pool.submit(task);
  95. List<Test> tests = submit.get();
  96. System.out.println("Final result:" + tests);
  97. System.out.println(tests.size());
  98. // 关闭线程池
  99. pool.shutdown();
  100. }
  101. }
 

图解

 

拓展

Java 8 stream 并行流 底层也是ForkJoin实现

Java 8 并行流(parallel stream)采用共享线程池,对性能造成了严重影响。可以包装流来调用自己的线程池解决性能问题。

ForkJoinPool.commonPool() 

使用共享线程池

new ForkJoinPool(num)

它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

创建自己的线程池,所以可以避免共享线程池,如果有需要,甚至可以分配比处理机数量更多的线程

ForkJoinPool forkJoinPool = new ForkJoinPool(<numThreads>);

需要特别注意的是:

  1. ForkJoinPool 使用submit 或 invoke 提交的区别:invoke是同步执行,调用之后需要等待任务完成,才能执行后面的代码;submit是异步执行,只有在Future调用get的时候会阻塞。
  2. 这里继承的是RecursiveTask,还可以继承RecursiveAction。前者适用于有返回值的场景,而后者适合于没有返回值的场景
  3. 这一点是最容易忽略的地方,其实这里执行子任务调用fork方法并不是最佳的选择,最佳的选择是invokeAll方法。
 
  1. eftTask.fork();
  2. rightTask.fork();
  3.  
  4. 替换为
  5.  
  6. invokeAll(leftTask, rightTask);
 

那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢?

        首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。

注意在多线程环境下 对集合进行操作 线程安全问题 

java.util.ConcurrentModificationException

可以使用线程安全的集合修饰

 

 

参考连接

高并发之Fork/Join框架使用及注意事项

Fork/Join框架原理及应用

Java8 parallelStream —— 替换默认的共享线程池ForkJoinPool.commonPool()

介绍 ForkJoinPool 的适用场景,实现原理

ForkJoinPool的使用

posted @ 2024-12-11 15:27  CharyGao  阅读(21)  评论(0编辑  收藏  举报