目录
在web开发中,服务器会为每一个请求分配一个线程来处理,如果每次请求都要创建一个线程的话,实现起来虽然简单,但是存在一个问题:如果开发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此会大大降低系统的效率,很有可能出现服务器在创建和销毁线程上花费的时间和消耗的系统资源比实际处理的用户请求的时间和资源还多。
于是,线程池应运而生,线程池满足了一个线程执行完一个任务不会被销毁,而是可以继续执行其他任务的目的,他为线程生命周期的开销和资源不足问题提供了解决方案,通过重用线程,线程创建的开销被分摊到了多个任务上,那么,什么时候适合使用线程池呢?
线程池的优点
线程池的核心属性是ctl(如下图),根据ctl拿到线程池的状态以及工作线程的个数

ctl是一个integer类型的数据,一般有32个比特位(可以通过Integer.SIZE获取):
- private static final int COUNT_BITS = Integer.SIZE - 3;、
- // CAPACITY表示线程池当前工作线程能记录的工作线程的最大个数
- private static final int CAPACITY = (1 << COUNT_BITS) - 1;
以下5个状态只有RUNNING表示线程池没问题,可以正常接收任务处理
补充: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

参数说明
线程池执行器将会根据corePoolSize和maximumPooSize自动维护线程池中的工作线程,大致规则如下图:

当在线程池接收到新任务,并且当前工作线程数少于CorePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到corePocSize核心;如果当前工作线程数多于corePoolSize数且小于maximumPoolSize数,那么仅当任务排队队列己满时才会创建新线程,故可以通过设置相同的corePoolSize和maximumPoolSize值来创建一个固定大小的线程池;当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务;corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize和setMaximumPoolSize两个方法进行动态更改

AbortPolicy(默认): 队列满了丢弃任务并抛出异常
DiscardPolicy:队列满了丢弃任务但不抛出异常
DiscardOldestPolicy:将最早进入队列的任务删除后,再尝试加入队列
CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务(会严重影响程序效率)
此外,tomcat的线程池和jdk的线程池的运行机制有些不同。在核心线程池满了后,jdk线程池,选择将任务添加到任务队列;而tomcat线程池则是开始按照最大线程池的定义,开始创建线程,待达到最大线程池数量后,才将任务添加到任务队列。
设计区别的原因是,jdk线程池设计考虑的是计算密集型,那么线程达到核心线程池数量,认为cpu已经很繁忙了;而tomcat则是IO密集型的,所以在达到核心线程数量时候,cpu还很空闲,应该先创建线程。(此处摘自@luzaichun,感谢作者分享)
总结下线程池调度器创建线程的重要规则
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()这个方法其实就是添加线程的,主要分为两大块
传入参数为当前待执行的任务实例(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()方法中,将刚进来的任务放入了阻塞队列中
搞定( ̄∇ ̄)/🎉~~~~~~~~~~