天天看点

java-线程池详解

作者:编程少年

1. 创建方式

1.1 newFixedThreadPool

// 创建固定线程数是5的线程池·
ExecutorService executorService= Executors.newFixedThreadPool(5);           

1.1.1 介绍

FixedThreadPool 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现:

1.1.1.2 执行过程

java-线程池详解

1.1.1.3 缺点

  • 使用无界队列LinkedBlockingQueue,容易照成OOM
  • 线程池的线程数时固定的,容易照成CPU空闲。

1.1.2 newSingleThreadExecutor

// 创建线程数是1的线程池 
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();           

1.1.2.1 介绍

SingleThreadExecutor 是只有一个线程的线程池。下面看看SingleThreadExecutor 的实现:

1.1.2.2 执行过程

java-线程池详解

1.1.2.3 缺点

  • 使用无界队列LinkedBlockingQueue,容易照成OOM。
  • 照成CPU空闲浪费。

1.1.3 newCachedThreadPool

// 创建无固定数量的线程池
ExecutorService executorService= Executors.newCachedThreadPool();           

1.1.3.1 介绍

CachedThreadPool 是一个会根据需要创建新线程的线程池。下面通过源码来看看 CachedThreadPool 的实现:

1.1.3.2 执行过程

java-线程池详解
  1. 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2。
  2. 当初始 maximumPool 为空或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成。

1.1.3.3 缺点

  • 允许创建的线程数量Integer.MAX_VALUE,会照成OOM。

1.1.4 ThreadPoolExecutor

int coreSize = 10;
int maximumPoolSize = 20;
int keepAliveTime = 5;
TimeUnit unit = TimeUnit.MINUTES;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue(50);
ThreadFactory factory = new ThreadFactoryBuilder().setNameFormat("xxx").build();
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
ExecutorService executorService = new ThreadPoolExecutor(coreSize,maximumPoolSize, keepAliveTime, unit, workQueue, factory, handler);           

通过构造方法的方式创建线程池。

1.1.4.1 介绍

ThreadPoolExecutor是线程池实现的核心类,上面其他三种创建方式,最终都是调用它的构造方法进行创建的,通过构造方法,我们可以自由控制线程池的各种参数。

1.1.4.2 执行过程

java-线程池详解

1.1.4.3 优点

  1. 灵活性:ThreadPoolExecutor构造方法提供了丰富的参数选项,可以根据实际需求对线程池进行灵活配置。可以指定核心线程数、最大线程数、线程空闲时间、任务队列类型等参数,以满足不同的并发场景。
  2. 并发控制:线程池可以有效地限制并发线程的数量,防止系统资源过度消耗。通过设置合适的线程池大小,可以平衡系统的并发负载,防止系统因过多的并发请求导致性能下降或崩溃。

1.2 如何优雅的关闭线程池

private static void shutdown(ExecutorService executorService) {
    // 第一步:使新任务无法提交
    executorService.shutdown();
    try {
        // 第二步:等待未完成任务结束
        if(!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
             // 第三步:取消当前执行的任务
            executorService.shutdownNow();
            // 第四步:等待任务取消的响应
            if(!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("Thread pool did not terminate");
            }
        }
    } catch(InterruptedException ie) {
        // 第五步:出现异常后,重新取消当前执行的任务
        executorService.shutdownNow();
        Thread.currentThread().interrupt(); // 设置本线程中断状态
    }
}           

1.3 配置线程池

1.3.1 重要参数

1、corePoolSize:核心线程数
    * 核心线程会一直存活,及时没有任务需要执行
    * 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
    * 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

2、queueCapacity:任务队列容量(阻塞队列)
    * 当核心线程数达到最大时,新任务会放在队列中排队等待执行

3、maxPoolSize:最大线程数
    * 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
    * 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

4、 keepAliveTime:线程空闲时间
    * 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
    * 如果allowCoreThreadTimeout=true,则会直到线程数量=0

5、allowCoreThreadTimeout:允许核心线程超时

6、rejectedExecutionHandler:任务拒绝处理器
    * 两种情况会拒绝处理任务:
        - 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务
        - 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用             shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
    * 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,           会抛出异常
    * ThreadPoolExecutor类有几个内部实现类来处理这类情况:
        - AbortPolicy 丢弃任务,抛运行时异常
        - CallerRunsPolicy 执行任务
        - DiscardPolicy 忽视,什么都不会发生
        - DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
    * 实现RejectedExecutionHandler接口,可自定义处理器           

1.3.2 饱和策略

  1. AbortPolicy :在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。
  2. CallerRunsPolicy:在 ThreadPoolExecutor.CallerRunsPolicy 中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  3. DiscardPolicy:在 ThreadPoolExecutor.DiscardPolicy 中,不能执行的任务将被删除。
  4. DiscardOldestPolicy:在 ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。

1.4 线程池执行过程

java-线程池详解
  1. 当正在运行的线程小于corePoolSize,线程池会创建新的线程。
  2. 当大于corePoolSize而任务队列未满时,就会将整个任务塞入队列。
  3. 当大于corePoolSize而且任务队列满时,并且小于maximumPoolSize时,就会创建新的线程执行任务。
  4. 当大于maximumPoolSize时,会根据拒绝策略处理线程。

1.5 参数如何设置

  • CPU密集型:尽量使用较小的线程池,一般 Cpu核心数+1
  • IO密集型:可以使用较大的线程池,一般 CPU核心数 * 2

1.6 线程池监控

通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用:

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  • largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不+ getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。通过继承线程池并重写线程池的 beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。