并发编程-07 ForkJoin分治

ForkJoin工作原理

ForkJoin框架时对线程池普通线程池的补充。我们通常在使用线程池时,会有哪些痛点?

  • 无法对大任务进行拆分,单个任务只能交个单个线程执行
  • 当阻塞队列中有积压时,工作线程从队列中获取任务会存在竞争

本篇文章就来研究Doug Lea是如何巧妙利用分治思想解决上述问题的。在开始看ForkJoin前,需要先对线程池线程数和死锁有些认识,否则啃完ForkJoin框架,也用不好它。

1、如何设计线程池的线程数?

通常我们会用如何方式,自定义线程池,核心线程、非核心线程、非核心线程的等待时间、阻塞队列等。那么线程数如何取值更合理的?

new ThreadPoolExecutor(nThreads, nThreads,
                              0L, TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>());
1.1 CPU密集型的任务
  • 主要任务类型:加密、解密、压缩、计算等计算密集型任务
  • 特点:计算压力大,CPU消耗大
  • 最佳线程数:CPU核心数的1-2倍
  • 原因:CPU密集型任务,主要耗时在计算上。线程需要充分利用CPU持续的完成大量计算任务,这时如果线程数量过多反而会因频繁的上下文切换,而造成性能下降。
1.2 IO密集型的计算
  • 主要任务类型:数据库读写IO、网络IO、磁盘文件读写IO等网络通信任务
  • 特点:CPU压力小,IO等待耗时长
  • 最佳线程数:CPU核心数的N倍
  • 原因:CPU计算压力小,更多的时间消耗在IO的阻塞上,频繁的切换上下文,能减少工作队列的等待耗时。
1.3 从业务实际角度考量
  • 考虑业务的并发量、每秒可以处理的请求数、阻塞队列长度兜底保证浪涌情况下,不丢失请求
  • 想要获取更准确线程数,需要经过压测来检验,结合实际情况,合理利用资源。

2、如何看待线程死锁、活锁

2.1 死锁问题
2.1.1 什么是死锁?

面试官:什么是死锁?(回答我,我就录取你!)

答:你录取我,我就告诉你什么是死锁!

死锁用生活中的道理表述 :双方继续执行下去的前提是,对方先满足自己的要求。(线程互相等待)

2.1.2 怎么解决死锁问题?

锁成立的必要条件:

  • 互斥条件[AQS中的信号量state]

  • 占有且等待[排它锁占有时,阻塞]

  • 不可强行占有[当线程已获取到锁,在它释放前,其他线程无法cas占有]

  • 循环等待条件(打破等待条件即可解决死锁)

2.1.3 死锁问题的场景
  • 线程间相互等待导致的死锁。

    打破循环等待条件即可

    案例

  • 线程饥饿,线程池瞬间满载阻塞,且没有中断策略。导致线程池无线程可用导致的死锁。ForkJoin如何拿来做阻塞类型任务,当线程饥饿时,极有可能导致该类问题。

    案例

2.1.4 活锁问题

线程A、B相互谦让,A发现B需要执行,将执行权交给B;线程B发现线程A需要执行,将执行权交给A,相互谦让,生成活锁。

案例

3、Fork/Join框架介绍

http://gee.cs.oswego.edu/dl/papers/fj.pdf Doug Lea 关于ForkJoin的论文

3.1 分治思想

思想比代码重要。充分利用多线程的优势,集中将庞大地、可拆分地任务进行fork(),分解到通过多个工作线程分别承担一部分任务,通过子线程的并行计算最终递归得到执行结果。
在这里插入图片描述

3.2 ForkJoinPool

3.2.1 构造方法

  • parallelism 线程池的并行数[控制线程工厂中线程个数]

    控制工作线程的数量,未设置则使用默认的Runtime.getRuntime().availableProcessors()核数

  • factory [负责工作线程创建的线程工厂]

    可是实现ForkJoinWorkerThreadFactory来自定义,默认DefaultForkJoinWorkerThreadFactory

  • handler[异常处理]

    UncaughtExceptionHandler运行时异常的处理器

  • asyncMode

    设置队列的工作模式:asyncMode ? FIFO_QUEUE : LIFO_QUEUE。相当于是否公平

public ForkJoinPool(int parallelism,
                    ForkJoinWorkerThreadFactory factory,
                    UncaughtExceptionHandler handler,
                    boolean asyncMode) {
    this(checkParallelism(parallelism),
         checkFactory(factory),
         handler,
         asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
         "ForkJoinPool-" + nextPoolId() + "-worker-");
    checkPermission();
}

3.2.2 核心任务提交方法

返回值方法
提交异步执行voidexecute(ForkJoinTask task)
execute(Runnable task)
等待并获取结果Tinvoke(ForkJoinTask task)
提交执行获取Future结果ForkJoinTasksubmit(ForkJoinTask task)
submit(Callable task)
submit(Runnable task)
submit(Runnable task, T result)
3.3 ForkJoinTask

ForkJoinTask是ForkJoinPool的核心之一,它是任务的实际载体,定义了任务执行时的具体逻辑和拆分逻辑。ForkJoinTask继承了Future接口,所以也可以将其看作是轻量级的Future。

