目录
线程池概述
在web开发中,服务器会为每一个请求分配一个线程来处理,如果每次请求都要创建一个线程的话,实现起来虽然简单,但是存在一个问题:如果开发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此会大大降低系统的效率,很有可能出现服务器在创建和销毁线程上花费的时间和消耗的系统资源比实际处理的用户请求的时间和资源还多。
于是,线程池应运而生,线程池满足了一个线程执行完一个任务不会被销毁,而是可以继续执行其他任务的目的,他为线程生命周期的开销和资源不足问题提供了解决方案,通过重用线程,线程创建的开销被分摊到了多个任务上,那么,什么时候适合使用线程池呢?
- 单个任务处理时间较短
- 需要处理的任务量较大
线程池的优点
- 减少线程创建、销毁的开销,提高了性能
线程池核心属性
线程池的核心属性是ctl(如下图),根据ctl拿到线程池的状态以及工作线程的个数
ctl是一个integer类型的数据,一般有32个比特位(可以通过Integer.SIZE获取):
- 高3位表示线程池状态
- 低29位表示工作线程的数量(32-3)
private static final int COUNT_BITS = Integer.SIZE - 3;、
// CAPACITY表示线程池当前工作线程能记录的工作线程的最大个数
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
线程池状态
以下5个状态只有RUNNING表示线程池没问题,可以正常接收任务处理
- RUNNING = -1 << COUNT_BITS
-
- 111
- SHUTDOWN = 0 << COUNT_BITS;
-
- 000
- STOP = 1 << COUNT_BITS;
-
- 不会接收新任务,
- TIDYING = 2 << COUNT_BITS;
-
- 过渡状态
- TERMINATED = 3 << COUNT_BITS;
补充:Java中的线程状态
线程状态是新建、就绪、运行、阻塞、结束等状态中的一种
A thread state. A thread can be in one of the following states:
- NEW A thread that has not yet started is in this state.
- RUNNABLE A thread executing in the Java virtual machine is in this state.
- BLOCKED A thread that is blocked waiting for a monitor lock is in this state.
- WAITING A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
- TIMED_WAITING A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
- TERMINATED A thread that has exited is in this state.
线程池执行逻辑
线程池,建议自己new ThreadPoolExecutor
参数说明
- int corePoolSize 核心(Core)线程池数量(corePoolSize)
- int maximumPoolSize 最大线程数量
线程池执行器将会根据corePoolSize和maximumPooSize自动维护线程池中的工作线程,大致规则如下图:
当在线程池接收到新任务,并且当前工作线程数少于CorePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePocSize核心;如果当前工作线程数多于corePoolSize数且小于maximumPoolSize数,那么仅当任务排队队列己满时才会创建新线程,故可以通过设置相同的corePoolSize和maximumPoolSize值来创建一个固定大小的线程池;当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务;corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize和setMaximumPoolSize两个方法进行动态更改
- long keepAliveTime 空闲线程存活时间(最大允许线程不干活的时间)
-
- 线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时长(或者说保活时长),如果超过这个时间,默认情况下Idle、非Core线程会被回收;如果池在使用过程中提交任务的频率变高,也可以调用方法setKeepAliveTime(long,TimeUnit)进行线程存活时间的动态调整,可以将时长延长;如果需要防止Idle线程被终止,可以将Idle时间设置为无限大,具体如下:setKeepAliveTime(Long.MAX VALUE,TimeUnit.NANOSECONDS);默认情况下,Idle超时策略仅适用于存在超过corePoolSize线程的情况。但若调用了allowCoreThreadTimeOut(boolean)方法,并且传入了参数true,则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。
- TimeUnit unit keepAliveTime的时间单位
- BlockingQueue<Runnable> workQueue 阻塞队列,存放未来得及执行的任务
-
- BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,那么所接收到的目标任务缓存在阻塞队列中。任务阻塞队列与普通队列相比的重要特点:在阻塞队列为空时,会阻塞当前线程的元素获取操作(被阻塞的线程会在队列里有了元素以后被自动唤醒)
- 常用实现类
-
-
- ArrayBlockingQueue:数组实现(有界队列)
- LinekedBlockingQueue:链表实现 有界(设置)/无界(不设置默认Integer.Max_VALUE 即无限进队列直到资源耗尽) FIFO,吞吐量 > ArrayBlockingQueue
-
-
-
-
- 使用此队列创建线程池的工厂方法
-
-
-
-
-
-
- Executors.newSingleThreadExecutor()
- Executors.newFIxedThreadPool()
-
-
-
-
-
- PriorityBlockingQueue:具有优先级的无界队列
- DelayQueue:无界阻塞延迟队列(底层基于PriorityBlockingQueue),每个元素都有过期时间(过了期的才能出队列,队列头部的元素最先过期)
-
-
-
-
- 使用此队列创建线程池的工厂方法
-
-
-
-
-
-
- Executors.newScheduledThreadPool()
-
-
-
-
-
- SynchronousQueue:同步队列,不存储元素(直接新建线程来执行),插入即出队(否则插入动作阻塞)
-
-
-
-
- 使用此队列创建线程池的工厂方法
-
-
-
-
-
-
- Executors.new CachedThreadPool()
-
-
-
- ThreadFactory threadFactory 创建线程的工厂
- RejectedExecutionHandler handler 拒绝策略
- 默认定义好的拒绝策略有4种(也可以通过继承RejectdExecutionHandler实现自定义的)
-
AbortPolicy(默认): 队列满了丢弃任务并抛出异常
DiscardPolicy:队列满了丢弃任务但不抛出异常
DiscardOldestPolicy:将最早进入队列的任务删除后,再尝试加入队列
CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务(会严重影响程序效率) -
此外,tomcat的线程池和jdk的线程池的运行机制有些不同。在核心线程池满了后,jdk线程池,选择将任务添加到任务队列;而tomcat线程池则是开始按照最大线程池的定义,开始创建线程,待达到最大线程池数量后,才将任务添加到任务队列。
设计区别的原因是,jdk线程池设计考虑的是计算密集型,那么线程达到核心线程池数量,认为cpu已经很繁忙了;而tomcat则是IO密集型的,所以在达到核心线程数量时候,cpu还很空闲,应该先创建线程。(此处摘自@luzaichun,感谢作者分享)
总结下线程池调度器创建线程的重要规则
- corePoolSize已满+阻塞队列已满 ->才会去创建新的线程(小于核心线程数是不会进行复用的)
- 当前线程数 < 核心线程数:只要有新任务进来,就会创建核心线程
- 队列已满,当前线程数 < 最大核心线程数,才会创建非核心线程
- 队列已满,当前线程数 >= 最大核心线程数,会调用拒绝策略
线程池源码解读
execute()
execute()方法是提交任务到线程池的核心方法,源码如下:
// command就是提交过来的任务
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
// 获取核心属性ctl,用于后续的判断
int c = ctl.get();
// 如果线程个数 < 核心线程数 -> 添加核心工作线程
if (workerCountOf(c) < corePoolSize) {
// addWorker(任务,是核心的吗?) 返回true表示添加成功,反之失败
// addWorker()方法中会根据线程池状态,以及工作线程数做判断,查看能否添加工作线程
if (addWorker(command, true))
// 成功构建,任务交给command处理
return;
// 此时线程池状态或者线程数发生了变化,重新获取ctl
c = ctl.get();
}
// 添加核心线程失败:判断线程池状态是否为Running
// 如果是,使用offer()方法将任务添加到阻塞队列中
if (isRunning(c) && workQueue.offer(command)) {
// 任务成功添加到阻塞队列中
// 以防止线程池状态忽然变化,再次进行判断
int recheck = ctl.get();
// 如果程池状态不是Running,将任务从阻塞队列移除
if (! isRunning(recheck) && remove(command))
// 采用拒绝策略
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
addWorker()
addWorker()
这个方法其实就是添加线程的,主要分为两大块
- 校验线程池的状态以及工作线程的个数
- 添加工作线程并启动工作线程
传入参数为当前待执行的任务实例(firstTask)和是否是核心线程的标识(core)
返回值为true时表示创建worker成功,为false表示创建worker失败
方法一开始的for (;;)
相当于开始自旋,整体功能是判断当前线程池状态是否允许创建线程
获取当前的 ctl 值和 当前线程池运行状态,然后判断当前线程池状态是否允许添加线程(其中,rs >= SHUTDOWN
是对于线程池状态的判断,从上面第二部分对于线程池状态的介绍里,我们可以知道,除了RUNNING状态(-1),其他的状态都大于SHUTDOWN(0)),如果符合不允许的条件就会返回false,不符合则会进入后面获取创建线程令牌的自旋(也是一个for (;;)
),这个内部的自旋会先获取当前线程池中的线程数量,然后根据线程数和是否是核心线程来再次判断能否添加新的线程,如果符合不允许创建的条件则返回false,否则会继续后面的逻辑,执行compareAndIncrementWorkerCount()
这个原子方法对线程数进行+1的操作,这个方法返回true表明记录线程数量已经+1成功了(即申请到了一块令牌),返回false表明其他线程修改过了ctl值了,申请成功,则直接跳出,失败,则再获取ctl的最新值并判断判断当前线程池状态是否发生过改变,如果状态改变则回到外部循环再次开始,如果未改变则继续当前自旋。
在获取到创建线程的令牌以后,开始尝试创建一个Worker
并将它添加进线程池中,变量workerStarted
表示创建的Worker是否已经启动(启动true/未启动false),变量workerAdded
表示创建的Worker是否已经添加进线程池中(默认false,未添加)。从源码中可以看到,在创建的过程中会先创建并持有一个ReentrantLock的全局锁,接着再次获取线程池最新的状态,如果线程池状态正常并且传入的任务实例未start,则将新建的线程对象添加进线程池,之后更新当前线程池的最大值并将一开始创建的标识位workerAdded
设为true,说明当前添加worker添加是成功的,在workerAdded
为true的前提下,启动线程(调用start()
)并将变量workerStarted
设置为true,如果线程未启动成功则会释放令牌并当前新建的Worker清理出线程池集合,最后,返回新创建的线程是否启动。
附:线程池为接口什么要构建空任务的非核心线程
首先线程池中的线程被称为Worker
这个问题其实就是为什么要加addWorker(null, false);
是为了避免出现线程池内无线程但是工作/阻塞队列中有任务,这种情况会导致任务一直在队列中放着(任务饥饿),直到下一个任务进来
可能出现这种情形的场景:
1. 核心线程数可以等于0
当核心线程数为0时,会导致execute()方法中进入将任务放入阻塞队列中,Excutors类中有一个创建线程池的方法newCachedThreadPool(),会在初始化时,将核心线程数设置为0
2. 核心线程可以设置超时时间
默认情况下,核心线程是没有设置超时时间的,但是可以通过属性allowCoreThreadTimeOut来设置
当设置了核心线程的超时机制,就可能会出现,在一个任务进来时,全部的核心线程刚好因为超时而被清除了,于是在execute()方法中,将刚进来的任务放入了阻塞队列中
搞定( ̄∇ ̄)/🎉~~~~~~~~~~