ExecutorService-ForkJoinPool(四)

170 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

3. ForkJoinPool

分叉合并池

分叉:将一个大的任务拆分成了多个小的任务交给不同的线程来执行

合并:将拆分出去的小任务计算结果进行汇总整合

特点:分叉合并池能够有效的提高CPU的利用率,甚至能到到CPU利用率的100%,所以实际开发中,分叉合并一般是放在凌晨执行

本质:分叉合并本质上就是将大任务拆分成小任务,分配给多个线程,让多个线程落在不同的CPU核上来,提高CPU的利用率

效率

  • 如果数据量偏大,分叉合并的效率要远远高于循环
  • 如果数据量偏小,循环的效率反而高于分叉合并

(work-stealing)工作窃取策略

分叉合并在向每个核上分配任务的时候,考虑任务的均衡问题:核上任务多的少分,任务少的多分。由于CPU核在执行任务的时候,有快有慢(有的任务简单执行的就快,有的任务复杂执行的就慢),所以先执行完所有任务的CPU核并不会闲下来,而是会随机的扫描一个其他的核,然后从被扫描的核的任务队列尾端"偷取"一个任务回来执行,这种策略称之为work-stealing(工作窃取)策略

使用

  • 功能类实现RecursiveTask,重写compute()方法
  • 创建分叉合并池
  • submit()提交任务,并接收返回值对象
public class ForkJoinPoolDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        long begin = System.currentTimeMillis();
        // 求1-100000000000L
        
        // long sum = 0;
        // for (long i = 1; i <= 100000000000L; i++) {
        //     sum += i;
        // }
        // System.out.println(sum);

        ForkJoinPool pool = new ForkJoinPool();
        Future<Long> f = pool.submit(new Sum(1, 100000000000L));
        pool.shutdown();
        System.out.println(f.get());

        long end = System.currentTimeMillis();
        System.out.println(end - begin);
    }
}

class Sum extends RecursiveTask<Long> {

    private long start;
    private long end;

    public Sum(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        // 如果start-end范围内数字比较少,那么可以将这个范围内的数字求和
        if (end - start <= 10000) {
            long sum = 0;
            for (long i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        } else {
            // 如果start-end数字依然比较多,那么就继续拆分
            long mid = (start + end) / 2;
            Sum left = new Sum(start, mid);
            Sum right = new Sum(mid + 1, end);
            // 拆分成了2个子任务
            left.fork();
            right.fork();
            // 需要将2个子任务的结果来进行汇总才是这个范围内的最终结果
            return left.join() + right.join();
        }

    }
}

4. 使用线程池过程的一些问题

4.1 父子线程之间数据的共享问题

4.1.1 每次都new Thread去执行线程

父子线程之间的数据共享通过InheritableThreadLocal类实现,在子线程初始化(new)的时候调用init方法设置的,在子线程中调用InheritableThreadLocal即可和父线程共享数据

/**
 * 用于父子线程间共享 LoginUserOrg 对象
 *
 * @date 2021/07/01
 */
public class LoginUserOrgHolder {

    private LoginUserOrgHolder() {
    }

    private static final ThreadLocal<LoginUserOrg> THREAD_LOCAL = new InheritableThreadLocal<>();

    public static LoginUserOrg get() {
        return THREAD_LOCAL.get();
    }

    public static void set(LoginUserOrg obj) {
        THREAD_LOCAL.set(obj);
    }

    public static void remove() {
        THREAD_LOCAL.remove();
    }
}

在子线程中使用 LoginUserOrgHolder 类获取共享线程数据

4.1.2 使用线程池

使用线程池时,会提前在线程池中初始化一些线程,就没办法使用InheritableThreadLocal 类,在子线程init时设置了

所以使用alibaba提供的一个jar包,来解决使用线程池时,数据共享的问题

关键类:TransmittableThreadLocal

import com.alibaba.ttl.TransmittableThreadLocal;
/**
 * 用于父子线程间共享 LoginUserOrg 对象
 *
 * @date 2021/07/01
 */
public final class LoginUserOrgHolder {
    private LoginUserOrgHolder() {
    }

    private static final ThreadLocal<LoginUserOrg> THREAD_LOCAL = new TransmittableThreadLocal<>();
    
    public static LoginUserOrg get() {
        return THREAD_LOCAL.get();
    }

    public static void set(LoginUserOrg obj) {
        THREAD_LOCAL.set(obj);
    }

    public static void remove() {
        THREAD_LOCAL.remove();
    }

}