Java并发编程实战(进阶篇 - 下)

紧接着上一篇我们继续分析在并发编程中所用到的一些并发工具。

5.Fork/Join 框架

5.1 什么是 Fork/Join 框架

Fork/Join 框架是 Java7 提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架
Fork :就是把一个大任务切分成若干子任务并行的执行;Join:就是合并这些子任务的执行结果。

设计思路图:
在这里插入图片描述

5.2 工作窃取算法

工作窃取(work-stealing)算法:某个线程从其他队列中窃取任务来执行

在 Fork/Join 框架中为了减少线程间的竞争,把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。但是,有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时他们会访问同一个队列,所以减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

双线程工作窃取的工作流程:
在这里插入图片描述

工作窃取算法的优点:充分利用线程进行并行计算,减少了线程间的竞争。
工作窃取算法的缺点:在某些情况下还是存在竞争,比如双端队列只有一个任务时;算法会消耗更多的系统资源,可能会创建多个线程和双端队列。

5.3 使用 Fork/Join 框架

使用 Fork/Join 框架来完成任务大概分为两个步骤:

  1. 分割任务;
  2. 执行任务并合并结果。

Fork/Join 框架使用两个类完成以上的两件事情。

  1. ForkJoinTask:我们要使用 Fork/Join 框架,必须首先创建一个 ForkJoin任务。它提供在任务中执行 fork() 和 join() 操作机制。通常情况下我们不需要直接继承 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类。

    RecursiveAction:用于没有返回结果的任务。
    RecursiveTask:用于有返回结果的任务。

  2. ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行。
    任务分割出的子任务会添加到当前工作线程多维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

光说不练,下面我们设计一个利用 Fork/Join 框架实现的排序算法:

public class SortTask extends RecursiveTask<int[]> {
   
    private static final int THRESHOLD = 10;
    private int start;
    private int end;
    private int[] arrays;

    public SortTask(int start, int end, int[] arrays) {
   
        this.start = start;
        this.end = end;
        this.arrays = arrays;
    }

    @Override
    protected int[] compute() {
   
        boolean canCompute = (end - start + 1) <= THRESHOLD;
        if (canCompute) {
   
            Arrays.sort(arrays, start, end);
        } else {
   
            // 如果任务大于阈值,就分裂成两个子任务计算
            int mid = (start + end) / 2;
            SortTask leftTask = new SortTask(start, mid, arrays);
            SortTask rightTask = new SortTask(mid+1, end, arrays);

            // 执行子任务
            leftTask.fork();
            rightTask.fork();

            // 等待子任务执行完成,得到结果
            int[] leftArrays = leftTask.join();
            int[] rightArrays = rightTask.join();
            
            // 合并子任务
            Arrays.sort(leftArrays, start, end);
        }
        return arrays;
    }

    public static void main(String[] args) {
   
        ForkJoinPool forkJoinPool = new ForkJoinPool();

        SortTask sortTask = new SortTask(0, 20, new int[]{
   20, 1, 3, 5, 4, 3, 6, 7, 1, 11, 19, 18, 13, 14, 15, 21, 3, 44, 3, 7});
        ForkJoinTask<int[]> result = forkJoinPool.submit(sortTask);
        try {
   
            int[] ints = result.get();
            for (int i = 0; i < ints.length; i++) {
   
                System.out.print(ints[i] + " ");
            }
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        } catch (ExecutionException e) {
   
            e.printStackTrace();
        }
    }
}
5.4 Fork/Join 框架的实现原理

Fork/Join 框架中双端队列的相关操作与 ConcurrentHashMap 的 table 数组较为相似,都是采用 CAS 的方式来处理每个桶的并发,这里推荐两篇博客可以结合着理解 Fork/Join 框架的实现原理:
Fork/Join框架(1) 原理
Fork/Join框架(2)实现

这两篇分别从原理和源码实现来分析了Fork/Join 框架。

三、并发工具类

在JDK并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier、Semaphore和Phaser 工具类提供了一种并发流程控制的手段,Exchanger 工具类则提供了在线程间交换数据的一种手段。

1.等待多线程完成的 CountDownLatch

CountDownLatch 允许一个或多个线程等待其他线程完成操作。

1.1 CountDownLatch 简介

CountDownLatch是一个辅助同步器类,用来作计数使用,它的作用类似于倒数计数器,先设定一个计数初始值,当计数降到0时,将会触发一些操作。

初始计数值在构造CountDownLatch对象时传入,每调用一次 countDown() 方法,计数值就会减1。

线程可以调用CountDownLatch的await方法进入阻塞,当计数值降到0时,所有之前调用await阻塞的线程都会释放。

注意:计数器必须大于等于0,计数器等于0时,调用await方法时不会阻塞当前线程。CountDownLatch 不可能重新初始化或者修改 CountDownLatch 对象的内部计数器的值。

1.2 CountDownLatch 应用场景

作为一个开关/入口
需求一:假设上学某天老师迟到(老师拿的钥匙),那么来的学生就只能在教室门外等老师,老师来了学生才能进入教室,这里老师就相当于一个教室的入口。

public class Holder {
   
    private static final CountDownLatch startLatch = new CountDownLatch(1);
    private static final int N = 5;

    public static void main(String[] args) {
   
        for (int i = 0; i < N; i++) {
   
            new Student(startLatch).start();
        }
        // 主线程调用countDown方法,相当于老师来了
        startLatch.countDown();
        System.out.println("teach coming,open the door");
    }

}
class Student extends Thread {
   
    private final CountDownLatch waitLatch;

    public Student(CountDownLatch waitLatch) {
   
        this.waitLatch = waitLatch;
    }

    @Override
    public void run() {
   
        try {
   
        	// 如果count不为0,说明老师没来
            if (waitLatch.getCount() != 0) {
   
                System.out.println("student need wait");
                waitLatch.await();
            } else {
   
                System.out.println("the teacher coming, student can enter");
            }
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
    }
}

程序运行可能会出现打印语句顺序的问题,这是因为没有同步出现的原因,因为这不是重点,就没有给代码加锁。

作为一个事件的完成信号
需求二:使某个线程在其它N个线程完成某项操作之前一直等待。

public class Holder {
   
    private static final int N = 5;
    private static final CountDownLatch Latch = new CountDownLatch(N);

    public static void main(String[] args) throws InterruptedException {
   
        for (int i = 0; i < N; i++) {
   
            new Worker(Latch).start();
        }
        Latch.await();
        System.out.println("work finish");
    }

}
class Worker extends Thread {
   
    private final CountDownLatch latch;

    public Worker(CountDownLatch latch) {
   
        this.latch = latch;
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值