创建和销毁线程非常损耗性能,那有没有可能复用一些已经被创建好的线程呢?答案是肯定的,那就是线程池。
另外,线程的创建需要开辟虚拟机栈、本地方法栈、程序计数器等线程私有的内存空间,在线程销毁时需要回收这些系统资源,频繁地创建销毁线程会浪费大量资源,而通过复用已有线程可以更好地管理和协调线程的工作。
线程池主要解决两个问题:
一、 当执行大量异步任务时线程池能够提供很好的性能。
二、 线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程等。
——《Java并发编程之美》
线程池体系
解释说明:
- Executor 是线程池最顶层的接口,在 Executor 中只有一个 execute 方法,用于执行任务。至于线程的创建、调度等细节由子类实现。
- ExecutorService 继承并拓展了 Executor,在 ExecutorService 内部提供了更全面的任务提交机制以及线程池关闭方法。
- ThreadPoolExecutor 是 ExecutorService 的默认实现,所谓的线程池机制也大多封装在此类当中。
- ScheduledExecutorService 继承自 ExecutorService,增加了定时任务相关方法。
- ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,并实现了 ScheduledExecutorService 接口。
- ForkJoinPool 是一种支持任务分解的线程池,一般要配合可分解任务接口 ForkJoinTask 来使用。
创建线程池
为了开发者可以更方便地使用线程池,JDK 中给我们提供了一个线程池的工厂类—Executors。在 Executors 中定义了多个静态方法,用来创建不同配置的线程池。常见有以下几种。
本文涉及到的源码来源于Java JDK15.0.2
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按先进先出的顺序执行。
源码
/**
* Creates an Executor that uses a single worker thread operating
* off an unbounded queue. (Note however that if this single
* thread terminates due to a failure during execution prior to
* shutdown, a new one will take its place if needed to execute
* subsequent tasks.) Tasks are guaranteed to execute
* sequentially, and no more than one task will be active at any
* given time. Unlike the otherwise equivalent
* {@code newFixedThreadPool(1)} the returned executor is
* guaranteed not to be reconfigurable to use additional threads.
*
* @return the newly created single-threaded Executor
*/
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
示例
public class TestnewSingleThreadExecutor {
public static void main(String[] args) {
//newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按先进先出的顺序执行。
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
int len = 5;
for (int i = 0; i < len; i++) {
final int taskID = i;
// 向线程池中提交任务
newSingleThreadExecutor.submit(new Runnable() {
@Override
public void run() {
PrintlnUtils.println("线程: "+Thread.currentThread().getName()+" 正在执行任务 taskID: "+taskID);
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行上述代码结果如下,可以看出所有的 task 始终是在同一个线程中被执行的。
//线程: pool-1-thread-1 正在执行任务 taskID: 0
//
//线程: pool-1-thread-1 正在执行任务 taskID: 1
//
//线程: pool-1-thread-1 正在执行任务 taskID: 2
//
//线程: pool-1-thread-1 正在执行任务 taskID: 3
//
//线程: pool-1-thread-1 正在执行任务 taskID: 4
}
}
newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
源码
/**
* Creates a thread pool that creates new threads as needed, but
* will reuse previously constructed threads when they are
* available. These pools will typically improve the performance
* of programs that execute many short-lived asynchronous tasks.
* Calls to {@code execute} will reuse previously constructed
* threads if available. If no existing thread is available, a new
* thread will be created and added to the pool. Threads that have
* not been used for sixty seconds are terminated and removed from
* the cache. Thus, a pool that remains idle for long enough will
* not consume any resources. Note that pools with similar
* properties but different details (for example, timeout parameters)
* may be created using {@link ThreadPoolExecutor} constructors.
*
* @return the newly created thread pool
*/
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
示例
public class TestnewCachedThreadPool {
public static void main(String[] args) {
//newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
int len = 5;
for (int i = 1; i <= len; i++) {
final int taskID = i;
/*
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
// 向线程池中提交任务
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
PrintlnUtils.println("线程: " + Thread.currentThread().getName() + " 正在执行任务 taskID: " + taskID);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
newCachedThreadPool.shutdown();
//线程: pool-1-thread-5 正在执行任务 taskID: 5
//
//线程: pool-1-thread-2 正在执行任务 taskID: 2
//
//线程: pool-1-thread-3 正在执行任务 taskID: 3
//
//线程: pool-1-thread-1 正在执行任务 taskID: 1
//
//线程: pool-1-thread-4 正在执行任务 taskID: 4
//从上面日志中可以看出,缓存线程池会创建新的线程来执行任务。但是如果将代码修改一下,在提交任务之前休眠 1 秒钟,如下:
//再次执行则打印日志同 SingleThreadPool 一模一样,原因是提交的任务只需要 500 毫秒即可执行完毕,
// 休眠 1 秒导致在新的任务提交之前,线程 “pool-1-thread-1” 已经处于空闲状态,可以被复用执行任务。
//线程: pool-1-thread-1 正在执行任务 taskID: 1
//
//线程: pool-1-thread-1 正在执行任务 taskID: 2
//
//线程: pool-1-thread-1 正在执行任务 taskID: 3
//
//线程: pool-1-thread-1 正在执行任务 taskID: 4
//
//线程: pool-1-thread-1 正在执行任务 taskID: 5
}
}
newFixedThreadPool
创建一个固定数目的、可重用的线程池。
源码
/**
* Creates a thread pool that reuses a fixed number of threads
* operating off a shared unbounded queue. At any point, at most
* {@code nThreads} threads will be active processing tasks.
* If additional tasks are submitted when all threads are active,
* they will wait in the queue until a thread is available.
* If any thread terminates due to a failure during execution
* prior to shutdown, a new one will take its place if needed to
* execute subsequent tasks. The threads in the pool will exist
* until it is explicitly {@link ExecutorService#shutdown shutdown}.
*
* @param nThreads the number of threads in the pool
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
示例
public class TestnewFixedThreadPool {
public static void main(String[] args) {
//newFixedThreadPool 创建一个固定数目的、可重用的线程池。
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
int len = 10;
for (int i = 1; i <= len; i++) {
final int taskID = i;
// 向线程池中提交任务
newFixedThreadPool.submit(new Runnable() {
@Override
public void run() {
try {
PrintlnUtils.println("线程: " + Thread.currentThread().getName() + " 正在执行任务 taskID: " + taskID);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//上述代码创建了一个固定数量 3 的线程池,因此虽然向线程池提交了 10 个任务,但是这 10 个任务只会被 3 个线程分配执行,执行效果如下:
//线程: pool-1-thread-1 正在执行任务 taskID: 1
//
//线程: pool-1-thread-3 正在执行任务 taskID: 3
//
//线程: pool-1-thread-2 正在执行任务 taskID: 2
//
//线程: pool-1-thread-3 正在执行任务 taskID: 4
//
//线程: pool-1-thread-2 正在执行任务 taskID: 6
//
//线程: pool-1-thread-1 正在执行任务 taskID: 5
//
//线程: pool-1-thread-3 正在执行任务 taskID: 7
//
//线程: pool-1-thread-2 正在执行任务 taskID: 8
//
//线程: pool-1-thread-1 正在执行任务 taskID: 9
//
//线程: pool-1-thread-2 正在执行任务 taskID: 10
}
}
newScheduledThreadPool
创建一个定时线程池,支持定时及周期性任务执行。
源码
/**
* Creates a thread pool that can schedule commands to run after a
* given delay, or to execute periodically.
* @param corePoolSize the number of threads to keep in the pool,
* even if they are idle
* @return the newly created scheduled thread pool
* @throws IllegalArgumentException if {@code corePoolSize < 0}
*/
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
示例
public class TestnewScheduledThreadPool {
public static void main(String[] args) {
//newScheduledThreadPool 创建一个定时线程池,支持定时及周期性任务执行。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
//延迟1秒执行,每隔一秒执行一次
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Date date = new Date();
PrintlnUtils.println("线程 "+Thread.currentThread().getName()+" 报时:"+date);
}
},500,500, TimeUnit.MILLISECONDS);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 使用shutdown关闭定时任务
scheduledExecutorService.shutdown();
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:42 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:43 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:43 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:44 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:44 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:45 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:45 CST 2021
//
//线程 pool-1-thread-2 报时:Sat Jan 09 09:00:46 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:46 CST 2021
//
//线程 pool-1-thread-1 报时:Sat Jan 09 09:00:47 CST 2021
}
}
上面这几种就是常用到的线程池使用方式,但是,在 阿里Java开发手册 中已经严禁使用 Executors 来创建线程池,这是为什么呢?
看一下newSingleThreadExecutor 和 newFixedThreadPool() 的具体实现,如下:
可以看到传入的是一个无界的阻塞队列,理论上可以无限添加任务到线程池。当核心线程执行时间很长(比如 sleep10s),则新提交的任务还在不断地插入到阻塞队列中,最终造成
OOM。
看一下 newCachedThreadPool 的实现:
可以看到,缓存线程池的最大线程数为 Integer 最大值。当核心线程耗时很久,线程池会尝试创建新的线程来执行提交的任务,当内存不足时就会报无法创建线程的错误。
以上两种情况如果发生在生产环境将会是致命的,从阿里手册中严禁使用 Executors 的态度上也能看出,阿里也是经历过血淋淋的教训。
总结
线程池是一把双刃剑,使用得当会使代码如虎添翼;但是使用不当将会造成重大性灾难。而剑柄是握在开发者手中,只有理解线程池的运行原理,熟知它的工作机制与使用场景,才会使这把双刃剑发挥更好的作用。
欢迎关注我的公众号,不定期推送优质的文章,
微信扫一扫下方二维码即可关注。