在Java的并发编程中,线程的运用十分广泛,使用多线程可使项目的性能得到明显的改善,但是如果每次都是创建线程->执行任务->销毁线程,会造成很大的性能开销。
那么能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接获取,避免多次重复创建、销毁带来的开销。
在编程领域,比较典型的池化技术还有连接池、内存池、对象池等。
在Java.util.concurrent
包下,Executors
利用工厂模式,提供了4种线程池实现方式。
单线程的线程池,这个线程池始终只有一个线程在工作。
ExecutorService threedPool = Executors.newSingleThreadExecutor();
threedPool.execute(() -> {
System.out.println("test");
});
可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
ExecutorService threedPool = Executors.newCachedThreadPool();
threedPool.execute(() -> {
System.out.println("test");
});
创建固定数目线程的线程池。
ExecutorService threedPool = Executors.newFixedThreadPool(100);
threedPool.execute(() -> {
System.out.println("test");
});
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
ExecutorService threedPool = Executors.newScheduledThreadPool(100);
//延迟0秒,每一秒打印一句"test"
((ScheduledExecutorService) threedPool).scheduleAtFixedRate(() ->
System.out.println("test"),
0,1000, TimeUnit.MILLISECONDS);
阿里巴巴
java
开发手册有说明:
线程池不允许使用Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors
返回的线程池对象的弊端如下:
1)FixedThreadPool
和SingleThreadPool
: 允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
。
2)CachedThreadPool
和ScheduledThreadPool
: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
。
//创建newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//创建newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
可以看出,其实Executors
也是通过ThreadPoolExecutor
来创建线程池,只是默认传递了部分参数,简化了创建线程池的步骤,但同时也使得开发人员不了解其中的运行规则,容易潜伏问题。
new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue workQueue,RejectedExecutionHandler handler)
//handler可以不传,此时使用内部默认handler
ExecutorService threedPool = new ThreadPoolExecutor(1, 1,1, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
//打印"test"
threedPool.execute(()-> System.out.println("test"));
参数名 | 类 | 说明 |
---|---|---|
corePoolSize | int | 线程池维护线程的最少数量 |
maximumPoolSize | int | 线程池维护线程的最大数量 |
keepAliveTime | long | 线程池维护线程所允许的空闲时间 |
unit | TimeUnit | 线程池维护线程所允许的空闲时间的单位 |
workQueue | BlockingQueue | 线程池所使用的缓冲队列 |
handler | RejectedExecutionHandler | 线程池对拒绝任务的处理策略 |
其他参数都好理解,对于handler,或许有人会比较陌生。
假设有如下场景:线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么此时便需要通过handler所指定的策略来处理此任务。
ThreadPoolExecutor.java
:
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
前面提到过,ThreadPoolExecutor
有默认handler
,通过源码可知是AbortPolicy()
,除了AbortPolicy()
,还有CallerRunsPolicy()
、DiscardOldestPolicy()
、DiscardPolicy()
三种,说明如下:
名称 | 说明 |
---|---|
AbortPolicy() | 抛出java.util.concurrent.RejectedExecutionException异常 |
CallerRunsPolicy() | 重试添加当前的任务,他会自动重复调用execute()方法 |
DiscardOldestPolicy() | 抛出抛弃旧的任务 |
DiscardPolicy() | 抛弃当前的任务 |
启用线程池执行任务时,可以使用execute()
也可以选择submit()
,execute()
并没有返回值,一般用来执行比较简单的线程任务。如果业务较复杂的线程任务,推荐使用submit()
,submit()
可以返回一个Future
对象,通过此对象,可以得知没一个线程任务的执行结果。
public class Demo{
public static void main(String[] args) {
//handler可以不传,此时使用内部默认handler
ExecutorService threedPool = new ThreadPoolExecutor(1, 1,1, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
//打印"test"
Future future = threedPool.submit(()-> System.out.println("test"));
//通过future.isDone()判断任务此时执行情况
if(future.isDone()) {
System.out.println("任务完成");
}else {
System.out.println("任务未完成");
}
try {
//休息两秒
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
//通过future.isDone()判断任务此时执行情况
if(future.isDone()) {
System.out.println("任务完成");
}else {
System.out.println("任务未完成");
}
}
}
执行结果:
任务未完成
test
任务完成
对于十分重要的线程任务,一般可以考虑为它创建一个守护线程,一方面可以实时监测它们的执行状况,另一方面也方便销毁卡住的线程,以免资源浪费。
简单守护线程demo:
public class Demo{
public static void main(String[] args) {
//handler可以不传,此时使用内部默认handler
ExecutorService threedPool = new ThreadPoolExecutor(1, 1,1, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
//打印"test"
Future future = threedPool.submit(()-> {
try {
//假设此方法执行时间需要2秒
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("test");
});
monitorThreadTime(0,future);
//通过future.isDone()判断任务此时执行情况
if(future.isDone()) {
System.out.println("任务完成");
}else {
System.out.println("任务未完成");
monitorThreadTime(0,future);
}
}
/**
* 守护线程
* @param timeOutTime 线程守护时间
* @param future future
*/
private static void monitorThreadTime(int timeOutTime,Future<String> future){
//执行守护线程
new Thread(() -> {
//等待timeOutTime的时间,然后调用执行结果,如果没有执行完,则试着去结束
try {
TimeUnit.MICROSECONDS.sleep(timeOutTime);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//判断future是否执行完成,如果超时未完成,则试着结束线程
if(!future.isDone()){
//false表示不允许线程执行时中断,true表示允许
System.out.println("执行线程销毁");
future.cancel(true);
}
}).start();
}
}
执行结果:
java.lang.InterruptedException: sleep interrupted
任务未完成
执行线程销毁
任务完成
at java.lang.Thread.sleep(Native Method)test
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.ouyang.sericefeign.ServiceFeignApplication.lambda$main$0(ServiceFeignApplication.java:34)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
因为在sleep过程中,守护线程强行结束了主线程,所以抛出java.lang.InterruptedException: sleep interrupted
异常。
对于业务简单,且调用次数不多的线程任务,我认为可以使用Executors
创建线程池,如此可以简化开发难度,提高开发效率,业务也比较好维护。
对于复杂业务,建议开发人员根据具体开发需要,使用ThreadPoolExecutor
构建符合自己项目需求的线程池,以免留下性能安全上的漏洞和隐患。