1 Executors: Executor与ExecutorService

java 8 提供了Executors类,它位于java.util.concurrent包下

Executors提供了一系列预配置线程池,我们可以直接调用,避免了重复造轮子

而Executor和ExecutorService是两个接口,可以对线程池进行操作

1.1 Executor

Executor只有一个方法:execute(),只能用来执行Runnable对象,没有返回值。用的比较少

以下面代码为例:

Runnable runnable = () -> System.out.println("Executor");
        Executor executor = Executors.newSingleThreadExecutor();
        executor.execute(runnable);

1.2 ExecutorService

ExecutorService 接口包含大量的方法控制任务的进度,可以执行Runnable/Callable对象。
使用这个接口,我们可以提交任务,还可以使用Future/CompletableFuture获取返回值。

ExecutorService中主要的方法有:execute()、submit()、invokeAny()、invokeAll()和
shutdown()。

execute(): 与Executor.executor一样,只能用来执行Runnable对象,没有返回值

submit():可以执行Runnable和Callable对象,在执行Callable对象时返回Future对象

Callable<String> callable = () -> "callable";
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(callable);

invokeAny():将一组任务分配给 ExecutorService,使每个任务都运行,并返回成功执行的任务的结果(如果有成功执行)

Callable<String> callable1 = () -> "callable1";
        Callable<String> callable2 = () -> "callable2";
        Callable<String> callable3 = () -> "callable3";

        List<Callable<String>> callableList = new ArrayList<>();
        callableList.add(callable1);
        callableList.add(callable2);
        callableList.add(callable3);

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        String result = executorService.invokeAny(callableList);

invokeAll():将一组任务分配给 ExecutorService,使每个任务都运行,并返回全部的执行结果,返回类型为Future

