Java并发-线程池
线程池的重要性:
计算机系统的资源总是有限的,如何利用有限的资源,更充分的满足需求呢?解决的有效办法之一就是提供池,在计算机中,有很多使用“池”这种技术的地方,比如常见的连接池、线程池、对象池等。所谓“池化技术”,就是程序先向系统申请一定的资源,然后自己管理,以备不时之需。这样每当后续有要申请该资源的时候,就发现已经有可以复用的资源,这样使用时就会变得非常快捷,大大提高程序运行效率。
如果不使用线程池,每一个任务都新开一个线程处理,在任务数量比较少的时候我们可以直接创建线程进行处理。
JVM对于线程的实现,每一个线程都对应到操作系统的原生线程,当任务数量非常多的时候,如果仍然通过直接创建线程的方式来处理多任务,对计算机系统资源的开销,尤其是内存资源,而且操作系统对于线程数量是有上限的,简单的来一个任务创建一个线程的开销太大。
使用线程池的目的就是为了解决反复创建线程开销太大,且过多的线程会占用太多的内存。
解决这两个问题的思路就是:
- 只允许存在少量的线程,避免内存占用过高。
- 让这部分少量的线程都保持工作,而且可以反复执行任务,这样可以避免频繁创建线程的开销。
线程池的好处:
- 可以加快相应速度,因为没有频繁创建销毁线程。
- 线程池的线程数量可控,可以更加合理的利用CPU和内存资源。
- 线程池可以方便我们统一管理线程(比如统一停止线程任务等)。
线程池的适用场景:
- 服务器接收到大量请求时,使用线程池,可以大大减少线程的创建和销毁,提高服务器的工作效率。
- 实际开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理。
创建和停止线程池:
线程池的构造函数的参数?
参数名 | 类型 | 含义 |
---|---|---|
corePoolSize | int | 核心线程数,核心线程数之下的线程会一直存活,线程池在完成初始化之后,默认的情况下,是没有任何线程的,线程池会等待有任务来到,再去创建新的线程去执行任务。 |
maxPoolSize | int | 最大线程数 ,线程池可能会在核心线程数的基础上,增加一些额外的线程,但是这些增加的额外线程数量也是有限制的,这个限制就是maxPoolSize。 |
keepAliveTime | long | 保持存活的时间,如果线程池当前的线程数多于corePoolSize,那么多于的那些线程空闲时间超过keepAliveTime,冗余线程就会被终止,减少资源消耗,对于corePoolSize数中的核心线程,也有回收的办法,可以通过 allowCoreThreadTimeOut(true) 方法设置,在核心线程空闲的时候,一旦超过 keepAliveTime 配置的时间,也将其回收掉。 |
workQueue | BlockingQueue | 任务存储队列 |
threadFactory | ThreadFactory | 当线程池需要新的线程的时候,会使用ThreadFactory来生成新的线程 |
handler | RejectedExecutionHandler | 由于线程池无法接受你所提交的任务的拒绝策略 |
corePoolSize和maxPoolSize图解:
当任务存储队列中的任务数量没有满的时候,任务都是交给CorePool中的线程去处理的,但是当任务存储队列满了,CorePool中的线程已经不能满足需求,就会扩展创建额外的线程,额外创建的线程数不能超过maxPoolSize。
如果线程数小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新的线程来运行新任务。
如果线程数等于或大于corePoolSize,但是少于maxPoolSize,则将任务放到等待队列。
如果队列已经满了,并且线程数量小于maxPoolSize,那么就去创建新的线程来运行任务。
如果队列也满了,并且线程数大于或者等于maxPoolSize,就会执行拒绝策略。
从上面的流程分析,我们可以得出以下结论:
- 通过设置corePoolSize和maxPoolSize为相同值,就可以创建固定大小的线程池。
- 线程池倾向于保持较少的线程数,当只有任务负载超过预期(任务队列满了)的时候,才会去增加线程。
- 当maxPoolSize设置一个很大的值比如Integer.MAX_VALUE的时候,线程池可以容纳“任意数量”的并发任务,因为线程不够就会去创建,反正也打不到maxPoolSize的上限。
- 只有在队列满了才有可能去创建超过corePoolSize限额的线程,如果使用无界限队列(比如LinkedBlockQueue),因为任务队列满不了,所以线程池中的线程数量永远都不会超过corePoolSize。
ThreadFactory
新的线程的创建都是通过ThreadFactory来创建,默认可以使用Executors.defaultThreadFactory来创建线程,创建出来的线程和当前线程都在同一个线程组,拥有同样的NORM_PRIORITY,而且都不是守护线程,如果自己制定ThreadFactory,就可以改变线程名,线程组,优先级,是否守护线程等。
守护线程:Java的线程分为User Thread(用户线程)、Daemon Thread(守护线程) 。只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就全部可以工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。User和Daemon两者几乎没有区别,唯一的不同之处就在于当虚拟机的结束运行的时候,如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了, 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了,因此,像读写操作这一类行为,就不太适合放到守护线程进行,因为一旦用户线程结束运行,守护线程也会结束运行,无法确保程序的读写操作是否正确。
defaultThreadFactory的源码如下:
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
@SuppressWarnings("removal")
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
工作队列:
直接交接:SynchronousQueue
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素,当线程池无法接收更多任务,就会执行拒绝策略。
无界队列:LinkedBlockingQueue:
这个队列需要注意的是,虽然通常称其为一个无界队列,但是可以人为指定队列大小,而且由于其用于记录队列大小的参数是int类型字段,所以通常意义上的无界其实就是队列长度为 Integer.MAX_VALUE,且在不指定队列大小的情况下也会默认队列大小为 Integer.MAX_VALUE。
有界队列:ArrayBlockingQueue,有界队列满了且线程数达到maxPoolSize,此时若再有新任务进来,就会执行拒绝策略。
线程池应该手动创建还是自动创建?
手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源消耗殆尽。
自动创建线程池的方式:
newFixedThreadPool:
比如通过newFixedThreadPool去自动创建一个固定大小的线程池,我们可以看下newFixedThreadPool的实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
就是创建一个corePoolSize=maxPoolSize=nThreads的线程池,且用的是一个无界队列,当无界队列中的请求越来越多的时候,并且线程池如果无法及时处理无界队列中的任务,就容易造成大量的内存占用,可能会导致OOM。
示例代码如下:
public class FixedThreadPoolOOMTest {
private static ExecutorService executorService = Executors.newFixedThreadPool(1);
public static void main(String[] args) {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
System.out.println("add task:" + i);
// 不停往无界队列中插入任务
executorService.execute(new SleepTask());
}
}
}
class SleepTask implements Runnable{
@Override
public void run() {
// 模拟耗时任务
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这里我们修改一下VM的参数,将VM的最大内存调小一点,更容易复现问题:
可以看到执行结果:
newSingleThreadExecutor:
这个线程池和newFixedThreadPool非常相似,区别在于线程池线程数只能是1:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
所以也存上上面的问题。
CachedThreadPool:
可缓存线程池,特点是线程池本身是无界限的,具有自动回收多于线程的功能。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到,CachedThreadPool创建的核心线程数是0,但是maxPoolSize是没有上限的(MAX_VALUE可以认为就是没有上限),然后任务队列用的是SynchronousQueue,即有一个任务来,如果没有空闲线程,就会创建一个线程然后执行。
我们可以测试一下:
public class CachedThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
executorService.execute(new Task());// 打印线程名字
}
}
}
// output,可以看到确实是复用了线程
pool-1-thread-884
pool-1-thread-833
pool-1-thread-8320
pool-1-thread-867
pool-1-thread-852
pool-1-thread-868
pool-1-thread-892
pool-1-thread-8313
pool-1-thread-859
因为maxPoolSize没有上限,当任务特别多,且有特别多的任务一直占用着工作线程没有释放,就会导致创建很多的工作线程,诱发出现线程表OOM。
ScheduleThreadPool
ScheduledThreadPool是一个能实现定时,周期性质的线程池,其内部maxPoolSize是无上限的,然后任务队列用的是DelayedWorkQueue,支持延时执行。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
示例代码如下:
public class ScheduleThreadPool {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
// 延迟5s执行task
scheduledExecutorService.schedule(new Task(), 5, TimeUnit.SECONDS);
// 延迟1s执行, 每隔3秒执行一次
scheduledExecutorService.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);
}
}
workStealingPool:
WorkStealingPool的使用简析_绅士jiejie的博客-CSDN博客_workstealingpool
正确创建线程池的方法:
根据不同的业务场景,自己设置线程池参数,比如我们能用的内存有多大,需要自定义线程的名字类型等等。
各类线程池比较:
入参 | FixedThreadPool | CachedThreadPool | ScheduledThreadPool | SingleThreadPool |
---|---|---|---|---|
corePoolSize | 构造函数入参x | 0 | 构造函数入参x | 1 |
maxPoolSize | x | Integer.MAX_VALUE | Integer.MAX_VALUE | 1 |
keepAliveTime | 0s,因为最大线程数等于核心线程数,此事keepAliveTime参数其实没作用 | 60s | 0s,可自动回收空闲的线程 | 0s,同FixedThreadPool |
FixedThreadPool和SingleThreadExecutor用LinkedBlockingQueue的原因是因为这两类线程池的线上数量是有上限的,为了能够让任务都可以执行,需要有一个无界限的队列存放任务,让线程池依次去取。
CachedThreadPool用SynchronousQueue的原因是,这个线程池本身没有线程数量上限,来一个任务都不需要去保存在任务队列中,直接交给线程池,因为没有空余线程的情况下,线程池可以直接创建线程处理任务。
ScheduleThreadPool用DelayedWorkQueue是为了支持延时和定时执行任务。
线程池里边的线程数量设定为多少比较合适?
线程池最重要的参数就是corePoolSize和maxPoolSize。通常按照下面的规则进行设置:
- CPU密集型的任务(计算hash,加密等),最佳线程数量为CPU核心数的1-2倍左右。
- 耗时IO类型的任务(读写数据库,文件,网络等),最佳线程数一般大于CPU核心数的很多倍,可以通过JVM线程监控,找到线程最繁忙的情况,保证线程空闲的时候,可以有任务进行衔接即可,参考公式:线程数=CPU核心数*(1 + 平均等待时间/平均工作时间),比如读写数据库等待时间100s,而线程的工作时间是1s,那么线程数就应该设置成核心数乘以101,当然,如果想拿到更精准的数量,可以通过给各种case做压测,找到一个最适宜的线程池数量。
停止线程池的正确方法?
shutdown:
它可以安全地关闭一个线程池,调用 shutdown() 方法之后线程池并不是立刻就被关闭,因为这时线程池中可能还有很多任务正在被执行,或是任务队列中有大量正在等待被执行的任务,调用 shutdown() 方法后线程池会在执行完正在执行的任务和队列中等待的任务后才彻底关闭。调用 shutdown() 方法后如果还有新的任务被提交,线程池则会根据拒绝策略直接拒绝后续新提交的任务。
public class ShuntDownThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executorService.execute(new ShutDownTask() );
}
// 让线程池运行一会儿
Thread.sleep(1500);
executorService.shutdown();
System.out.println("关闭线程池");
}
}
class ShutDownTask implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
}
可以看到,我们调用了shutdown,但是线程池中没有执行完的任务仍然可以执行。
如果我们在shutdown后面再往线程池中提交任务,就会执行异常策略,通常默认的是抛出异常:
但是我们可以看到,主线程异常终止了,但是线程池中没有执行完的任务仍然会继续执行。
isShutdown:
isShutdown可以判断线程池是否处于shutdown状态:
System.out.println("线程池状态1 " + executorService.isShutdown());
executorService.shutdown();
System.out.println("线程池状态2 " + executorService.isShutdown());
执行结果如下:
我们可以看到,调用shutdown之后,虽然得到了线程池是shutdown状态,但是线程池仍然在运行,因此这个方法不能用来判断线程池是否真的结束运行了。
isTerminated:
返回整个线程池是否完全终止:
// 让线程池运行一会儿
Thread.sleep(1500);
executorService.shutdown();
System.out.println("线程状态1 " + executorService.isTerminated());
Thread.sleep(15000);
System.out.println("线程状态2 " + executorService.isTerminated());
执行结果:
只有等到整个线程池中的任务都执行完毕了,才会返回true。
awaitTermination
接收timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false,这个方法会阻塞当前线程指定时间。
测试代码:
// 让线程池运行一会儿
Thread.sleep(1500);
System.out.println("主线程休眠结束" + System.currentTimeMillis());
boolean status = executorService.awaitTermination(3L, TimeUnit.SECONDS);
System.out.println("等待判断是否线程池终止" + status + " " + System.currentTimeMillis());
// output:
主线程休眠结束1645206514090
xxxxx
等待判断是否线程池终止false 1645206517106
可以看到主线程在执行awaitTermination期间阻塞了3s。
shutdownNow
立刻关闭线程池中的所有线程,在执行 shutdownNow 方法之后,首先会给所有线程池中的线程发送 interrupt 中断信号,尝试中断这些任务的执行,然后会将任务队列中正在等待的所有任务转移到一个 List 中并返回,我们可以根据返回的任务 List 来进行一些补救的操作,例如记录在案或者在后期重试等。
测试代码如下:
public class ShuntDownThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
executorService.execute(new ShutDownTask(i));
}
// 让线程池运行一会儿
Thread.sleep(1500);
List<Runnable> saveRunList = executorService.shutdownNow();
saveRunList.forEach(new Consumer<Runnable>() {
@Override
public void accept(Runnable runnable) {
System.out.println("尚未执行的任务" + ((ShutDownTask)runnable).toString());
}
});
}
}
class ShutDownTask implements Runnable{
private final int index;
public ShutDownTask(int i){
this.index = i;
}
@Override
public String toString() {
return "ShutDownTask{" +
"index=" + index +
'}';
}
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程被中断" + Thread.currentThread().getName());
}
System.out.println(Thread.currentThread().getName());
}
}
如何处理线程池任务拒绝
拒绝时机:
- 在上面的代码中我们也演示了,当线程池被关闭的时候,再提交新的任务会被拒绝。
- 当线程池对线程数量已经达到了最大线程和工作队列使用了有限边界的队列且已经饱和,再提交新的任务的时候就会被拒绝。
拒绝策略:
AbortPolicy:
线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy:
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃,任务提交方不会感知到任何信息。
DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务。
CallerRunsPolicy
如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务,避免像Discard策略那样任务被丢弃的损失,同时,因为线程池无法继续处理更多的任务,将任务返回给提交任务的线程,可以作为一种负反馈,减慢提交任务线程继续提交更多任务的速率,缓解线程池的压力。
钩子方法:
线程池在处理任务的前后提供了两个切面方法,beforeExecute和afterExecute,分别在任务执行前后被调用,利用这两个方法,我们可以做一些比如监控线程池对任务的处理耗时,可以在before中记录收集各个任务开始的时间,在after中记录收集各个任务结束的时间,然后帮助我们更好的统计分析性能问题。
同时,还可以借助这两个钩子函数,实现比如可暂停的线程池:
public class PasueableThreadPool extends ThreadPoolExecutor {
private boolean isPaused;
private ReentrantLock lock = new ReentrantLock();
private Condition unpaused = lock.newCondition();
// ...省略构造函数
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
lock.lock();
try {
// 在任务被执行之前,如果isPaused为true,就一直等待
while (isPaused){
unpaused.await();
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 暂停线程池
public void pause() {
lock.lock();
try {
isPaused = true;
} finally {
lock.unlock();
}
}
// 恢复线程池
public void resume(){
lock.lock();
try {
isPaused = false;
// 修改完isPaused标记位,同时通知所有等待condition的线程恢复运行。
unpaused.signalAll();
}finally {
lock.unlock();
}
}
}
测试方法和运行结果:
public static void test() throws InterruptedException {
PasueableThreadPool pasueableThreadPool = new PasueableThreadPool(5,10,10L,TimeUnit.SECONDS, new LinkedBlockingQueue<>());
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "执行了我");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 5000; i++) {
pasueableThreadPool.execute(task);
}
Thread.sleep(1500);
pasueableThreadPool.pause();
System.out.println("线程池被暂停了");
Thread.sleep(1500);
pasueableThreadPool.resume();
System.out.println("线程池恢复了");
}
// output
运行结果:
可以看到,在线程池被暂停之后,是没有任务被执行的(线程池恢复了和线程池被暂停了之间插入的任务是因为并发问题导致的,主线程resume之后,立即有任务抢险主线程sout之前先sout了)。
线程池源码分析:
线程池由四部分组成:
- 线程池管理器, 负责创建线程,停止线程等。
- 工作线程
- 任务队列,因为任务队列是可能出现多个线程并发往队列中提交任务的,因此队列数据结构选择的是线程安全的BlockingQueue。
- 任务接口(Runnable),具体要执行的任务。
线程池的接口继承关系:
- Executor作为线程池家族的顶层接口,只定义了一个execute方法,表示任务执行。
- ExecutorService继承自Executor接口,增加了比如shutdown等方法。
public interface ExecutorService extends Executor {
// 请求关闭、发生超时或者当前线程中断,无论哪一个首先发生之后,都将导致阻塞,直到所有任务完成执行。
boolean awaitTermination(long timeout, TimeUnit unit);
// 执行给定的任务,当所有任务完成时,返回保持任务状态和结果的 Future 列表。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
// 执行给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);
// 执行给定的任务,如果某个任务已成功完成(也就是未抛出异常),则返回其结果。
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
// 执行给定的任务,如果在给定的超时期满前某个任务已成功完成(也就是未抛出异常),则返回其结果。
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit);
// 如果此执行程序已关闭,则返回 true。
boolean isShutdown();
// 如果关闭后所有任务都已完成,则返回 true。
boolean isTerminated();
// 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
void shutdown();
// 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
List<Runnable> shutdownNow();
// 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
<T> Future<T> submit(Callable<T> task);
// 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
Future<?> submit(Runnable task);
// 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
<T> Future<T> submit(Runnable task, T result);
}
- AbstractExecutorService,抽象的线程池,对线程池任务执行做了抽象实现,便于子类复用父类的能力。
- ThreadPoolExecutor,真正的线程池实现。
注:Executors和Executor的区别是,前者是JDK提供的一个方便创建各类线程池的工具类。
线程池实现线程复用的原理:
线程复用的本质就是用同一个线程去执行不同的任务,即在线程池中的线程不同于不同的线程那样没有任务执行就会结束,线程池中的线程的run方法中,做的事儿就是执行任务或者阻塞等待有新任务到来,没有任务执行的时候就阻塞等待。
源码分析:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 如果工作线程数量小于核心线程数,有新任务到来,那么就添加一个Worker线程
// 并把新任务交给这个线程执行。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 尝试把任务插入到任务队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// double check如果当前线程池已经不再运行状态且能够把任务从队列中移除
// 就表明刚刚新插进去的任务应该要被拒绝
if (!isRunning(recheck) && remove(command))
reject(command);// 拒绝任务
else if (workerCountOf(recheck) == 0)
// 任务已经放到任务队列里了,如果此时没有线程,那么创建新的工作线程
// 工作线程再去任务队列取任务运行。
addWorker(null, false);
}
// 如果插入失败,再尝试一次看能不能创建非核心线程,如果不能
// 说明此时线程池不能再创建更多的线程,任务队列也没办法接纳任务,只能拒绝了
else if (!addWorker(command, false))
reject(command);
}
Worker是线程池中任务线程的封装,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行workQueue中的任务,也就是非核心线程的创建。
而Worker的run方法,就是工作线程的核心工作,调用到ThreadPoolExecutor#runWorker方法:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 只要firstTask任务不为空,或者任务队列中拿到的任务不为空
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 回调前面提到过的before钩子函数
beforeExecute(wt, task);
try {
// 执行task
task.run();
// 回调前面提到过的after钩子函数
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
// 将task置空,那么后面就会去取任务队列中的task
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
而上面getTask是一个阻塞方法,如下:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
// 判断是否可以返回空值
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
// 如果当前的线程状态已经>shutdown且(stop或者任务队列是空队列)
decrementWorkerCount();
// 就返回空,那样上面的worker就会退出
return null;
}
// 获取当前的worker线程数
int wc = workerCountOf(c);
// 如果设置了允许核心线程超时或者存在非核心线程在等待的情况,timed就是true
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
// 下面这里,假设对于核心线程,如果当前是处于等待状态
// wc > maximumPoolSize || (timed && timedOut) 是false,因为前一个条件是false,timed是false,timedOut可能为true,整体是false
// 那么是不会给核心线程return null的,因此核心线程会一直阻塞直到有任务到来。
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
// 如果有允许超时设置,那么workQueue中取任务就会阻塞keepAliveTime
// 的时间长度,然后返回
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 如果超时时间内有新任务,就可以拿到,或者不需要超时也能立即拿到
// 就表示有任务可以被处理,返回给worker
// 如果没有,那么下一轮循环执行的时候,如果仍然没有,就会return null
// 让工作线程结束掉
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
线程池的状态:
-
RUNNING:运行状态,可以接受新的任务或者在处理排队的任务。
-
SHUTDOWN:不接受新的任务,但是可以处理已经排上了队的任务。
-
STOP:不接受新的任务,也不处理排队的任务,并且中断正在执行的任务(shutdownNow的效果)。
-
TIDYING:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。在TIDYING状态之后正常就是会转变成TERMINATED状态。
-
TERMINATED
状态说明:线程池彻底终止,就变成TERMINATED状态。 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED
使用线程池的注意事项:
- 避免任务堆积导致OOM。
- 避免线程数过多,因为可能导致OOM。
- 注意排查线程是否被泄露,线程泄露指的是线程的任务执行完毕,本该被回收,但是却没有办法被回收的问题,通常是因为任务的逻辑写的有问题,导致任务并没有被正常结束,而是一直占用着线程资源不放。