Fork/join框架之ForkJoinPool

本文详细介绍了ForkJoinPool的工作原理及其与ExecutorService的区别。ForkJoinPool是一种用于执行ForkJoinTask的线程池,它采用了work-stealing模式来提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

jdk7新增了并发框架-fork/join框架,在这种框架下,ForkJoinTask代表一个需要执行的任务,真正执行这些任务的线程是放在一个线程池(ForkJoinPool)里面。ForkJoinPool是一个可以执行ForkJoinTask的ExcuteService,与ExcuteService不同的是它采用了work-stealing模式:所有在池中的线程尝试去执行其他线程创建的子任务,这样就很少有线程处于空闲状态,非常高效。

池中维护着ForkJoinWorkerThread对象数组,数组大小由parallelism属性决定,parallelism默认为处理器个数

[java]  view plain  copy
 print ?
  1. int n = parallelism << 1;  
  2.         if (n >= MAX_ID)  
  3.             n = MAX_ID;//MAX_ID=0x7fff  
  4.         else {   
  5.             n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8;  
  6.         }  
  7.         workers = new ForkJoinWorkerThread[n + 1];  
可见线程个数不会超过0x7fff。

添加线程

什么情况下需要添加线程呢?当新的任务到来,线程池会通知其他线程前去处理,如果这时没有处于等待的线程或者处于活动的线程非常少(这是通过ctl属性来判断的),就会往线程池中添加线程。

[java]  view plain  copy
 print ?
  1. private void addWorker() {  
  2.     Throwable ex = null;  
  3.     ForkJoinWorkerThread t = null;  
  4.     try {  
  5.         t = factory.newThread(this);  
  6.     } catch (Throwable e) {  
  7.         ex = e;  
  8.     }  
  9.     if (t == null) {  // null or exceptional factory return  
  10.         long c;       // adjust counts  
  11.         do {} while (!UNSAFE.compareAndSwapLong  
  12.                      (this, ctlOffset, c = ctl,  
  13.                       (((c - AC_UNIT) & AC_MASK) |  
  14.                        ((c - TC_UNIT) & TC_MASK) |  
  15.                        (c & ~(AC_MASK|TC_MASK)))));  
  16.         // Propagate exception if originating from an external caller  
  17.         if (!tryTerminate(false) && ex != null &&  
  18.             !(Thread.currentThread() instanceof ForkJoinWorkerThread))  
  19.             UNSAFE.throwException(ex);  
  20.     }  
  21.     else  
  22.         t.start();  
  23. }  
添加线程的代码比较简单,通过工厂类创建一个线程,通过调用ForkJoinWorkerThread的run方法启动这个线程。如果失败则恢复ctl以前的值,并终止线程。工厂类直接调用其构造方法,最终添加线程其实是在registerWorker方法完成的

