【线程池】

线程池执行任务的具体流程是怎样的

ThreadPoolExecutor中提供了两种执行任务的方法:

  • void execute(Runnable command)
  • Future<?> submit(Runnable task)

实际上submit中最终还是调用的execute()方法,只不过会返回一个Future对象,用来获取任务执行结果。

execute(Runnable command)方法执行时会分为三步:
注意:提交一个Runnable时,不管当前线程池中的线程是否空闲,只要数量小于核心线程数就会创建新线程。
注意:ThreadPoolExecutor相当于是非公平的,比如队列满了之后提交的Runnable可能会比正在排队的Runnable先执行。

线程池的五种状态流转

线程池有五种状态:
RUNNING:会接收新任务并且会处理队列中的任务
SHUTDOWN:不会接收新任务并且会处理队列中的任务
STOP:不会接收新任务并且不会处理队列中的任务,并且会中断在处理的任务(注意:一个任务能不能被中断
得看任务本身)
TIDYING:所有任务都终止了,线程池中也没有线程了,这样线程池的状态就会转为TIDYING,一旦达到此状
态,就会调用线程池的terminated()
TERMINATED:terminated()执行完之后就会转变为TERMINATED
这五种状态并不能任意转换,只会有以下几种转换情况:

  1. RUNNING -> SHUTDOWN:手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用
    shutdown()
  2. (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()触发,如果先调shutdown()紧着调
    shutdownNow(),就会发生SHUTDOWN -> STOP
  3. SHUTDOWN -> TIDYING:队列为空并且线程池中没有线程时自动转换
  4. STOP -> TIDYING:线程池中没有线程时自动转换(队列中可能还有任务)
  5. TIDYING -> TERMINATED:terminated()执行完后就会自动转换

线程池中的线程的关闭方式

我们一般会使用thread.start()方法来开启一个线程,那如何停掉一个线程呢?
Thread类提供了一个stop(),但是标记了@Deprecated,为什么不推荐用stop()方法来停掉线程呢?
因为stop()方法太粗暴了,一旦调用了stop(),就会直接停掉线程,但是调用的时候根本不知道线程刚
刚在做什么,任务做到哪一步了,这是很危险的。
这里强调一点,stop()会释放线程占用的synchronized锁(不会自动释放ReentrantLock锁,这也是
不建议用stop()的一个因素)。

线程池是阻塞队列的原因

线程池中的线程在运行过程中,执行完创建线程时绑定的第一个任务后,就会不断的从队列中获取任
务并执行,那么如果队列中没有任务了,线程为了不自然消亡,就会阻塞在获取队列任务时,等着队
列中有任务过来就会拿到任务从而去执行任务。
某个线程在从队列获取任务时,会判断是否使用超时阻塞获取,我们可以认为非核心线程会poll(),核
心线程会take(),非核心线程超过时间还没获取到任务后面就会自然消亡了。

线程发生异常,是会被移出线程池的

是会的,那有没有可能核心线程数在执行任务时都出错了,导致所有核心线程都被移出了线程
池?
在源码中,当执行任务时出现异常时,最终会执行processWorkerExit(),执行完这个方法后,当前线
程也就自然消亡了,但是!processWorkerExit()方法中会额外再新增一个线程,这样就能维持住固定
的核心线程数。

tomcat定义线程池的方式

Tomcat中用的线程池为org.apache.tomcat.util.threads.ThreadPoolExecutor,注意类名和JUC下的
一样,但是包名不一样。

Tomcat的这个线程池,在提交任务时:

  1. 仍然会先判断线程个数是否小于核心线程数,如果小于则创建线程
  2. 如果等于核心线程数,会入队,但是线程个数小于最大线程数会入队失败,从而会去创建线程
    所以随着任务的提交,会优先创建线程,直到线程个数等于最大线程数才会入队。
    当然其中有一个比较细的逻辑是:在提交任务时,如果正在处理的任务数小于线程池中的线程个数,
    那么也会直接入队,而不会去创建线程,也就是上面源码中getSubmittedCount的作用。

线程池设置核心线程数,最大线程数

我们都知道,线程池中有两个非常重要的参数:

  1. corePoolSize:核心线程数,表示线程池中的常驻线程的个数
  2. maximumPoolSize:最大线程数,表示线程池中能开辟的最大线程个数
    那这两个参数该如何设置呢?
    我们对线程池负责执行的任务分为三种情况:
  3. CPU密集型任务,比如找出1-1000000中的素数
  4. IO密集型任务,比如文件IO、网络IO
  5. 混合型任务
    CPU密集型任务的特点时,线程在执行任务时会一直利用CPU,所以对于这种情况,就尽可能避免发
    生线程上下文切换。
    比如,现在我的电脑只有一个CPU,如果有两个线程在同时执行找素数的任务,那么这个CPU就需要
    额外的进行线程上下文切换,从而达到线程并行的效果,此时执行这两个任务的总时间为:
    任务执行时间2+线程上下文切换的时间
    而如果只有一个线程,这个线程来执行两个任务,那么时间为:
    任务执行时间
    2

通常,如果IO型任务执行的时间越长,那么同时阻塞在IO上的线程就可能越多,我们就可以设置更多
的线程,但是,线程肯定不是越多越好,我们可以通过以下这个公式来进行计算:
线程数 = CPU核心数 *( 1 + 线程等待时间 / 线程运行总时间 )

  • 线程等待时间:指的就是线程没有使用CPU的时间,比如阻塞在了IO
  • 线程运行总时间:指的是线程执行完某个任务的总时间

我们再工作中,对于:

  1. CPU密集型任务:CPU核心数+1,这样既能充分利用CPU,也不至于有太多的上下文切换成本
  2. IO型任务:建议压测,或者先用公式计算出一个理论值(理论值通常都比较小)
  3. 对于核心业务(访问频率高),可以把核心线程数设置为我们压测出来的结果,最大线程数可以等于核心线程
    数,或者大一点点,比如我们压测时可能会发现500个线程最佳,但是600个线程时也还行,此时600就可以为最
    大线程数
  4. 对于非核心业务(访问频率不高),核心线程数可以比较小,避免操作系统去维护不必要的线程,最大线程数可
    以设置为我们计算或压测出来的结果。

线程池源码的基础属性和方法

在线程池的源码中,会通过一个AtomicInteger类型的变量ctl,来表示线程池的状态和当前线程池中
的工作线程数量。
一个Integer占4个字节,也就是32个bit,线程池有5个状态:

  1. RUNNING
  2. SHUTDOWN
  3. STOP
  4. TIDYING
  5. TERMINATED
    2个bit能表示4种状态,那5种状态就至少需要三个bit位

线程池有5个状态,这5个状态分别表示:
1 RUNNING:线程池正常运行中,可以正常的接受并处理任务
2 SHUTDOWN:线程池关闭了,不能接受新任务,但是线程池会把阻塞队列中的剩余任务执行完,剩余任务都处
理完之后,会中断所有工作线程
3 STOP:线程池停止了,不能接受新任务,并且也不会处理阻塞队列中的任务,会中断所有工作线程
4 TIDYING:当前线程池中的工作线程都被停止后,就会进入TIDYING
5 TERMINATED:线程池处于TIDYING状态后,会执行terminated()方法,执行完后就会进入TERMINATED状
态,在ThreadPoolExecutor中terminated()是一个空方法,可以自定义线程池重写这个方法

addWorker方法

addWorker方法是核心方法,是用来添加线程的,core参数表示添加的是核心线程还是非核心线程。
在看这个方法之前,我们不妨先自己来分析一下,什么是添加线程?
实际上就要开启一个线程,不管是核心线程还是非核心线程其实都只是一个普通的线程,而核心和非
核心的区别在于:

如果是要添加核心工作线程,那么就得判断目前的工作线程数是否超过corePoolSize
如果没有超过,则直接开启新的工作线程执行任务
如果超过了,则不会开启新的工作线程,而是把任务进行入队
如果要添加的是非核心工作线程,那就要判断目前的工作线程数是否超过maximumPoolSize
如果没有超过,则直接开启新的工作线程执行任务
如果超过了,则拒绝执行任务

所以在addWorker方法中,首先就要判断工作线程有没有超过限制,如果没有超过限制再去开启一个
线程。
并且在addWorker方法中,还得判断线程池的状态,如果线程池的状态不是RUNNING状态了,那就
没必要要去添加线程了,当然有一种特例,就是线程池的状态是SHUTDOWN,但是队列中有任务,
那此时还是需要添加添加一个线程的。

addWorker方法的核心逻辑

先判断工作线程数是否超过了限制
修改ctl,使得工作线程数+1
构造Work对象,并把它添加到workers集合中
启动Work对象对应的工作线程

runWorker方法

在利用ThreadFactory创建线程时,会把this,也就是当前Work对象作为Runnable传给线程,所以工
作线程运行时,就会执行Worker的run方法

processWorkerExit方法

某个工作线程正常情况下会不停的循环从阻塞队列中获取任务来执行,正常情况下就是通
过阻塞来保证线程永远活着,但是会有一些特殊情况:
如果线程被中断了,那就会退出循环,然后做一些善后处理,比如ctl中的工作线程数-1,然后自己运行结束
如果线程阻塞超时了,那也会退出循环,此时就需要判断线程池中的当前工作线程够不够,比如是否有
corePoolSize个工作线程,如果不够就需要新开一个线程,然后当前线程自己运行结束,这种看上去效率比较
低,但是也没办法,当然如果当前工作线程数足够,那就正常,自己正常的运行结束即可
如果线程是在执行任务的时候抛了移除,从而退出循环,那就直接新开一个线程作为替补,当然前提是线程池的
状态是RUNNING

只有通过调用线程池的shutdown方法或shutdownNow方法才能真正中断线程池中的线
程。
因为在java,中断一个线程,只是修改了该线程的一个标记,并不是直接kill了这个线程,被中断的线
程到底要不要消失,由被中断的线程自己来判断,比如上面代码中,线程遇到了中断异常,它可以选
择什么都不做,那线程就会继续进行外层循环,如果选择return,那就退出了循环,后续就会运行结
束从而消失。
调用线程池的shutdown方法,表示要关闭线程池,不接受新任务,但是要把阻塞队列中剩余的任务执
行完。
根据前面execute方法的源码,只要线程池的状态不是RUNNING,那么就表示线程池不接受新任务,
所以shutdown方法要做的第一件事情就是修改线程池状态。
那第二件事情就是要中断线程池中的工作线程,这些工作线程要么在执行任务,要么在阻塞等待任
务:
// 如果线程池的状态变成了STOP或者SHUTDOWN,最终也会return null,线
程会运行结束
52
// 但是如果线程池的状态仍然是RUNNING,那当前线程会继续从队列中去获取
任务,表示忽略了本次中断
53
// 只有通过调用线程池的shutdown方法或shutdownNow方法才能真正中断线
程池中的线程
54
timedOut = false;
55
}
56
57 }
58 }
shutdown方法
对于在阻塞等待任务的线程,直接中断即可,
对于正在执行任务的线程,其实只要等它们把任务执行完,就可以中断了,因为此时线程池不能接受新任务,所
以正在执行的任务就是最后剩余的任务

mainLock

它是线程池中的一把全局锁,主要是用来控制
workers集合的并发安全,因为如果没有这把全局锁,就有可能多个线程公用同一个线程池对象,如果
一个线程在向线程池提交任务,一个线程在shutdown线程池,如果不做并发控制,那就有可能线程池
shutdown了,但是还有工作线程没有被中断,如果1个线程在shutdown,99个线程在提交任务,那
么最终就可能导致线程池关闭了,但是线程池中的很多线程都没有停止,仍然在运行,这肯定是不
行,所以需要这把全局锁来对workers集合的操作进行并发安全控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值