ThreadPool线程池api及工作原理简析

2,100 阅读4分钟

线程池也是第四冲多线程的创建方式,先来说一下线程池的优势

线程池做的工作主要是控制运行的线程数量

即在处理过程中将任务放入队列,然后在线程创建后自动启动这些任务,如果线程数超过了最大输了,超出数量的线程排队等候,等其他线程执行完毕再从队列中取出

  • 线程复用
  • 控制最大并发
  • 管理线程

值得一提的是,这里的队列用到了阻塞队列BlockingQueue,对于ThreadPool,我们可以理解为已经为我们创建了一堆线程,然后再让这些线程去做我们指定的事情。

Executors创建线程的三种方法

//一池5线程,类似一个银行有5个受理窗口
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);
//一池1线程,类似一个银行有1个受理窗口
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
//一池N线程,类似一个银行有N个受理窗口,可扩展,内存可能溢出
ExecutorService threadPool3 = Executors.newCachedThreadPool();

线程的操作类

        for (int i =1; i <=10; i++) {
            threadPool.execute(()->{
                try {
//                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName()+"办理业务");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    threadPool.shutdown();
                }

            });
        }

newFixedThreadPool

第一个是固定容量的线程池,这个是固定了大小的线程池,每次都是从这个线程池中取的线程。

接着我们加上一句线程睡眠一小会的代码Thread.sleep(500);

newSingleThreadExecutor

单例的线程池,一直是一个线程在受理业务

newCachedThreadPool


在上面的代码中,我们可以发现的是,对从线程池取的线程睡了0.4s,然而却可以发现创建出了跟多的线程,所以我们可以发现当请求过多的时候会创建线程。

以上是线程池的简单api。

基本原理

首先点进他们的构造方法看看

可以发现,返回的实际上只是一个ThreadPoolExecutor,只是利用构造器传入的不同的参数而已,而且我们也能发现底层是阻塞队列。

同时说明我们也可以通过ThreadPoolExecutor来创建线程池,Executors只是一个创建线程池的工具类,实际上返回的还是ThreadPoolExecutor。

ThreadPoolExecutor

接着继续查看ThreadPoolExecutor

  • 找了一张图,解释一下七大参数 然后了解一下每个参数的具体意义
  • maximumPool包含corePool,maximumPool表示最多能放的线程数,而corePool表示的就是线程的常驻数
  • corePool满了之后,多余的线程就在workQueue等着,就像候客区
  • 当阻塞队列满了之后,corePool就开始扩容到maximumPool,如果此时阻塞队列也满了,handle拒绝策略出来了,相当于银行门口立了块牌子,上面写着不办理后面的业务了
  • 当多出来的线程空闲下来的时候,即在corePool的基础上扩容的线程,在经过keepAliveTime的时间后就关闭了,重新恢复到corePool个受理窗口

然后简单总结一下线程池的工作流程

首先线程池接收到任务,先判断核心线程数是否满了,没有满继续接客,满了就放到阻塞队列,如果阻塞队列没满,这些任务放在阻塞队列,如果满了,就扩容线程数到最大线程数,如果最大线程数也满了,就是我们的拒绝策略。这就是线程池四大步骤。 接客、放入队列,扩容线程,拒绝策略!

  • 创建线程后,开始等待请求
  • 当调用了excute()方法添加一个任务的时候,线程池会做出判断
    • 如果正在运行的线程数小于corePoolSize,马上创建线程运行该任务
    • 如果正在运行的线程数大于或等于corePoolSize,那么将这个任务放入队列
    • 如果队列满了,正在运行的线程数小于maximumPoolSize,那么继续创建非核心线程立刻运行该任务
    • 如果队列满了,且正在运行的线程数大于或等于maximumPoolSize,那么线程池将会启动饱和拒绝策略
  • 当一个线程完成任务后,他会从队列中取下一个任务来执行
  • 当一个线程空闲下来且超过keepAliveTime时间时,线程会进行判断
    • 如果当前运行的线程数大于corePoolSize,线程会停掉
    • 所有线程池的所有任务完成后,他会收缩到corePoolSize大小

实际运用

  • 对于newSingleThreadExecutor(); 而言LinkedBlockQueue的长度是Integer.MAX_VALUE,
  • 对于newCachedThreadPool()而言,maximumPool的值竟然为Integer.MAX_VALUE!! 两者均会导致OOM异常

自定义线程池

//系统线程数
int tc = Runtime.getRuntime().availableProcessors();
System.out.println(Runtime.getRuntime().availableProcessors());

//自定义线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
        2,
        5,
        2L,
        TimeUnit.SECONDS,
        new LinkedBlockingDeque<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

threadPool 是我们自定义的线程池,连接过上面的参数的应该都知道,该线程池最大支持的并发量就应该是maximumPool+Queue的大小,即5+3=8,而超过了大小之后就会报错:java.util.concurrent.RejectedExecutionException 拒绝执行异常

以上策略均实现RejectedExecutionHandler接口,具体策略看业务来使用,另外maximumPoolSize的设置一般为CPU核数+1