为什么阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建?

3,856 阅读5分钟

阿里巴巴的java手册里面说到,线程池的创建不要用jdk提供的那些简单方法,容易买坑,要用ThreadPoolExecutor构造函数, 来明确各个参数的意义,这样可以避免出错,代码可读性也很好,然而在实际开发中,我发现还是有很多同学对ThreadPoolExecutor 这些参数似懂非懂。今天就来捋一下,捋顺了,对线程池也就了解。

先来看一段简单的demo:

package com.wuyue.test;

import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.*;


public class PoolTest {

    //核心线程池的大小
    public static final int CORE_POOL_SIZE = 10;
    //最大线程池的大小
    public static final int MAXIMUM_POOL_SIZE = 20;
    //超过核心线程池的大小哪些线程 最多可以存活多久
    public static final long KEEP_ALIVE_TIME = 3000;

    //创建线程的线程工厂,这个建议一定要自己重写一下,因为可以增加很多关键信息,方便出问题的时候dump或者看日志能定位到问题
    public static ThreadFactory threadFactory;

    //阻塞队列 --本篇文章的重点
    public static BlockingQueue<Runnable> workQueue;

    //拒绝策略--本篇文章的重点
    public static RejectedExecutionHandler rejectedExecutionHandler;

    public static void main(String args[]) {
        threadFactory = new MyThreadFactory("wuyuetestfactory");
        workQueue = new ArrayBlockingQueue(100);
        rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();


        /**
         * 本质上来说 线程池的执行逻辑其实真的很简单:
         * 如果当前线程池的线程个数小于CORE_POOL_SIZE 那么有任务到来的时候 就直接创建一个线程 执行这个任务
         * 如果当前线程池的线程个数已经到了CORE_POOL_SIZE这个极限,那么新来的任务就会被放到workQueue中
         * 如果workQueue里面的任务已满,且MAXIMUM_POOL_SIZE这个值大于CORE_POOL_SIZE,那么此时线程池会继续创建线程执行任务
         * 如果workQueue满了,且线程池的线程数量也已经达到了MAXIMUM_POOL_SIZE 那么就会把任务丢给rejectedExecutionHandler 来处理
         * 当线程池中的线程超过了CORE_POOL_SIZE的哪些线程 如果空闲时间到了KEEP_ALIVE_TIME 那么就会自动销毁
         * 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 
         */
        ExecutorService executorService = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, workQueue, threadFactory, rejectedExecutionHandler);

        for (int i = 0; i < Long.MAX_VALUE; i++) {
            Task task = new Task();
            executorService.execute(task);
        }
//        System.out.println(myThreadFactory.getStas());

    }
}

// 自定义一个线程工厂
class MyThreadFactory implements ThreadFactory {
    int counter = 0;
    String name;
    private List<String> stats;

    public MyThreadFactory(String name) {
        this.name = name;
        stats = new ArrayList<>();
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, name + "-Thread-" + counter);
        counter++;
        String logInfo = String.format("Created thread %d with name %s on%s\n", t.getId(), t.getName(), new Date());
//        stats.add(logInfo);
        System.out.println(logInfo);
        return t;
    }

    //这个方法的调用时机 就看你们具体业务上需求如何了,其实线程工厂真的很简单,主要就是根据你的环境
    //定制出你需要的信息 方便日后调试即可 不需要太纠结。
    public String getStas() {
        StringBuffer buffer = new StringBuffer();
        Iterator<String> it = stats.iterator();
        while (it.hasNext()) {
            buffer.append(it.next());
        }
        return buffer.toString();
    }

}

class Task implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

看完这个demo你应该对线程池有了基本了解了,同时对线程工厂的作用也有了一定基础,剩下我们首先来看看这个队列到底是啥意思

队列就分为两种,一种是有界队列,一种是无界队列。他俩最大的区别是:

无界队列可以一直往里面丢任务,而有界队列当发现到了队列大小极限以后就直接拒绝新任务的到来了。

这里面的坑就是 无界队列你无限往里面丢任务,如果任务执行的慢 有可能任务太多 就oom了。

比如把我们刚才的例子改成一个无界队列:

        workQueue = new LinkedBlockingDeque<>();

然后把线程执行时间改长一点,或者线程里面多放点东西让内存大一点,你就会发现很快就会报oom的异常了:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

所以,为了防止这种oom的情况出现,才有 有界队列这种说法,他的作用就是一旦发现有界队列里面的任务 已经到了极限,那么就开始把新来的任务丢弃掉,注意既然是丢弃,那么显然就有丢弃策略了,也就是我们的RejectedExecutionHandler

这个里面也有坑,我们来看看系统提供的几个丢弃策略

        //要启用拒绝策略的前提就是一定得是有界队列,你无界队列可以无限制丢 当然不用care 决绝策略了
        workQueue = new ArrayBlockingQueue(100);
        rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();

这个默认的AbortPolicy最坑,没有之一,因为他直接就是抛异常了!程序都退出了!

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.wuyue.test.Task@308db1 rejected from java.util.concurrent.ThreadPoolExecutor@1c170f0[Running, pool size = 20, active threads = 20, queued tasks = 100, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)

这个就好很多,虽然也是直接丢,但是不会打扰你主程序的运行,最多就是你不知道丢了哪些而已。。

 rejectedExecutionHandler = new ThreadPoolExecutor.DiscardPolicy();

这个其实和上面的差不多,只不过这个不是丢新来的,而是丢最老的。优先丢队头

        rejectedExecutionHandler = new ThreadPoolExecutor.DiscardOldestPolicy();

这个也是比较坑的,这个是丢弃的时候直接run了,android多数都在主线程里创建了线程, 你直接run 就等于在主线程做这个操作,很容易引发anr或者卡顿。这个也是大坑,慎用!

rejectedExecutionHandler = new ThreadPoolExecutor.CallerRunsPolicy();

那么最终只有自定义一个拒绝策略拉:

class MyReject implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        //实际生产中,我们最好把任务也赋值一下关键的log信息,方便 这些任务被抛弃以后存储在本地,等待时机
        //再重新拉出来继续执行,否则丢弃掉的任务也蛮可惜的,重要的信息不可以丢失
        //这里只是演示方便弄了个toString而已
        System.out.println("runnable 任务 被丢弃了" + r.toString());
    }
}

所以阿里爸爸还是很牛逼的,一个简单的线程池使用背后的知识点还是不少的,我们在阅读此类业界公认的好文档的时候, 一定要知其然并且知其所以然,否则日后稍微变一点的问题你就束手无策了。

那么回到标题的问题,为啥阿里巴巴不允许呢?有了上文的基础,你自己点进去看看就知道了

就这么几个方法,点进去看看就知道 这些东西其实有埋了不少的深坑。