1 线程池的使用场景
在程序中经常需要用到多线程处理一些任务,这时候不建议单纯使用Thread或者实现Runnable接口的方式来创建线程,因为创建和销毁线程、和线程的上下文切换是需要耗费资源的,另一方面不加限制的创建线程可能导致系统资源耗尽。所以需要使用多线程的场景建议使用线程池,使用线程池会带来以下好处:
- 通过重用已有线程降低系统资源的消耗,降低创建和销毁线程的消耗
- 提高系统的响应速度,不需要等待线程创建完成,直接使用已存在线程
- 方便线程并发数的管控。
- 提供更强大的功能,延时定时线程池
2 常用的四种线程池
2.1 newFixedThreadPool
创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
Executors.newFixedThreadPool(8);
// 构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 构造函数
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
使用的构造方式为new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()),设置了corePoolSize=maxPoolSize,keepAliveTime=0(此时该参数没作用),无界队列,任务可以无限放入,当请求过多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致占用过多内存或直接导致OOM异常
2.2 newSingleThreadExector
创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
Executors.newSingleThreadExecutor();
// 构造函数
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 构造函数
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
使用的构造方式为new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), var0),基本同newFixedThreadPool,但是将线程数设置为了1,单线程,弊端和newFixedThreadPool一致
2.3 newCachedThreadPool
用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
Executors.newCachedThreadPool();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
使用的构造方式为new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue()),corePoolSize=0,maxPoolSize为很大的数,同步移交队列,也就是说不维护常驻线程(核心线程),每次来请求直接创建新线程来处理任务,也不使用队列缓冲,会自动回收多余线程,由于将maxPoolSize设置成Integer.MAX_VALUE,当请求很多时就可能创建过多的线程,导致资源耗尽OOM
2.4 newScheduledThreadPool
适用于执行延时或者周期性任务。
Executors.newScheduledThreadPool(111);
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
使用的构造方式为new ThreadPoolExecutor(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new ScheduledThreadPoolExecutor.DelayedWorkQueue()),支持定时周期性执行,注意一下使用的是延迟队列,弊端同newCachedThreadPool一致
3 线程池的核心参数
从第二节可以看到四种常用的线程池,最后都是调用ThreadPoolExecutor进行创建,构造函数共有六个参数。
3.1 corePoolSize
corePoolSize(线程池基本大小):核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务。
当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
3.2 maximumPoolSize
maximumPoolSize(线程池最大大小):最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)。另外,对于无界队列,可忽略该参数。
3.3 keepAliveTime
keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
3.4 workQueue
用于传输和保存等待执行任务的阻塞队列。用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中
- SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
- LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
- ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
3.5 threadFactory
用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号),默认使用Executors.defaultThreadFactory()。
3.6 handler
线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。
- AbortPolicy:中断抛出异常
- DiscardPolicy:默默丢弃任务,不进行任何通知
- DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
- CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)
4 线程池其他相关概念
Callable和Runable
向线程池中提交的任务有两种,Callable和Runable,二者的区别如下:
- Callable允许有返回值,Runable没有返回值。
- Callable允许抛出异常,Runable不允许抛出异常。
三种任务提交
1、 Future submit(Callable task)
关心返回值结果
2、 void execute(Runnable command)
不关心返回值结果
3、 Future<?> submit(Runnable task)
不关心返回值结果,虽然返回Future,但是其get方法总是返回null
Future和FutureTask
Future就是对于具体的Runnable或者Callable任务的执⾏结果进⾏取消、查询是否完成、获取结果。必要时可以通过get⽅法获取执⾏结果,该⽅法会阻塞直到任务返回结果。总的来说Future提供了三种功能:
- 判断任务是否完成,isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,它仅仅表示一种状态说后面任务不会再执行了;isCancelled():判断任务是否被取消;
- 取消任务,cancel(boolean mayInterruptIfRunning),boolean类型入参表示如果任务正在运行中是否强制中断;
- 获取返回结果,get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取消任务;
Future是⼀个接⼝,是⽆法⽣成⼀个实例的,所以⼜有了FutureTask。FutureTask实现了RunnableFuture接⼝,RunnableFuture接⼝⼜实现了Runnable接⼝和Future接⼝。所以FutureTask既可以被当做Runnable来执⾏,也可以被当做Future来获取Callable的返回结果。
5 线程池的工作过程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
6 线程池实现原理
线程池有三个优点:线程复用;控制最大并发数;管理线程
线程复用
理解线程的复用首先要了解线程的生命周期,线程的生命周期包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。
Thread通过new来新建一个线程,这个过程是是初始化一些线程信息,如线程名,id,线程所属group等,可以认为只是个普通的对象。调用Thread的start()后Java虚拟机会为其创建方法调用栈和程序计数器,同时将hasBeenStarted为true,之后调用start方法就会有异常。
处于这个状态只是表示该线程可以运行了,JVM里线程调度器调度后才能开始运行。当线程获取cpu后,run()方法会被调用,之后根据CPU的调度在就绪——运行——阻塞间切换,直到run()方法结束或其他方式停止线程,进入dead状态。保持线程处于存活状态(就绪,运行,阻塞)才能实现线程复用。
在ThreadPoolExecutor主要Worker类来控制线程的复用,Worker类简化后的代码如下:
private final class Worker implements Runnable {
final Thread thread;
Runnable firstTask;
Worker(Runnable firstTask) {
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Runnable task = w.firstTask;
w.firstTask = null;
while (task != null || (task = getTask()) != null){
task.run();
}
}
Worker是一个Runnable,同时拥有一个thread,这个thread就是要开启的线程,在新建Worker对象时同时新建一个Thread对象,同时将Worker自己作为参数传入Thread,这样当Thread的start()方法调用时,运行的实际上是Worker的run()方法,接着到runWorker()中,有个while循环,一直从getTask()里得到Runnable对象,顺序执行。getTask()又是怎么得到Runnable对象的呢?
private Runnable getTask() {
if(一些特殊情况) {
return null;
}
Runnable r = workQueue.take();
return r;
}
这个workQueue就是初始化ThreadPoolExecutor时存放任务的BlockingQueue队列,这个队列里的存放的都是将要执行的Runnable任务。因为BlockingQueue是个阻塞队列,BlockingQueue.take()得到如果是空,则进入等待状态直到BlockingQueue有新的对象被加入时唤醒阻塞的线程。所以一般情况Thread的run()方法就不会结束,而是不断执行从workQueue里的Runnable任务,这就达到了线程复用的原理了。
控制最大并发数
execute简化后的代码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 当前线程数 < corePoolSize
if (workerCountOf(c) < corePoolSize) {
// 直接启动新的线程。
if (addWorker(command, true))
return;
c = ctl.get();
}
// 活动线程数 >= corePoolSize
// runState为RUNNING && 队列未满
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次检验是否为RUNNING状态
// 非RUNNING状态 则从workQueue中移除任务并拒绝
if (!isRunning(recheck) && remove(command))
reject(command);// 采用线程池指定的策略拒绝任务
// 两种情况:
// 1.非RUNNING状态拒绝新的任务
// 2.队列满了启动新的线程失败(workCount > maximumPoolSize)
} else if (!addWorker(command, false))
reject(command);
}
addWorker简化后的代码:
private boolean addWorker(Runnable firstTask, boolean core) {
int wc = workerCountOf(c);
if (wc >= (core ? corePoolSize : maximumPoolSize)) {
return false;
}
w = new Worker(firstTask);
final Thread t = w.thread;
t.start();
}
根据代码再来看上面提到的线程池工作过程中的添加任务的情况:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。
addWorker如果成功创建新的线程成功,通过start()开启新线程,firstTask将作为这个Worker里的run()中执行的第一个任务。后面会从队列中获取待执行的任务。虽然每个Worker的任务是串行处理,因为创建了多个Worker,因为共用一个workQueue,所以就会并行处理了。
管理线程
ThreadPoolExecutor有个ctl的AtomicInteger变量。通过这一个变量保存了两个内容:
- 所有线程的数量
- 每个线程所处的状态
其中低29位存线程数,高3位存runState,通过位运算来得到不同的值。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//得到线程的状态
private static int runStateOf(int c) {
return c & ~CAPACITY;
}
//得到Worker的的数量
private static int workerCountOf(int c) {
return c & CAPACITY;
}
// 判断线程是否在运行
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
这里主要通过shutdown和shutdownNow()来分析线程池的关闭过程。首先线程池有五种状态来控制任务添加与执行。主要介绍以下三种:
- RUNNING状态:线程池正常运行,可以接受新的任务并处理队列中的任务;
- SHUTDOWN状态:不再接受新的任务,但是会执行队列中的任务;
- STOP状态:不再接受新任务,不处理队列中的任务
shutdown这个方法会将runState置为SHUTDOWN,会终止所有空闲的线程,而仍在工作的线程不受影响,所以队列中的任务人会被执行。shutdownNow方法将runState置为STOP。和shutdown方法的区别,这个方法会终止所有的线程,所以队列中的任务也不会被执行了。
7 线程池的使用建议
线程不是越多越好,原因主要有以下三点:
- 创建线程和销毁线程都是需要时间的,如果创建时间+销毁时间>执行任务时间就很不划算。
- 创建后的线程是需要内存去存放的,创建的线程对应一个Thread对象,对象是会占用JVM的堆内存的,根据jvm规范,一个线程默认最大栈大小为1M,这个栈空间也是需要从系统内存中分配的,所以线程越多,需要的内存就越多。
- 创建线程,操作系统是需要频繁进行线程上下文切换的,所以线程创建太多,是会影响性能的。
一般根据任务内容把任务分为两种,计算密集型和IO密集型。
计算密集型
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
参考:
https://www.jianshu.com/p/7726c70cdc40
https://www.jb51.net/article/221712.htm
https://blog.csdn.net/qq_40428665/article/details/121651421
https://blog.csdn.net/fanrenxiang/article/details/79855992
https://blog.csdn.net/aiengelangte/article/details/80397952