作者 | Java圣斗士 | 原创图文,转载请注明出处
全文2000字,阅读可能需要点时间,建议收藏

哈喽大家好,我是又皮又可爱的Java圣斗士,关注我,每天带你飞!
小伙伴们都知道,在初级Java工程师的面试题中,总会被问到诸如如何启动一个执行绪?如何执行一个任务?start()和run()哪一个才是启动执行绪,等等诸如此类的低阶问题,我们都知道执行绪可以通过两种方式来实现,继承Thread类或者实现Runnable界面。我们也可以通过像下面的程式码这样快速启动一个执行绪:
new Thread(() -> {
// some codes run here
}).start();
但是很显然,这种程式码太Demo了,作为某些知识的演示还好,但是实际的并发场景如此复杂,这样的程式码可无法胜任!

那么如何更加专业地设计执行绪的执行策略呢?今天我们就来讨论这个话题!
我们都知道,我们的并发程式都是通过“执行任务”的机制来设计的,而任务通常就是一些抽象且离散的工作单元。当围绕“任务执行”来设计应用程序结构时,第一步就是要找出清晰的任务边界。
在理想情况下,各个任务之间是相互独立的:任务并不依赖于其他任务的状态、结果或边界效应。
执行绪数量的限制
在《Java并发程式设计实战》中,当描述执行绪数量限制的时候,引出了一个重要的问题,当执行绪过多的时候,系统内存会不会因此而丢掷OutOfMemoryError?为了不破坏系统的稳定性,在我们可建立的执行绪数量上存在一个限制。这个限制受平台以及多个因素影响,包括JVM启动引数、Thread建构函式中请求的栈大小、底层操作系统对执行绪的限制等。
正因为有了这种限制,上面的程式码才无法胜任实际业务需求,因为通过new关键字,轻易地将执行绪创建出来是对系统内存的一种不负责任的消耗。而正确的执行绪执行策略应该是通过Executor来完成。
public interface Executor {
void execute(Runnable command);
}
Executor是一个非常简单的界面,它的语义是“执行器”,我们通过向execute()方法传入一个Runnable来执行任务。
Executor本身是基于生产者消费者,它将任务的定义与执行解耦开来,提交任务相当于生产者,而执行任务相当于消费者,所以,如果程式中需要实现一个生产者-消费者的设计,那么最简单的方式通常就是使用Executor。而我们前面也说过,所有的执行绪几乎都围绕着“执行任务”而展开,从某种层面上来讲,Executor就是我们定义任务、执行任务的首选方案!
四大执行绪池
常用的执行绪池有四种,这也是面试中经常问到的:你用过哪些执行绪池?没听过可就糟糕了。fixedThreadPool
cachedThreadPool
scheduledThreadPool
singleThreadExecutor
它们的建立方式如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
我们通过Executors这个工具类的工厂方法来建立对应的执行绪池,这些工厂方法分别呼叫了ThreadPoolExecutor和ScheduledExecutorService的一系列构造器,它们之间的继承关系图谱我已经整理出来了,如下所示:

来说说这四个工厂方法:
1、newFixedThreadPool(int) :建立一个定额执行绪池,每提交一个任务建立一个执行绪,达到数量限制后不再增加,这时执行绪池的规模将不再变化(如果某个执行绪由于发生了未预期的异常而结束,那么执行绪池会补充一个新的执行绪)
2、newCachedThreadPool() : 建立一个可快取的执行绪池,执行绪池的规模不存在任何限制,当执行绪多余任务时,回收空闲执行绪;当任务增加时,建立新执行绪。
3、newSingleThreadExecutor:单执行绪的Executor,如果这个执行绪异常结束,会建立另一个执行绪来替代。NewSingleThreadExecutor能确保依照任务在伫列中的顺序序列执行(例如FIFO、LIFO、优先级)。
4、newScheduleThreadPool:建立一个固定长度的执行绪池,而且以延迟或定时的方式来执行任务,类似于Timer。
Executor的生命周期
Executor界面简单的定义了任务提交后的执行方法execute(),而执行器的生命周期必须通过某些额外的方法才能够实现,比如shutdown()等,这些方法均扩充套件在了它的子界面ExecutorService中:public interface ExecutorService extends Executor {
void shutdown();
List shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
// ......其他用于任务提交的便利方法
}
这五个方法是宣告周期管理的方法。那么可以得出Executor的三种状态:
执行、关闭、终止
ExecutorService在初始建立时处于执行状态。shutdown()方法将执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成——包括那些还未开始执行的任务。
shutdownNow()方法将执行粗暴的关闭方式:它将尝试取消所有执行中的任务,并且不再启动伫列中尚未开始执行的任务。
延迟任务与周期任务
有时候,我们实现一个“闹钟”功能会用到Timer,这个类可以管理任务以及周期任务,但是它本身存在一定缺陷,其缺陷在于Timer在执行所有定时任务时只会建立一个执行绪,如果某个执行绪的执行时间过长,那么势必破坏任务定时的准确性。因此,通常用scheduleThreadPoolExecutor来代替使用。
往期精彩:
《技术新人不知道如何提升自己?教你一招,坚持下去准没错!》
《用惯了框架的分页API?今天教你手写分页查询》
《不会MySQL效能优化?21条最佳经验让HR对你刮目相看》
---欢迎关注【Java圣斗士】,我是你们的小可爱(✪ω✪) Morty---
---专注IT职场经验、IT技术分享的灵魂写手---
---每天带你领略IT的魅力---
---期待与您陪伴!---





























