线程池
线程池是一种多线程处理形式,处理过程中将任务添加队列,然后在创建线程后自动启动这些任务,每个线程都使用默认的堆栈大小,以默认的优先级运行,并处在多线程单元中。如果某个线程在托管代码中空闲,则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后辅助线程的数目永远不会超过最大值。超过最大值的线程可以排队,但要等到其他线程完成后才能启动。java里面的线程池的顶级接口是Executor,Executor并不是一个线程池,而只是一个执行线程的工具,而真正的线程池是ExecutorService。使用线程池的优点:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能执行
- 提高线程的可管理性,线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控
线程池的缺点:
1.适用于生存周期较短的的任务,不适用于又长又大的任务,因为它很容易导致线程不足
2.非核心线程的创建时机
核心线程的数量是 corePoolSize 的值,非核心线程的数量是 maxinumPoolSize - corePoolSize
非核心线程创建的触发时机是:当前线程池中核心线程已满,且没有空闲的线程,还有任务等待队列已满,满足上面的所有条件,才会去创建线程去执行新提交的任务
如果线程池中的线程数量达到 maxinumPoolSize 的值,此时还有任务进来,就会执行拒绝策略,抛弃任务或者其他。如果拒绝策略是抛弃任务的话,有一种场景,就会造成大量任务的丢弃,就是瞬时冲高的情况下
3.不能对于线程池中任务设置优先级
4.有任务导致线程长时间阻塞,线程池具有最大线程数,因此大量阻塞的线程池线程可能会阻止任务启动
ThreadPoolExecutor继承了AbstractExecutorService,AbstractExecutorService是一个抽象类,它实现了ExecutorService接口,而ExecutorService又是继承了Executor接口,其构造函数参数的含义:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
//corePoolSize 线程池中核心线程的数量
//maximumPoolSize 线程池中最大线程数量
//keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长
//unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等
//workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的
//threadFactory 为线程池提供创建新线程的功能,这个我们一般使用默认即可
//handler 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException
maximumPoolSize(最大线程数) = corePoolSize(核心线程数) + noCorePoolSize(非核心线程数);当一个任务通过execute(Runnable)方法欲添加到线程池时:
(1)当currentSize<corePoolSize时,直接启动一个核心线程并执行任务。
(2)当currentSize>=corePoolSize、并且workQueue未满时,添加进来的任务会被安排到workQueue中等待执行。
(3)当workQueue已满,但是currentSize<maximumPoolSize时,会立即开启一个非核心线程来执行任务。
(4)当currentSize>=corePoolSize、workQueue已满、并且currentSize>maximumPoolSize时,调用handler默认抛出RejectExecutionExpection异常。
队列类型 | 特性及使用场景 |
---|---|
ArrayBlockingQueue | 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序 |
LinkedBlockingQueue | 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,无界队列,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列 |
SynchronousQueue | 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列 |
PriorityBlockingQueue | 优先级队列,线程池会优先选取优先级高的任务执行,队列中的元素必须实现Comparable接口 |
在使用包含无界队列LinkedBlockingQueue的时候,要注意是否会导致队列不断增长,导致内存溢出。使用SynchronousQueue的时候要注意是否会导致线程数不断增长。
线程池参数设置
默认值
corePoolSize=1
queueCapacity=Integer.MAX_VALUE
maxPoolSize=Integer.MAX_VALUE
keepAliveTime=60s
allowCoreThreadTimeout=false
rejectedExecutionHandler=AbortPolicy()
参数设置需要根据几个值来决定:
tasks
:每秒的任务数,假设为500~1000
taskcost
:每个任务花费时间,假设为0.1s
responsetime
:系统允许容忍的最大响应时间,假设为1s
做几个计算:
【1】corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~ 1000)*0.1 = 50~100 个线程,corePoolSize设置应该大于50。如果80%的每秒任务数小于800,那么corePoolSize设置为80即可。
【2】queueCapacity = (coreSizePool/taskcost)* responsetime
计算可得 queueCapacity = 80/0.1*1 = 800。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行。切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
【3】maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
计算可得 maxPoolSize = (1000-80)/10 = 92。(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数。
【4】rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理。
【5】keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
线程池的种类以及各自的作用
Executor里面提供了一些静态工厂,生成一些常用的线程池。
newCachedThreadPool
【作用】:使用SynchronousQueue队列,创建一个可缓存线程池。可根据需要创建新线程,可重用以前构造的线程。当一个线程超过60s没有任务执行,被回收。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
newFixedThreadPool
【作用】:使用LinkedBlockingQueue创建一个固定大小线程池。创建一个指定工作线程数量的线程池。只有核心线程并且这些核心线程不会被回收,能够更快速的响应外界请求。如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到线程池队列中。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
newSingleThreadExecutor
【作用】:创建一个单线程化的线程池,以无界队列方式来运行该线程。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。类似于newFixedThreadPool(1)。
newScheduledThreadPool
【作用】: 创建一个定长线程池,主要用于执行定时任务和具有固定周期的重复任务。核心线程数量是固定的,而非核心线程数是没有限制,非核心线程闲置时会被立即回收。
线程池满了,往线程池里提交任务会发生什么样的情况?
具体分几种情况:
- 如果使用的LinkedBlockingQueue,会继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue是无界队列,可以无限存放任务
- 如果使用的是ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
如何创建线程池
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。Executors 返回线程池对象的弊端如下:
1、FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM
2、CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM
==方式一:通过构造方法实现 ==
new ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
==方式二:通过Executor 框架的工具类Executors来实现 ==,可以创建三种类型的ThreadPoolExecutor:
- FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务
- SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务
- CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用
增长策略
线程池线程的增长策略,和 3 个参数有关系:corePoolSize:核心线程数;maximumPoolSize:最大线程数;workQueue:等待任务队列。
线程池中线程的增长策略。默认情况下,初始时线程池是空的,当有新任务来了时,线程池开始通过线程工厂创建线程来处理任务。新的任务会不断的触发线程池中线程的创建,直到线程数量达到核心线程数(corePoolSize),接下来会停止线程的创建,而是将这个新任务放入任务等待队列(workQueue)。新任务不断进入任务等待队列,当该队列满了时,开始重新创建线程处理任务,直到线程池中线程的数量,到达 maximumPoolSize 配置的数量。到这一步时,线程池的线程数达到最大值,并且没有空闲的线程,任务队列也存满了任务,这时如果还有新的任务进来,就会触发线程池的拒绝策略(handler),如果没有配置拒绝策略就会抛出 RejectedExecutionException 异常。
拒绝策略
在使用线程池并且使用有界队列的时候,如果队列满了,任务添加到线程池的时候就会有问题,针对这些问题java线程池提供了以下几种策略:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
AbortPolicy
该策略是线程池的默认策略。使用该策略时,如果线程池队列满了,丢掉这个任务并且抛出RejectedExecutionException异常。
源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
//不做任何处理,直接抛出异常
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
DiscardPolicy
如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
//就是一个空的方法
}
DiscardOldestPolicy
如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。
源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
//移除队头元素
e.getQueue().poll();
//再尝试入队
e.execute(r);
}
}
CallerRunsPolicy
使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。
源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
//直接执行run方法
r.run();
}
}
自定义
如果以上策略都不符合业务场景,则可以自定义一个拒绝策略。需要实现RejectedExecutionHandler接口,并且实现rejectedExecution方法。具体的逻辑就在rejectedExecution方法中定义。
例如:自定义拒绝策略MyRejectPolicy,逻辑是打印处理被拒绝的任务内容。
public class MyRejectPolicy implements RejectedExecutionHandler{
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//Sender是我的Runnable类,里面有message字段
if (r instanceof Sender) {
Sender sender = (Sender) r;
//直接打印
System.out.println(sender.getMessage());
}
}
}
手撕线程池
主要包含四个变量:核心线程数coreSize
、最大线程数maxSize
、阻塞队列BlockingQueue
、拒绝策略RejectPolicy
。
/**
* 自动动手写一个线程池
*/
public class MyThreadPoolExecutor implements Executor{
/**
* 线程池的名称
*/
private String name;
/**
* 线程序列号
*/
private AtomicInteger sequence = new AtomicInteger(0);
/**
* 核心线程数
*/
private int coreSize;
/**
* 最大线程数
*/
private int maxSize;
/**
* 任务队列
*/
private BlockingQueue<Runnable> taskQueue;
/**
* 拒绝策略
*/
private RejectPolicy rejectPolicy;
/**
* 当前正在运行的线程数
* 需要修改时线程间立即感知,所以使用AtomicInteger
* 或者也可以使用volatile并结合Unsafe做CAS操作
*/
private AtomicInteger runningCount = new AtomicInteger(0);
public MyThreadPoolExecutor(String name, int coreSize,int maxSize, BlockingQueue<Runnable> taskQueue, RejectPolice rejectPolicy){
this.name = name;
this.coreSize = coreSize;
this.maxSize = maxSize;
this.taskQueue = taskQueue;
this.rejectPolicy = rejectPolicy;
}
/*
首先,如果运行的线程数小于核心线程数,直接创建一个新的核心线程来运行新的任务。
其次,如果运行的线程数达到了核心线程数,则把新任务入队列。
然后,如果队列也满了,则创建新的非核心线程来运行新的任务。
最后,如果非核心线程数也达到最大了,那就执行拒绝策略。
*/
@Override
public void execute(Runnable task){
// 正在运行的线程数
int count = runningCount.get();
// 如果正在运行的线程数小于核心线程数,直接加一个线程
if(count < coreSize){
// 注意,这里不一定添加成功,addWorker()方法里面还要判断一次是不是确 实小
if (addWorker(task, true)) {
return;
}
// 如果添加核心线程失败,进入下面的逻辑
}
// 如果达到了核心线程数,先尝试让任务入队
// 这里之所以使用offer(),是因为如果队列满了offer()会立即返回false
if (taskQueue.offer(task)) {
// do nothing,为了逻辑清晰这里留个空if
} else{
// 如果入队失败,说明队列满了,那就添加一个非核心线程
if(!addWorker(task, false)) {
// 如果添加非核心线程失败了,那就执行拒绝策略
rejectPolicy.reject(task, this);
}
}
}
/*
首先,创建线程的依据是正在运行的线程数量有没有达到核心线程数或者最大线程数,所以我们还需要一个变量runningCount用来记录正在运行的线程数。
其次,这个变量runningCount需要在并发环境下加加减减,所以这里需要使用到Unsafe的CAS指令来控制其值的修改,用了CAS就要给这个变量加上volatile修饰,为了方便我们这里直接使用AtomicInteger来作为这个变量的类型。
然后,因为是并发环境中,所以需要判断runningCount < coreSize(或maxSize)(条件一)的同时修改runningCount CAS加一(条件二)成功了才表示可以增加一个线程,如果条件一失败则表示不能再增加线程了直接返回false,如果条件二失败则表示其它线程先修改了runningCount的值,则重试。
最后,创建一个线程并运行新任务,且不断从队列中拿任务来运行。
*/
private boolean addWorker(Runnable newTask, boolean core) {
// 自旋判断是不是真的可以创建一个线程
for(;;){
// 正在运行的线程数
int count = runningCount.get();
// 核心线程还是非核心线程
int max = core ? coreSize : maxSize;
// 不满足创建线程的条件,直接返回false
if(count >= max){
return false;
}
// 修改runningCount成功,可以创建线程
if(runningCount.compareAndSet(count, count + 1)) {
// 线程的名字
String threadName = (core ?"core_" : "")+ name +sequence.incrementAndGet();
// 创建线程并启动
new Thread(() -> {System.out.println("thread name: "+Thread.currentThread().getName());
// 运行的任务
Runnable task = newTask;
// 不断从任务队列中取任务执行,如果取出来的任务为null,则跳出循环,线程也就结束了
while (task != null || (task = getTask()) != null) {
try {
// 执行任务
task.run();
}finally{
// 任务执行完成,置为空
task = null ;
}
}
},threadName.start();
break;
}
}
return true;
}
/*
从队列中取任务应该使用take()方法,这个方法会一直阻塞直至取到任务或者中断,
如果中断了就返回null,这样当前线程也就可以安静地结束了,
另外还要注意中断了记得把runningCount减一。
*/
private Runnable getTask(){
try {
// take()方法会一直阻塞直到取到任务为止
return taskQueue.take();
}catch(InterruptedException e){
// 线程中断了,返回null可以结束当前线程
// 当前线程都要结束了,理应要把runningCount的数量减一
runningCount.decrementAndGet();
return null;
}
}
}
DiscardRejectPolicy丢弃策略实现类
/**
* 丢弃当前任务
*/
public class DiscardRejectPolicy implements RejectPolicy{ @Override
public void reject(Runnable task, MyThreadPoolExecutor myThreadPoolExecutor){
// do nothing
System.out.println("discard one task");
}
}
测试类
/*
(1)先连续创建了5个核心线程,并执行了新任务
(2)后面的15个任务进了队列
(3)队列满了,又连续创建了5个线程,并执行了新任务
(4)后面的任务就没得执行了,全部走了丢弃策略
(5)所以真正执行成功的任务应该是 5 + 15 + 5 = 25 条任务
*/
public class MyThreadPoolExecutorTest{
public static void main(String[] args){
Executor threadPool = new MyThreadPoolExecutor("test",5,10, new ArrayBlockingQueue<>(15),new DiscardRejectPolicy());
AtomicInteger num = new AtomicInteger(0);
for(int i = 0; i < 100; i++){
threadPool.execute(()->{
try{
Thread.sleep(1000);
System.out.println("running: "+ System.currentTimeMillis() + ": "+ num.incrementAndGet());
} catch (InterruptedException e){
e.printStackTrace();
}
});
}
}
}