为什么要使用线程池?
创建普通对象,仅仅是在 JVM 的堆里分配一块内存而已;而创建一个线程,却需要调用操作系统内核的 API,然后操作系统要为线程分配一系列的资源,这个成本就很高了,所以线程是一个重量级的对象,应该避免频繁创建和销毁。
而线程池其实就是一个容纳了多个线程的容器,其中的线程可以反复使用,无需反复创建线程而消耗过多资源。
线程池的设计:生产者-消费者模式
线程池和一般意义上的池化资源是不同的。一般意义上的池化资源,都是下面这样,当你需要资源的时候就调用 acquire() 方法来申请资源,用完之后就调用 release() 释放资源。但Java 提供的线程池里面压根就没有申请线程和释放线程的方法。
class XXXPool{
// 获取池化资源
XXX acquire() {}
// 释放池化资源
void release(XXX x){}
}
目前业界线程池的设计,普遍采用的都是生产者 - 消费者模式
。线程池的使用方是生产者,线程池本身是消费者。我们可以看看下面JDK线程池的创建和使用示例来理解线程池的生产者-消费者模式。
线程池的创建示例
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.ThreadFactory;
// 初始化示例
private static final ThreadPoolExecutor pool;
static {
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("po-detail-pool-%d").build();
pool = new ThreadPoolExecutor(
4,
8,
60L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(512),
threadFactory,
new ThreadPoolExecutor.AbortPolicy());
pool.allowCoreThreadTimeOut(true);
}
初始化参数含义解释:
- threadFactory:给出带业务语义的线程命名。
- corePoolSize:快速启动4个线程处理该业务,是足够的。
- maximumPoolSize:IO密集型业务,我的服务器是4C8G的,所以4*2=8。
- keepAliveTime:服务器资源紧张,让空闲的线程快速释放。
- workQueue:一个任务的执行时长在100~300ms,业务高峰期8个线程,按照10s超时(已经很高了)。10s钟,8个线程,可以处理10 * 1000ms / 200ms * 8 = 400个任务左右,往上再取一点,512已经很多了。
- handler:极端情况下,一些任务只能丢弃,保护服务端。
- pool.allowCoreThreadTimeOut(true):也是为了在可以的时候,让线程释放资源。
线程池的使用示例
// 使用execute方法或submit方法提交任务
public class TestClass {
public static void main(String[] args) {
//创建包含4个线程的线程池对象
ExecutorService threadPool=Executors.newFixedThreadPool(4);
//创建Runnable线程任务对象
TaskRunnable task=new TaskRunnable();
System.out.println("提交自定义线程任务");
//将任务提交线程池,并开始执行
threadPool.submit(task);
//关闭线程池
threadPool.shutdown();
}
}
class TaskRunnable implements Runnable{
public void run() {
System.out.println("自定义线程任务在执行");
System.out.println("自定义线程任务执行完毕");
}
}
JDK中的线程池相关类
- Executor接口:线程池的抽象接口,只包含一个execute方法。
- ExecutorService子接口:提供了有关终止线程池和Future返回值的一些方法。
- AbstractExecutorService抽象类:提供了ExecutorService的一些默认实现。
- ThreadPoolExecutor类:JDK提供的线程池的实现类。
- Executors类:线程池工厂类,提供了几种线程池的工厂方法。
线程池实现类ThreadPoolExecutor
构造方法
参数最多的一个构造方法共有7个参数,如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
几个重要参数解释
corePoolSize
- 默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,才会去创建一个线程来执行任务
- 当线程池中的线程数目达到corePoolSize后,就停止线程创建,转而会把任务放到任务队列当中等待。
- 调用prestartAllCoreThreads()或者prestartCoreThread()方法,可以在没有任务到来之前就预创建线程。
maxPoolSize
- 当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。
- 如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
keepAliveTime & unit
- 当线程空闲时间达到keepAliveTime,单位unit时,该线程会退出,直到线程数量等于corePoolSize。
workQueue
任务队列(或工作队列),是一个阻塞队列,用来存储等待执行的任务,一般来说,这里的阻塞队列有以下几种选择:
- ArrayBlockingQueue;
- LinkedBlockingQueue;
- SynchronousQueue;
- PriorityBlockingQueue
threadFactory
线程工厂,通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。
handler
拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收,可以通过 handler 这个参数来指定拒绝的策略。ThreadPoolExecutor 已经提供了以下 4 种策略:
- ThreadPoolExecutor.AbortPolicy:默认的拒绝策略。丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列等待最久的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。
allowCoreThreadTimeout
是否允许核心线程空闲退出,默认值为false。如果allowCoreThreadTimeout设置为true,则所有线程均会超时退出,直到线程数量为0。
常用方法
- void execute(Runnable command):提交任务给线程池执行,任务无返回值
带Future返回值的,可以通过Future.get方法获取任务的返回值,get方法会抛出异常。
- Future<?> submit(Runnable task):由于Runnable接口没有返回值,所以Future返回值执行get()方法返回值为null,作用只是等待,类似于join
- Future submit(Runnable task, T result):由于Runnable没有返回值,所以额外提供了一个参数,作为返回值。
- Future submit(Callable task):提交任务给线程池执行,能够返回执行结果
- void allowCoreThreadTimeOut(boolean value):是否允许核心线程超时,默认false。
- shutdown():关闭线程池,等待任务都执行完
- shutdownNow():关闭线程池,不等待任务执行完,并返回等待执行的任务列表。
- getTaskCount():线程池已执行和未执行的任务总数
- getCompletedTaskCount():已完成的任务数量
- getPoolSize():线程池当前的线程数量
- getActiveCount():当前线程池中正在执行任务的线程数量
线程池的执行流程
线程池最佳实践及注意事项
- 【强制】使用ThreadPoolExecutor的构造函数声明线程池,避免使用Executors类的 newFixedThreadPool和newCachedThreadPool。
- 【强制】 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。即threadFactory参数要构造好。
- 【建议】建议不同类别的业务用不同的线程池。
- 【建议】CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数)+1,比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。
- 【建议】I/O密集型任务(2N):这种任务应用起来,系统会用大部分的时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU来处理,这时就可以将CPU交出给其它线程使用。因此在I/O密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是2N。
- 【建议】workQueue不要使用无界队列,尽量使用有界队列。避免大量任务等待,造成OOM。支持有界的阻塞队列有ArrayBlockingQueue 和 LinkedBlockingQueue。
- 【建议】如果是资源紧张的应用,使用allowsCoreThreadTimeOut可以提高资源利用率。
- 【建议】虽然使用线程池有多种异常处理的方式,但在任务代码中,使用try-catch最通用,也能给不同任务的异常处理做精细化。
- 【建议】对于资源紧张的应用,如果担心线程池资源使用不当,可以利用ThreadPoolExecutor的API实现简单的监控,然后进行分析和优化。
- 【建议】线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此默认拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。
线程池工厂类Executors
虽然说不推荐生产环境使用Executors类来创建线程池,但是了解一下Executors是如何创建不同特性的线程池,还是有利于我们加深对线程池使用的理解。
4中不同类型线程池的构造方法
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue());
}
4中不同类型线程池的比较
线程池类型 | CachedThreadPool | FixedThreadPool | SingleThreadExecutor | ScheduledThreadPool |
---|---|---|---|---|
corePoolSize | 0 | nThread(接收参数) | 1 | nThread(接收参数) |
maximumPoolSize | Integer.MAX_VALUE | nThread(接收参数) | 1 | Integer.MAX_VALUE |
keepAliveTime | 60L | 0L | 0L | 0 |
unit | TimeUnit.SECONDS | TimeUnit.MILLSECONDS | TimeUnit.MILLSECONDS | TimeUnit.NANOSECONDS |
workQueue | SynchronousQueue | LinkedBlockingQueue(无界阻塞队列) | LinkedBlockingQueue(无界阻塞队列) | DelayedWorkQueue(一个按超时时间升序排序的队列) |
通俗解释 | 当有新任务到来,则插入到 SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程则创 建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。 | 核心线程是固定的,阻塞队列是无限大小。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。 | 创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时, 对于新任务会进入阻塞队列中(无界的阻塞队列) | 创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构 |
适用 | 执行很多短期异步的小程序,或者负载较轻的服务器。 | 执行长期的任务,性能好很多 | 一个任务一个任务执行的场景 | 周期性执行任务的场景 |
缺点 | 1、SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。所以SynchronousQueue没有起到队列缓冲的效果。2、允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。 | 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 | 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 |