并发编程从入门到放弃系列开始和结束(五)

发布于 2022-6-13 17:41
浏览
0收藏

 

Fork/Join

Fork/Join 是一个并行执行任务的框架,利用的分而治之的思想。

Fork 是把一个大的任务拆分成若干个小任务并行执行,Join 则是合并拆分的子任务的结果集,最终计算出大任务的结果。

所以整个 Fork/Join 的流程可以认为就是两步:

  1. Fork 拆分任务,直到拆分到最小粒度不可拆分为止
  2. Join 计算结果,把每个子任务的结果进行合并

并发编程从入门到放弃系列开始和结束(五)-开源基础软件社区

 fork:join

这里我们需要介绍一下主要的几个类:

ForkJoinTask:就是我们的分治任务的抽象类

RecursiveTask:继承于 ForkJoinTask,用于计算有返回结果的任务

RecursiveAction: 继承于 ForkJoinTask,用于计算没有返回结果的任务

ForkJoinPool:用于执行 ForkJoinTask 任务的线程池,通常我们可以用 ForkJoinPool.commonPool() 去创建一个 Fork/Join 的线程池,然后用 submit 或者 invoke 去提交执行任务。

这里我们写一个测试程序,用于计算[0,999]的求和结果,所以我们写一个类继承 RecursiveTask ,并且实现他的 compute 方法。

invokeAll() 相当于每个任务都执行 fork,fork 之后会再次执行 compute 判断是否要继续拆分,如果无需拆分那么则使用 join 方法计算汇总结果。

public class ForkJoinTest {

    public static void main(String[] args) throws Exception {
        List<Integer> list = new LinkedList<>();
        Integer sum = 0;
        for (int i = 0; i < 1000; i++) {
            list.add(i);
            sum += i;
        }

        CalculateTask task = new CalculateTask(0, list.size(), list);
        Future<Integer> future = ForkJoinPool.commonPool().submit(task);
        System.out.println("sum=" + sum + ",Fork/Join result=" + future.get());
    }

    @Data
    static class CalculateTask extends RecursiveTask<Integer> {
        private Integer start;
        private Integer end;
        private List<Integer> list;

        public CalculateTask(Integer start, Integer end, List<Integer> list) {
            this.start = start;
            this.end = end;
            this.list = list;
        }

        @Override
        protected Integer compute() {
            Integer sum = 0;
            if (end - start < 200) {
                for (int i = start; i < end; i++) {
                    sum += list.get(i);
                }
            } else {
                int middle = (start + end) / 2;
                System.out.println(String.format("从[%d,%d]拆分为:[%d,%d],[%d,%d]", start, end, start, middle, middle, end));
                CalculateTask task1 = new CalculateTask(start, middle, list);
                CalculateTask task2 = new CalculateTask(middle, end, list);
                invokeAll(task1, task2);
                sum = task1.join() + task2.join();
            }
            return sum;
        }
    }
}
//输出
从[0,1000]拆分为:[0,500],[500,1000]
从[0,500]拆分为:[0,250],[250,500]
从[500,1000]拆分为:[500,750],[750,1000]
从[0,250]拆分为:[0,125],[125,250]
从[250,500]拆分为:[250,375],[375,500]
从[500,750]拆分为:[500,625],[625,750]
从[750,1000]拆分为:[750,875],[875,1000]
sum=499500,Fork/Join result=499500

使用完成之后,我们再来谈一下 Fork/Join 的原理。

先看 fork 的代码,调用 fork 之后,使用workQueue.push() 把任务添加到队列中,注意 push 之后调用 signalWork 唤醒一个线程去执行任务。

public final ForkJoinTask<V> fork() {
    Thread t;
    if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
        ((ForkJoinWorkerThread)t).workQueue.push(this);
    else
        ForkJoinPool.common.externalPush(this);
    return this;
}
final ForkJoinPool.WorkQueue workQueue; // 工作窃取

 final void push(ForkJoinTask<?> task) {
   ForkJoinTask<?>[] a; ForkJoinPool p;
   int b = base, s = top, n;
   if ((a = array) != null) {    // ignore if queue removed
     int m = a.length - 1;     // fenced write for task visibility
     U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
     U.putOrderedInt(this, QTOP, s + 1);
     if ((n = s - b) <= 1) {
       if ((p = pool) != null)
         p.signalWork(p.workQueues, this);
     }
     else if (n >= m)
       growArray();
   }
}

