Java 线程池工作机制详解

196 阅读7分钟

为什么要使用线程池

  1. 减低资源消耗,通过重复利用已创建的线程降低线程创建和销毁带来的性能消耗
  2. 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能够立即执行
  3. 提高线程的可管理型。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性

创建线程池

先看一下 ThreadPoolExecutor 的构造方法(此处只列举参数完整的一个构造方法,其他构造方法在此基础上做了简化)

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • corePoolSize:线程池的核心线程数,当提交一根任务时,线程池会创建一个新线程执行任务,直到当前线程数等于 corePoolSize。如果当前线程数等于 corePoolSize,继续提交的任务会被保存在阻塞队列中(第 5 个参数)

  • maximumPoolSize:线程池最大线程数,如果当前阻塞队列满了,再提交任务时,会创建新的线程执行任务,直到线程数达到 maximumPoolSize,则会使用拒绝策略(第 7 个参数)

  • keepAliveTime:线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于 corePoolSize 时才有用

  • unit:存活时间的单位,分钟、秒、毫秒等

  • workQueue:阻塞队列,当线程数超过 corePoolSize 数后,会将后续任务放入此阻塞队列中等待。一般此阻塞队列使用有界队列。

  • threadFactory:创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。

  • handler:拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,则采用此拒绝策略处理该任务,JDK 内置了 4 种拒绝策略:

    • DisCardOldestPolicy:直接丢弃最老的任务(队列最前面的任务)
    • AbortPolicy:直接抛出异常(线程池默认拒绝策略)
    • CallerRunsPolicy:让调用者线程自己执行任务
    • DiscardPolicy:新提交的任务直接丢弃

提交任务

  • execute(Runnable runnable):没有返回值
  • submit(Callable callable):用于提交需要返回值的任务,线程池会返回一个 future 类型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可以通过 future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候可能任务没有执行完。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
        5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>());

threadPoolExecutor.execute(new Runnable() {
    @Override
    public void run() {
        // do something
        System.out.println("任务1");
    }
});

Future<?> future = threadPoolExecutor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        // do something
        Thread.sleep(3000);
        return "返回值";
    }
});

try {
    // 此处获取返回值会阻塞当前线程,直到此任务执行完成
    Object o = future.get();
    System.out.println(o);
} catch (ExecutionException | InterruptedException e) {
    e.printStackTrace();
}

关闭线程池

  • shutdown 关闭线程池,中断未执行任务的线程
  • shutdownNow 将所有线程尝试中断(包括正在执行任务的线程)

调用以上任意一个方法,isShutdown 方法就会返回 true,但这是并不代表所有线程都以停止,当所有任务都已关闭后,才表示线程池关闭成功,这是调用 isTerminaed 方法会返回 true。

注意:上面说的中断只是发起中断信号,具体能不能中断要看线程内部的处理

线程池的工作机制

  1. 如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(执行这一步骤需要获取全局锁)
  2. 如果运行的线程数量大于等于 corePoolSize,则将新任务加入 BlockingQueue 进行等待
  3. 如果 BlockingQueue 队列已满,则创建新线程处理任务
  4. 如果创建新线程将使当前运行的线程总数超过 maximumPoolSize,则采用拒绝策略处理该任务

合理配置线程池

配置线程池一般根据如下因素进行配置:

  1. 任务性质:CPU 密集型任务、IO 密集型任务和混合型任务
  2. 任务的优先级:高、中、低
  3. 任务的执行时间:长、中、短
  4. 任务的依赖性:是否依赖其他系统资源
  • CPU 密集型:最大线程数建议不要超过机器的 CPU 核心数+1,使用Runtime.getRuntime().availableProcessors();可获取当前设备核心数
  • IO 密集型:最大线程数一般设置为 CPU 核心数*2
  • 混合型:如果两种任务执行时间相差不大,建议拆分成两个线程池,分别处理不同性质任务。如果任务执行时间相差较大,可以按照执行时间较长的任务类型来配置
  • 优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理,他可以让优先级高的任务先执行
  • 执行时间不同的任务可以交给不同规模的县城市来处理,或者可以使用优先级队列,让执行时间短的任务先执行
  • 建议使用有界队列,有界队列能增加系统稳定性和预警能力,可以根据需要把容量设置大一点

JDK 内置线程池

JDK 默认提供了四种配置好的线程池,可以通过 Exectors 创建

  1. Executors.newCachedThreadPool()
    可缓存线程池,这是一种只有非核心线程,并且最大线程数为 Integer.MAX_VALUE。由于 Integer.MAX_VALUE 数很大,所以此线程池容量相当于任意大。当前线程池中的线程都处于工作状态,则新建一个线程执行任务,否则使用空闲线程执行任务。超过 60 秒的空闲线程就会被回收。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                        new SynchronousQueue());
    }
    
  2. Executors.newFixedThreadPool(int nThreads)
    定长线程池,这是一个固定长度并且只有核心线程的线程池。空闲线程并不会被回收,当所有线程都在工作时,新任务则进行等待。由于核心线程不会被回收,这意味着此线程池能够更快的响应提交的任务。此线程池的任务队列是无界的。

    public static ExecutorService newFixedThreadPool(int nThreads){
        return new ThreadPoolExecutor(nThreads, nThreads,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>());
    }
    
  3. Executors.newScheduledThreadPool()
    定长线程池,这是一个核心线程数固定,非核心线程数无限大(近似)的线程池,当非核心线程空闲时会被立即回收,适合处理定时任务或周期任务。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int var1) {
        super(var1, Integer.MAX_VALUE,
            0L, TimeUnit.NANOSECONDS,
            new ScheduledThreadPoolExecutor.DelayedWorkQueue());
    }
    
  4. Executors.newSingleThreadExecutor()
    单线程线程池,这是一个只有一个核心线程,没有非核心线程的线程池,由于只有一个线程,所以可以保证所有任务顺序执行,这样就不需要处理下线程安全问题。此线程池的任务队列是无界的。

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

总结

不建议使用 JDK 内置的四种线程池,具体原因参考阿里巴巴开发 Java 开发手册

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。