3.3.1 ForkJoinTask核心方法

  • fork() [查分任务提交到线程池]

    fork()方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。

  • join() [阻塞等待任务结果]

    join()方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果

3.3.2 常用实现类

  • RecursiveAction

    用于递归执行但不需要返回结果的任务

  • RecursiveTask

    用于递归执行需要返回结果的任务。

  • CountedCompleter

    在任务完成执行后会触发执行一个自定义的钩子函数

3.4 ForkJoinWorkerThread

由ForkJoinWorkerThreadFactory工厂创建的工作线程,每个线程与ForkJoinPool.WorkQueue队列深度绑定

普通线程池的预热

//核心线程的预热
ExecutorService executors = Executors.newFixedThreadPool(10);
((ThreadPoolExecutor)executors).prestartAllCoreThreads();

4、Fork/Join原理及使用场景

4.1 核心使用场景(递归计算返回结果)

使用场景:大量CPU计算场景。

案例:

public class ForkJoinTest {
    final static ForkJoinPool.ForkJoinWorkerThreadFactory factory = new ForkJoinPool.ForkJoinWorkerThreadFactory() {
        @Override
        public ForkJoinWorkerThread newThread(ForkJoinPool pool) {
            final ForkJoinWorkerThread worker = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
            worker.setName("My-ForkJoin-thread-"+ worker.getPoolIndex());
            return worker;
        }
    };
    static ExecutorService executorService = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws Exception {
        int arr[] = Utils.buildRandomIntArray(100000000);

        //为避免cpu缓存,先计算forkJoin分治算法
        testForkJoinSub(8,arr);
        //单线程耗时
        testForkJoinSub(arr);
        //并行流算法
        testParallelSum(arr);
    }

    /**
     * forkJoin算法
     *
     * @param parallelism
     * @param arr
     * @throws Exception
     */
    public static void testForkJoinSub(int parallelism,int arr[]) throws Exception {
        //Runtime.getRuntime().availableProcessors()  forkJoin默认并行数
        ForkJoinPool forkJoinPool = new ForkJoinPool(parallelism,factory,null,false);
        Instant now = Instant.now();
        RecursiveTask task = new CalcTask(arr,0,arr.length);

        //提交任务
        ForkJoinTask<Long> submit = forkJoinPool.submit(task);

        Long aLong = submit.get();

        System.out.println("forkJoin sum="+aLong);
        System.out.println("forkJoin 耗时:"+ Duration.between(now,Instant.now()).toMillis());
    }

    /**
     * 单线程计算结果
     * @param arr
     */
    public static void testForkJoinSub(int arr[]){
        Long sum = 0L;
        Instant now = Instant.now();
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        System.out.println("单线程 sum="+sum);
        System.out.println("单线程 耗时:"+ Duration.between(now,Instant.now()).toMillis());
    }

    /**
     * stream流
     * @param arr
     */
    public static void testParallelSum(int arr[]){
        Instant now = Instant.now();
        Long sum = IntStream.of(arr).asLongStream().sum();
        System.out.println("stream流 sum="+sum);
        System.out.println("stream流 耗时:"+ Duration.between(now,Instant.now()).toMillis());
    }
}

单线程、forkJoin、并行流计算的结果及耗时对比:

执行的结果
#print result
forkJoin sum=49951294336
forkJoin 耗时:48
单线程 sum=49951294336
单线程 耗时:493
stream流 sum=49951294336
stream流 耗时:72

Process finished with exit code 0
public class CalcTask extends RecursiveTask<Long> {
    // 任务拆分最小阈值
    static final int SEQUENTIAL_THRESHOLD = 10000000;

    int low;
    int high;
    int[] array;

    CalcTask(int[] arr, int lo, int hi) {
        array = arr;
        low = lo;
        high = hi;
    }

    @Override
    protected Long compute() {

        //当任务拆分到小于等于阀值时开始求和
        if (high - low <= SEQUENTIAL_THRESHOLD) {

            long sum = 0;
            for (int i = low; i < high; ++i) {
                sum += array[i];
            }
            return sum;
        } else {  // 任务过大继续拆分
            int mid = low + (high - low) / 2;
            CalcTask left = new CalcTask(array, low, mid);
            CalcTask right = new CalcTask(array, mid, high);
            // 提交任务
            left.fork();
            right.fork();
            //获取任务的执行结果,将阻塞当前线程直到对应的子任务完成运行并返回结果
            long rightAns = right.join();
            long leftAns = left.join();
            return leftAns + rightAns;
        }
    }
}
4.2 任务窃取

每个workQueue为了方便窃取任务,都是双端队列。
在这里插入图片描述

4.3 内外部提交任务流程图

在这里插入图片描述

5、思考

5.1 并流行的坑

ForkJoinTask在执行fork方法时可见,外部提交的任务由ForkJoinPool.common.externalPush(this)来执行

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;
}

在这里插入图片描述
在这里插入图片描述
因此,如果forkjoin出现阻塞任务时,极有可能出现在开头中描述的因饥饿或阻塞的死锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旧梦昂志

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值