上面我们看到了 workQueue,这个其实就是我们说的工作队列,它是一个双端队列,并且有一个工作线程和他对应。

@sun.misc.Contended
static final class WorkQueue {
    volatile int base;         // 下一个出队列索引
    int top;                   // 下一个入队列索引
    ForkJoinTask<?>[] array;   // 队列中的 task
    final ForkJoinPool pool;   
    final ForkJoinWorkerThread owner; // 工作队列中的工作线程
    volatile Thread parker;    // == owner during call to park; else null
    volatile ForkJoinTask<?> currentJoin;  // 当前join的任务
    volatile ForkJoinTask<?> currentSteal; // 当前偷到的任务
}

那如果工作线程自己队列的做完了怎么办?只能傻傻地等待吗?并不是,这时候有一个叫做工作窃取的机制,所以他就会去其他线程的队列里偷一个任务来执行。

为了避免偷任务线程和自己的线程产生竞争,所以自己的工作线程是从队列头部获取任务执行,而偷任务线程则从队列尾部偷任务。

并发编程从入门到放弃系列开始和结束(五)-开源基础软件社区 工作窃取

 

Executor

Executor是并发编程中重要的一环,任务创建后提交到Executor执行并最终返回结果。

并发编程从入门到放弃系列开始和结束(五)-开源基础软件社区

 Executor

任务
线程两种创建方式:Runnable和Callable。

Runnable是最初创建线程的方式,在JDK1.1的版本就已经存在,Callable则在JDK1.5版本之后加入,他们的主要区别在于Callable可以返回任务的执行结果。

任务执行
任务的执行主要靠Executor,ExecutorService继承自Executor,ThreadPoolExecutor和ScheduledThreadPoolExecutor分别实现了ExecutorService。

并发编程从入门到放弃系列开始和结束(五)-开源基础软件社区

那说到线程池之前,我们肯定要提及到线程池的几个核心参数和原理,这个之前的文章也写到过,属于基础中的基础部分。

首先线程池有几个核心的参数概念:

  1. 最大线程数maximumPoolSize
  2. 核心线程数corePoolSize
  3. 活跃时间keepAliveTime
  4. 阻塞队列workQueue
  5. 拒绝策略RejectedExecutionHandler

当提交一个新任务到线程池时,具体的执行流程如下:

  1. 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
  2. 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
  3. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
  4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理

并发编程从入门到放弃系列开始和结束(五)-开源基础软件社区

拒绝策略主要有四种:

  1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
  2. CallerRunsPolicy:使用调用者所在的线程来处理任务
  3. DiscardOldestPolicy:丢弃等待队列中最老的任务,并执行当前任务
  4. DiscardPolicy:直接丢弃任务,也不抛出异常
    ThreadPoolExecutor
    通常为了快捷我们会用Executors工具类提供的创建线程池的方法快速地创建一个线程池出来,主要有几个方法,但是一般我们不推荐这样使用,非常容易导致出现问题,生产环境中我们一般推荐自己实现,参数自己定义,而不要使用这些方法。

创建

//创建固定线程数大小的线程池,核心线程数=最大线程数,阻塞队列长度=Integer.MAX_VALUE
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}
//创建只有一个线程的线程池,阻塞队列长度=Integer.MAX_VALUE
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}
//创建核心线程数为0,最大线程数=Integer.MAX_VALUE的线程池,阻塞队列为同步队列
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

最好的办法就是自己创建,并且指定线程名称:

new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), 
Runtime.getRuntime().availableProcessors()*2,
1000L, 
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactoryBuilder().setNameFormat("thread-name").build());

 

文章转自公众号:艾小仙

分类
标签
已于2022-6-13 17:41:26修改
收藏
回复
举报
回复
添加资源
添加资源将有机会获得更多曝光,你也可以直接关联已上传资源 去关联
    相关推荐