Callable<String> callable1 = () -> "callable1";
        Callable<String> callable2 = () -> "callable2";
        Callable<String> callable3 = () -> "callable3";

        List<Callable<String>> callableList = new ArrayList<>();
        callableList.add(callable1);
        callableList.add(callable2);
        callableList.add(callable3);

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        List<Future<String>> futureList = executorService.invokeAll(callableList);
        futureList.forEach((s) -> {
            try {
                System.out.println(s.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });

shutdown():在通常情况下,当没有任务需要执行时,ExecutorService不会立刻关闭,它会等待新的任务。
我们需要使用 executorService.shutdown()进行停止。

Notice:shutdown不会立刻关闭ExecutorService,它会等到所有的线程运行完后关闭!
shutdownNow()方法可以尝试立即关闭ExecutorService,但是它不能保证所有的线程同时停止!

1.3 Executors提供的线程池

1 newCachedThreadPool :创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,否则新建线程。(线程最大并发数不可控制)

2 newFixedThreadPool:创建一个固定大小的线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3 newScheduledThreadPool : 创建一个定时线程池,支持定时及周期性任务执行。

4 newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

更多可见 Executors类创建四种常见线程池

Notice:这里不对Executors创建的线程池做过多介绍的原因是,依据《阿里巴巴JAVA开发手册》中的规定:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回的线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadPool : 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

2 ThreadPoolExecutor

ThreadPoolExecutor可以自定义线程池,其函数定义为:

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)

corePoolSize 参数是池中的核心线程数。当有新任务进来时,如果所有核心线程都忙且内部队列已满,
则允许池增长到maximumPoolSize。BlockingQueue是阻塞队列的实现方案。

keepAliveTime 参数是非核心线程(corePoolSize以外的,补充进来的线程)的存活时间。默认情况下,
ThreadPoolExecutor 只考虑删除非核心线程。如果要对核心线程也采取存活策略,我们可以使用
allowCoreThreadTimeOut(true) 方法。

下面为示例:

ExecutorService service = new ThreadPoolExecutor(5,10,100,TimeUnit.SECONDS,new LinkedBlockingQueue<>(10));
CompletableFuture<String> future = CompletableFuture.supplyAsync(()->"supplier",service);

在实际的开发中,推荐使用ThreadPoolExecutor创建线程池

3 ForkJoinPool

fork/join 在java 7 中被提出,它是一种处理思想,实际原理是分治法。

当一个任务过大时,先fork,将其分成若干个小任务利用多线程进行异步处理。然后join,将子任务的结果递归合成一个结果。
如果任务返回值为void,那么程序只是等待每个子任务都执行完毕。

为了提高效率,Fork/Join使用的线程池被称为 ForkJoinPool,它用来管理ForkJoinWorkerThread类型的工作线程。

Fork/Join最重要的部分是使每个线程的负载相对均衡,就是每个线程干的活尽量差不多,不要有偷懒的,也不要有太累的。
这里的关键是利用了“工作窃取算法”

【工作窃取算法】:ForkJoinPool中的每个线程都维护一个双端队列。默认情况下,工作线程从它自己的双端队列的头部获取任务。
当它为空时,线程从另一个繁忙线程的双端队列的尾部或全局入口队列中获取任务。
这种方法最大限度地减少了线程竞争任务的可能性,减少了线程去寻找“工作”的次数。

在JAVA 8 中,可以使用下面的方式来创建ForkJoinPool的实例对象。

ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();

如果要处理的任务没有返回值,就继承RecursiveAction类
如果要处理的任务有返回值,就继承RecursiveTask<V>类

3.1 RecursiveAction

下面以一个例子说明该类的使用方法:

这个简单的例子将传入的小写字母变成大写,目的是为了模拟工作线程协同处理的过程

public class TestRecursiveAction extends RecursiveAction {
    private String workload = "";
    private static final int THRESHOLD = 4;

    public TestRecursiveAction(String workload){
        this.workload = workload;
    }

    @Override
    protected void compute() {
        if(workload.length()>THRESHOLD){
            ForkJoinTask.invokeAll(createSubTasks());
        }else{
            processing(workload);
        }
    }

    private List<TestRecursiveAction> createSubTasks(){
        List<TestRecursiveAction> subTasks = new ArrayList<>();
        String partA = workload.substring(0,workload.length()/2);
        String partB = workload.substring(workload.length()/2,workload.length());

        subTasks.add(new TestRecursiveAction(partA));
        subTasks.add(new TestRecursiveAction(partB));

        return subTasks;
    }

    private void processing(String work){
        String result = work.toUpperCase();
        System.out.println(result + "被线程" + Thread.currentThread().getName() + "处理");
    }
}

使用Test类对其进行调用:

public class Test {
    public static void main(String[] args) {
        TestRecursiveAction testRecursiveAction = new TestRecursiveAction("abcdefghijkl");
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        forkJoinPool.invoke(testRecursiveAction);
    }
}

结果为:

JKL被线程ForkJoinPool.commonPool-worker-2处理
DEF被线程ForkJoinPool.commonPool-worker-2处理
GHI被线程ForkJoinPool.commonPool-worker-9处理
ABC被线程main处理

在这里使用invoke()提交任务到ForkJoinPool,还有以下方式可以提交任务:

1 使用submit()或者execute()

TestRecursiveAction testRecursiveAction = new TestRecursiveAction("abcdefghijkl");
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        forkJoinPool.execute(testRecursiveAction);
        testRecursiveAction.join();

2 使用invokeAll()

invokeAll()用于提交多个任务

3 使用fork()和join()

fork() 方法向池提交任务,但不会触发其执行。join()方法必须紧跟其后。

TestRecursiveAction testRecursiveAction = new TestRecursiveAction("abcdefghijkl");
       testRecursiveAction.fork();
       testRecursiveAction.join();

3.2 RecursiveTask

带返回值的Task与RecursiveAction类似,不过这里需要将返回的结果进行组合。

以下面的代码为例:

这个简单的例子将传入的arr数组分块相加

public class TestRecursiveTask extends RecursiveTask<Integer> {

    private int[] arr;
    private static final int THRESHOLD = 3;

    public TestRecursiveTask(int []arr){
        this.arr = arr;
    }
    @Override
    protected Integer compute() {
        if (arr.length > THRESHOLD){
            return ForkJoinTask.invokeAll(createSubTasks())
                    .stream()
                    .mapToInt(ForkJoinTask::join)
                    .sum();
        }else{
            return processing(arr);
        }
    }
    private List<TestRecursiveTask> createSubTasks(){
        List<TestRecursiveTask> dividedTasks = new ArrayList<>();
        int[] partA = Arrays.copyOfRange(arr,0,arr.length/2);
        int[] partB = Arrays.copyOfRange(arr,arr.length/2,arr.length);
        dividedTasks.add(new TestRecursiveTask(partA));
        dividedTasks.add(new TestRecursiveTask(partB));
        return dividedTasks;
    }

    private Integer processing(int[] arr){
        System.out.println(Thread.currentThread().getName() + "线程负责处理" + Arrays.toString(arr));
        return Arrays.stream(arr).sum();
    }
}

使用Test类对其进行调用:

public class Test {
    public static void main(String[] args) {

        int[] arr = {1,2,3,4,5,6,7,8,9};
        TestRecursiveTask testRecursiveTask = new TestRecursiveTask(arr);
        ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
        int result = forkJoinPool.invoke(testRecursiveTask);
        System.out.println("最终结果为 " + result);
    }
}

最后的结果是:

main线程负责处理[1, 2]
ForkJoinPool.commonPool-worker-2线程负责处理[3, 4]
ForkJoinPool.commonPool-worker-11线程负责处理[7, 8, 9]
ForkJoinPool.commonPool-worker-9线程负责处理[5, 6]
最终结果为 45

【总结】:fork/join 可以加速大型任务的处理速度,但是要遵守以下规则:

1 使用尽可能少的线程池——在大多数情况下,最好每个应用程序或系统使用一个线程池

2 使用默认的公共线程池CommonPool(),如果不需要特殊调优

3 使用合理的阈值将 ForkJoinTask 拆分为子任务

4 避免 ForkJoinTasks 中的任何阻塞