shutdown()方法原理:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//检查权限.
checkShutdownAccess();
//设置线程池状态
advanceRunState(SHUTDOWN);
//中断空闲线程.
interruptIdleWorkers();
//钩子函数,主要用于清理一些资源.
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
shutdown方法首先加锁,其次检查调用者是否具有执行线程关闭的Java Security权限.接着shutdown方法会将线程状态变为SHUTDOWN,在这之后线程池不在接受提交的新任务.如果还在提交新的任务,就会使用线程池的拒绝策略.
shutdownNOW()方法原理:
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//检查状态.
checkShutdownAccess();
//将线程池状态改为STOP.
advanceRunState(STOP);
//中断所有线程,包括工作线程以及空闲线程.
interruptWorkers();
//丢弃工作队列中剩余的任务.
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
shutdownNow方法将会把线程池状态设置为STOP,然后中断所有线程(包括工作线程以及空闲线程),最后清空工作队列,取出工作队列中未完成的任务返给调用者.与有序的shutdown方法相比,shutdownNow方法比较粗暴,直接中断线程.不过需要注意的是,中断线程并不是直接结束,而是设立了中断状态.
awaitTermination方法:
调用了线程池的shutdown和shutdownNow方法之后,用户程序都不会主动等待线程池关闭完成,如果需要等到线程池关闭完成,需要调用awaitTermination方法进行主动等待.
threadPool.shutdown();
try {
while (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("线程池任务还未执行完毕");
}
} catch (Exception e) {
e.printStackTrace();
}
如果线程池完成关闭 awaitTermination方法会返回true,否则等待时间超过指定时间会返回false.如果需要调用waitTermination方法建议不是永久等待,而是设置重试次数.
优雅的关闭线程池:
大家可以结合上面的几种方式优雅的关闭一个线程池.
1:执行shutdown方法,拒绝提交新的任务,并等待所有的任务有序的执行完成.
2:执行awaitTermination(long timeout, TimeUnit unit)方法,指定超时时间,判断是否关闭所有任务,线程池关闭完成.
3:如果awaitTermination方法返回false,或者被中断,就调用shutdownNow方法立即关闭线程池所有任务.
4:补充执行awaitTermination(long timeout, TimeUnit unit),判断线程池是否关闭完成.如果超时,就进行循环关闭,循环一定的次数,如1000次.不管关闭线程池,直到其关闭或者循环结束.
public class ThreadUtil {
public static void shutdownThreadPoolGracefully
(ExecutorService threadPool) {
//如果线程以及关闭就返回.
if (!((threadPool instanceof ExecutorService)
|| threadPool.isTerminated())) {
return;
}
//拒绝接收任务.
try {
threadPool.shutdown();
} catch (SecurityException e) {
return;
} catch (NullPointerException e) {
return;
}
try {
//等待60秒,等待线程池中的任务完成.
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
//调用shutdownNow立即关闭,
threadPool.shutdownNow();
//在等待60秒,如果还未结束,可以再次尝试或者直接放弃.
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
System.out.println("线程池任务未正常执行结束");
}
}
} catch (InterruptedException e) {
//捕获异常,重新调用shutdownNow方法.
threadPool.shutdownNow();
}
//仍然没有关闭,循环关闭1000次,每次等待10毫秒.
if (!threadPool.isTerminated()) {
try {
for (int i = 0; i < 1000; i++) {
if (threadPool.awaitTermination
(10, TimeUnit.MILLISECONDS)) {
break;
}
}
}catch (InterruptedException e){
System.err.println(e.getMessage());
}catch (Throwable e){
System.err.println(e.getMessage());
}
}
}
}
优雅关闭的原理和线程状态有关系,包括线程的中断点的完美结合.
注册JVM钩子函数自动关闭线程池:
如果使用了线程池,可以再JVM注册一个钩子函数,在JVM进程关闭之前,由钩子函数自动将线程池优雅关闭,以确保资源正常释放.
public class ThreadUtil {
//懒汉式单例加载线程池.
static class ThreadPoolLazyHolder{
//线程池
static final ScheduledThreadPoolExecutor EXECUTOR =
new ScheduledThreadPoolExecutor(1);
static {
//注册JVM关闭时的钩子函数.
Runtime.getRuntime().addShutdownHook(new Thread(()->{
shutdownThreadPoolGracefully(EXECUTOR);
}));
}
}
}
这个例子和上面那个util例子是一起的.就不重复了.
Executors快捷创建线程的潜在问题:
1:Executors.newFixedThreadPool固定线程池的潜在问题
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
//核心线程数.
nThreads,
//最大线程数
nThreads,
//线程最大空闲时间.
0L,
//时间单位.毫秒.
TimeUnit.MILLISECONDS,
//任务的排队队列,无界队列.
new LinkedBlockingQueue<Runnable>());
}
可以看到阻塞队列没有指定大小.看源码
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
参数大小默认为 Integer.MAX_VALUE.如果任务提交速率持续大于处理速度,就会造成大量任务等待,如果队列很大,很可能会导致JVM出现OOM异常,内存资源耗尽.
2:Executors.newSingleThreadExecutor单例线程池的潜在问题:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
以上代码首先通过调用工厂方法FinalizableDelegatedExecutorService对该固定线程池进行包装,这一层包装的作用是防止线程池的核心线程被动态的修改.
可以看到阻塞队列没有指定大小.看源码
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
阻塞队列的默认构造方法也是一个无界队列.
3:Executors.newCachedThreadPool缓存线程池潜在问题:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
通过源码可以看到创建了一个核心线程数为0 最大线程数不设限制的线程池,所以理论上可缓存线程池可以拥有无数个工作线程.
可缓存线程池的工作队列为SynchronousQueue同步队列,这个队列类似于一个接力棒,与出队必须同时传递.当可缓存线程池有任务到来时,新任务会被插入到SynchronousQueue实例中,由于SynchronousQueue是同步队列,会从线程池中寻找可用的线程来执行,没有的话就创建一个新的线程.
SynchronousQueue是一个比较特殊的阻塞队列实现类.没有容量,每一个插入操作都对应着删除操作,反之每个删除操作都对应着插入操作.也就是说,提交的任务不会被保存,而是将新任务交给空闲线程执行,如果没有空闲线程就创建一个新的线程,如果线程数已经大于最大线程数,就执行拒绝策略.使用这种阻塞队列就需要把最大线程数设置的很大.而这个线程池的最大问题就在最大线程数量不设上限.如果任务提交过多会造成OOM异常,甚至因为线程过多,造成资源耗尽.
4:Executors.newScheduledThreadPool可调度线程池潜在问题
public static ScheduledExecutorService
newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
Executors.newScheduledThreadPool工厂方法调用了ScheduledThreadPoolExecutor实现类的构造器,而ScheduledThreadPoolExecutor继承了ThreadPoolExecutor的普通线程池类,在构造器内部进一步调用了父类的构造器.
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
以上代码创建了一个 ThreadPoolExecutor实例,其中corePoolSize为传递来的参数.最大线程数为Integer.MAX_VALUE,表示线程数不设上限,阻塞队列为DelayedWorkQueue实例,这是一个按照到期时间升序的阻塞队列.
可调度线程池的潜在问题是最大线程数不设置上限,如果过期任务过多的话,会导致CPU的线程资源耗尽,可以通过源码看到潜在问题首先还是在阻塞队列为无界队列,可能会堆积大量的任务,造成OOM异常.
确定线程池的线程数
按照任务类型对线程池进行分类
1:IO密集型任务
此类任务主要执行IO操作.由于IO操作的时间比较长,导致CPU的使用率不高,这类任务CPU常处于空闲状态.
2:CPU密集型任务
此类任务主要是执行计算任务,由于响应时间很快,CPU一直运行,这种任务的CPU利用率很高.
3:混合型任务
此类任务是既要执行逻辑计算,又要进行IO操作(如RPC调用 数据库访问).相对来说,由于执行IO操作的耗时比较长(一次网络往返往往在数百毫秒级别),这种任务的CPU利用率也不是很高.Web服务器的HTTP请求处理操作为此类任务的典型例子.
IO密集型任务确定线程数
由于IO密集型任务CPU使用率比较低.导致线程空余时间很多.因此通常需要开CPU核数两倍的线程.当IO线程空闲时,可以使用其他线程继续使用CPU.提高CPU的使用率.
CPU密集型任务确定线程数
CPU密集型任务也叫计算密集型任务,其特点是要进行大量的计算需要消耗CPU资源.比如计算圆周率 对视频进行高清解码.CPU密集型任务虽然可以并行完成,但是并行的任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,,所以为了高效的利用CPU,线程数量应当等于CPU的核心数就行.
混合型任务确定线程数
混合型任务既要执行逻辑计算,又要进行IO操作(如RPC调用 数据库访问).所以混合型任务CPU利用率不是很高,但是非CPU耗时往往又是CPU耗时的数倍.比如WEB应用处理HTTP请求时,一次请求处理会包括DB操作 RPC操作 缓存操作等多种耗时操作.
业界有一种比较成熟的估算方式来确定混合任务的线程数.
最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数
经过换算,上面的公式可以转换为.
最佳线程数=(线程等待时间与线程CPU时间之比+1)*CPU核数.
通过公式可以看出来,线程等待时间越长,线程数越多.CPU耗时所占比例越少,线程数越少.
不管怎么做可能都是不完美的,坚持或者不坚持.但是我觉着有一种大爱叫做,我在.
我在坚持,我在坚持最需要的时候会义无反顾的站出来.
如果大家喜欢我的分享的话.可以关注一下微信公众号.
心有九月星辰