如何优雅的使用java线程池

简介:

在java开发中我们对“池”的概念并不陌生,常见的有数据库连接池、线程池、对象池、常量池等等,其作用基本上就是避免频繁的创建和回收,造成资源浪费,线程池也不例外。

创建线程池的好处:

  1. 降低创建线程和销毁线程的性能开销。
  2. 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行。
  3. 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题。

JDK 自带四种线程池分析

1 newFixedThreadPool

FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。另外 keepAliveTime 为 0,指的是超出核心线程数量以外的空闲线程存活时间。而这里选用的阻塞队列是LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}

2 newCachedThreadPool

CachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程;并且没有核心线程,非核心线程数无上限,但是每个线程空闲的时间只有 60 秒,超过后就会被回收。

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

3 newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

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

4 newScheduledThreadPool

ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor,并另外提供一些调度方法以支持定时和周期任务。DelayedWorkQueue是一个最小堆,最顶点的值最小,即堆中某个节点的值总是不小于其父节点的值使用优先级队列。DelayedWorkQueue,保证添加到队列中的任务,会按照任务的延时时间进行排序,延时时间少的任务首先被获取。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

合理创建线程池:

阿里java开发手册规定如下:
在这里插入图片描述
我们先来分析ThreadPoolExecutor参数最全的构造方法:

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

1 corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到阻塞队列当中。如果代码中没有显示设置allowCoreThreadTimeOut:核心线程超时时间,核心线程不会销毁。
2 maximumPoolSize:该线程池最大线程数。
3 keepAliveTime:表示非核心线程没有任务执行时最多保持多久时间会终止。
4 unit:参数keepAliveTime的时间单位(DAYS、HOURS、MINUTES、SECONDS 等)。
5 workQueue:阻塞队列,用来存储等待执行的任务。BlockingQueue实现类:
(1) ArrayBlockingQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
(2)DelayQueue:是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的Delayed 元素。
(3)LinkedBlockingQueue:内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。LinkedBlockingQueue 内部以 FIFO(先进先出)的顺序对元素进行存储。队列中的头元素在所有元素之中是放入时间最久的那个,而尾元素则是最短的那个。
(4)PriorityBlockingQueue: 一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。
(5)SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
(6)BlockingDeque 类是一个双端队列,在不能够插入元素时,它将阻塞住试图插入元素的线程;在不能够抽取元素时,它将阻塞住试图抽取的线程。deque(双端队列) 是 “Double Ended Queue” 的缩写。因此,双端队列是一个你可以从任意一端插入或者抽取元素的队列。
(7)LinkedBlockingDeque:是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全。此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。

6 threadFactory:线程工厂,主要用来创建线程。
7 handler:拒绝处理任务的策略。有如下四种拒绝策略:
(1)AbortPolicy:直接抛出异常,默认策略;
(2)CallerRunsPolicy:用调用者所在的线程来执行任务;
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
(4)DiscardPolicy:直接丢弃任务;

注意:
1 在创建线程池时如果代码没有显示设置executorService.allowCoreThreadTimeOut(true) 则线程池中核心线程不会被回收,如果设置则核心线程空闲时间超过keepAliveTime时间也被回收,这样线程池没有任务执行最终会自动销毁。
2 默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。在实 际中如果需要 线程池创建之 后立即创建线 程,可以通过 以下两个方法 办到:prestartCoreThread():初始化一个核心线程; prestartAllCoreThreads():初始化所有核心线程。

线程池任务执行流程:
在这里插入图片描述

基于源码分析线程池执行流程:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
      
        int c = ctl.get();
        //1.当前池中线程比核心数少,新建一个线程执行任务
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //2.核心池已满,但阻塞队列未满,添加到阻塞队列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
            if (! isRunning(recheck) && remove(command))
            //如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
                reject(command);
                //如果之前的线程已被销毁完,新建一个线程
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //3.核心池已满,队列已满,试着创建一个新线程
        else if (!addWorker(command, false))
            //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
            reject(command);
    }

execute源码说明:
1 ctl 的作用在线程池中,ctl 贯穿在线程池的整个生命周期中ctl:private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));它是一个原子类,主要作用是用来保存线程数量和线程池的状态。我们来分析一下这段代码,其实比较有意思,他用到了位运算一个 int 数值是 32 个 bit 位,这里采用高 3 位来保存运行状态,低 29 位来保存线程数量。
2 addWorker:如果工作线程数小于核心线程数的话,会调用 addWorker,顾名思义,其实就是要创建一个工作线程。

线程池容量的动态调整 :

ThreadPoolExecutor 提供了动态调整线程池容量大小的方法 : setCorePoolSize() 和setMaximumPoolSize()。setCorePoolSize:设置核心池大小; setMaximumPoolSize:设置线程池最大能创建的线程数目大小。

合理配置线程池大小:

如果想合理设置线程池的线程数量需要考虑两个问题:

  1. 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型。
  2. 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系。
    如果是 CPU 密集型,那线程池的最大线程数可以配置为 cpu 核心数+1;如果是 IO 密集型,线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/线程 CPU 时间 )* CPU 数目。

线程池的关闭

ThreadPoolExecutor 提供了两个方法 ,用于线程池的关闭 ,分 别 是 shutdown() 和shutdownNow(),其中:shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务 shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值