[java]  view plain  copy
 print ?
  1. for (int g;;) {  
  2.             ForkJoinWorkerThread[] ws;  
  3.             if (((g = scanGuard) & SG_UNIT) == 0 &&  
  4.                 UNSAFE.compareAndSwapInt(this, scanGuardOffset,  
  5.                                          g, g | SG_UNIT)) {  
  6.                 int k = nextWorkerIndex;  
  7.                 try {  
  8.                     if ((ws = workers) != null) { // ignore on shutdown  
  9.                         int n = ws.length;  
  10.                         if (k < 0 || k >= n || ws[k] != null) {  
  11.                             for (k = 0; k < n && ws[k] != null; ++k)  
  12.                                 ;  
  13.                             if (k == n)  
  14.                                 ws = workers = Arrays.copyOf(ws, n << 1);  
  15.                         }  
  16.                         ws[k] = w;  
  17.                         nextWorkerIndex = k + 1;  
  18.                         int m = g & SMASK;  
  19.                         g = (k > m) ? ((m << 1) + 1) & SMASK : g + (SG_UNIT<<1);  
  20.                     }  
  21.                 } finally {  
  22.                     scanGuard = g;  
  23.                 }  
  24.                 return k;  
  25.             }  
  26.             else if ((ws = workers) != null) { // help release others  
  27.                 for (ForkJoinWorkerThread u : ws) {  
  28.                     if (u != null && u.queueBase != u.queueTop) {  
  29.                         if (tryReleaseWaiter())  
  30.                             break;  
  31.                     }  
  32.                 }  
  33.             }  

这里有个属性scanGuard有必要提一下,从Guard这个词可以知道它是在保护什么,是在保护works这个数组。当需要更新这个数组时,通过不断地检查scanGuard来达到保护的目的。

整个框架大量采用顺序锁,好处是不用阻塞,不好的地方是会有额外的循环。这里也是通过循环来注册这个线程,在循环的过程中有两种情况发生:1、compareAndSwapInt操作成功;2、操作失败。

第一种情况:扫描workers数组,找到一个为空的项,并把新创建的线程放在这个位置;如果没有找到,表示数组大小不够,则将数组扩大2倍;

第二种情况:需要循环重新尝试直接成功为止,从代码中可以看出,即使是失败了,也不忘做一些额外的事:通知其他线程去执行没有完成的任务。

执行任务

除了从ExecutorService继承过来的execute和submit方法外,还对这两个方法进行了覆盖和重载。
[java]  view plain  copy
 print ?
  1. public void execute(ForkJoinTask<?> task) {  
  2.     if (task == null)  
  3.         throw new NullPointerException();  
  4.     forkOrSubmit(task);  
  5. }  
  6.   
  7. public void execute(Runnable task) {  
  8.     if (task == null)  
  9.         throw new NullPointerException();  
  10.     ForkJoinTask<?> job;  
  11.     if (task instanceof ForkJoinTask<?>) // avoid re-wrap  
  12.         job = (ForkJoinTask<?>) task;  
  13.     else  
  14.         job = ForkJoinTask.adapt(task, null);  
  15.     forkOrSubmit(job);  
  16. }  
对参数为Runnable的execute进行了加强,将Runnable这种普通任务适配成ForkJoinTask这种任务,然后做为参数传给forkOrSubmit方法统一处理。
[java]  view plain  copy
 print ?
  1. private <T> void forkOrSubmit(ForkJoinTask<T> task) {  
  2.     ForkJoinWorkerThread w;  
  3.     Thread t = Thread.currentThread();  
  4.     if (shutdown)  
  5.         throw new RejectedExecutionException();  
  6.     if ((t instanceof ForkJoinWorkerThread) &&  
  7.         (w = (ForkJoinWorkerThread)t).pool == this)  
  8.         w.pushTask(task);  
  9.     else  
  10.         addSubmission(task);  
  11. }  
从以上代码可以看出,这两种任务最终的归属还是不一样,ForkJoinTask这种任务被放到线程内部的队列里面,普通的Runnable任务被放到线程池的队列里面了。
除了通过调用execute方法外,对于ForkJoinTask任务通过调用fork方法也可以向自己所在的线程队列中添加一个任务。

终止线程池

和ExecutorService一样,可以调用shutdown()和 shutdownNow()来终止线程,会先设置每个线程的任务状态为CANCELLED,然后调用Thread的interrupt方法来终止每个线程。

总结:ForkJoinPool就是一个ExcuteService,与ExcuteService不同的是:

1、ExcuteService执行Runnable/Callable任务,ForkJoinPool除了可以执行Runnable任务外,还可以执行ForkJoinTask任务;

2、ExcuteService中处于后面的任务需要等待前面任务执行后才有机会执行,而ForkJoinPool会采用work-stealing模式帮助其他线程执行任务,即ExcuteService解决的是并发问题,而ForkJoinPool解决的是并行问题。

下一节分析Fork/Join框架中的ForkJoinTask任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值