浅谈 “线程池”

454 阅读8分钟

往期推荐

一、为什么要有线程池

在实际使用中,线程是很占用系统资源的,如果对线程管理不完善的话很容易导致系统问题。因此,在大多数并发框架中都会使用线程池来管理线程,使用线程池管理线程主要有如下好处:

1. 降低资源消耗
线程池可以充分复用线程,让线程持续不断地处理任务,有效避免频繁创建和销毁线程造成的资源消耗。  
2. 提高响应速度
当任务到达时,任务可以不需要等待线程的创建就立即执行,没有线程创建和销毁时的消耗。 
3. 提高线程的可管理性
可以避免无限创建线程引起`OutOfMemoryError`,如果无限制地创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行同意分配、调优和监控。

同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程

  • 线程池的核心工作流程:

12.png

二、创建线程池的七种方法

13.png

  • (1)Executors.newFixedThreadPool:

创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;使用了LinkedBlockingQueue队列,该队列其实是有界队列。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • (2)Executors.newCachedThreadPool:

创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;它可容纳的最大线程数量为Integer.MAX_VALUE,所以比较容易出现内存溢出。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • (3)Executors.newSingleThreadExecutor:

创建单个线程数的线程池,它可以保证先进先出的执行顺序;它只会用一个唯一的工作线程来执行任务,如果这个唯一线程因为异常结束,那么会有一个新的工作线程来替代它,它必须保证前一项任务执行后,才能执行后一项任务。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • (4)Executors.newScheduledThreadPool:

创建一个可以执行延迟任务的线程池;可安排在给定延迟后运行命令或者定期地执行。使用了DelayedWorkQueue队列,该队列具有延时的功能

public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
            }
        }
  • (5)Executors.newSingleThreadScheduledExecutor:

创建一个单线程的可以执行延迟任务的线程池;

public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1, threadFactory));
}
  • (6)Executors.newWorkStealingPool:

创建一个抢占式执行的线程池(JDK1.8新增的新线程池)会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。

public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}
  • (7)ThreadPoolExecutor:

最原始的创建线程池的方式,它包含了 7 个参数可供设置,后面会详细讲。

三、ThreadPoolExecutor详解

1、ThreadPoolExecutor的七个参数

在阿里巴巴的开发规约中明确指出,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor去创建,因为这样的处理方式可以让我们更加明确线程池的运行规则,规避资源耗尽的风险。

public ThreadPoolExecutor(int corePoolSize, //核心线程池的大小
                          int maximumPoolSize, //最大的核心线程池的数量
                          long keepAliveTime,	//没人调用的存活时间,即最大空闲时间
                          TimeUnit unit,	//超时单位
                          BlockingQueue<Runnable> workQueue,  //阻塞队列
                          ThreadFactory threadFactory, // 线程工厂,用来创建线程的
                          RejectedExecutionHandler handler  //拒绝策略

1. int corePoolSize:线程池内的核心线程的数量。如果属性allowCoreThreadTimeOut为false ,那么核心线程即使处于空闲状态也不会被回收。如果属性为true,那么核心线程也可以被销毁。

2. int maximumPoolSize:线程池可容纳的最大线程数量。

3.long keepAliveTime:空闲线程等待新任务的最大等待时间。

4.TimeUnit unit:keepAliveTime的时间单位。

5.BlockingQueue<Runnable> workQueue:在任务被执行之前用于保存任务的队列。

6.ThreadFactory threadFactory:线程池创建新线程时使用的线程工厂。

7.RejectedExecutionHandler handler:线程池的拒绝执行处理程序。(4种拒绝策略)

2、ThreadPoolExecutor的四种拒绝策略

  • AbortPolicy:当线程池饱和后,抛出RejectedExecutionException异常。
/**
 * 拒绝策略1 AbortPolicy ->超出最大承载量抛java.util.concurrent.RejectedExecutionException
 */
public class demo1 {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                0,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try{
            //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
            for (int i = 1; i <=9; i++) {
                //超过maximumPoolSize+capacity的大小,会被拒绝并抛出java.util.concurrent.RejectedExecutionException异常
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
  • CallerRunsPolicy:当线程池饱和后,由提交任务的线程执行任务。
/**
 * 拒绝策略2 CallerRunsPolicy ->超出最大承载量时返回主线程进行处理
 */
public static void main(String[] args) {
    //自定义线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            2,
            5,
            0,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(3),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());

    try{
        //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
        for (int i = 1; i <=9; i++) {

            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName());
            });
        }
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        threadPool.shutdown();
    }
}
  • DiscardPolicy:默默地丢弃任务。
/**
 * 拒绝策略3 DiscardPolicy ->超出最大承载量时丢弃任务,不抛出异常
 */
public class demo3 {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                0,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());

        try{
            //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
            for (int i = 1; i <=9; i++) {

                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
  • DiscardOldestPolicy:丢弃阻塞队列中最早入队的任务。
/**
 * 拒绝策略4 DiscardOldestPolicy ->超出最大承载量时丢弃队列里最久未处理的请求,然后重试,不会抛出异常
 */
public class demo4 {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                0,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());

        try{
            //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
            for (int i = 1; i <=9; i++) {

                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

3、ThreadPoolExecutor的工作流程

  1. 判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。

  2. 判断任务队列是否已满,没满则将新提交的任务添加在工作队列。

  3. 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和(拒绝)策略。

12.png 工作流程如图:

14.png

  • 当提交任务到线程池执行时,线程池的状态变化如下: (1)当线程池内的线程数量少于corePoolSize时,线程池创建新线程用于执行提交到线程池的任务。

(2)当线程池内的线程数量大于等于corePoolSize时,任务将进入阻塞队列,等待核心线程执行。如果阻塞队列没有达到最大容量,那么任务入队成功,否则执行步骤 3

(3)当线程池内的线程数量大于等于corePoolSize并且阻塞队列已满时,线程池内就会创建新的非核心线程用于执行任务。

(4)如果线程池内的数量小于maximumPoolSize,那么创建非核心线程成功,否则执行步骤 4

(5)当线程池中的线程数量等于maximumPoolSize时,线程池拒绝执行处理程序。

4、ThreadPoolExecutor的五种状态

  1. RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务。

  2. SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。

  3. STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态。

  4. TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。

  5. TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。进入TERMINATED的条件如下:

    • 线程池不是RUNNING状态;

    • 线程池状态不是TIDYING状态或TERMINATED状态;

    • 如果线程池状态是SHUTDOWN并且workerQueue为空;

    • workerCount为0;

    • 设置TIDYING状态成功。

15.png

5、关闭ThreadPoolExecutor

  • 关闭线程池,可以通过shutdownshutdownNow两个方法。
    • shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;

    • shutdown只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

6、合理配置ThreadPoolExecutor队列大小

  1. CPU密集型任务

    • 尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
  2. IO密集型任务

    • 可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
  3. 混合型任务

    • 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

参考 & 鸣谢

《Java并发编程的艺术》

zhuanlan.zhihu.com/p/337544553