在 Java 语言中,并发编程都是通过创立线程池来实现的,而线程池的创立形式也有很多种,每种线程池的创立形式都对应了不同的应用场景,总体来说线程池的创立能够分为以下两类:
- 通过 ThreadPoolExecutor 手动创立线程池。
- 通过 Executors 执行器主动创立线程池。
而以上两类创立线程池的形式,又有 7 种具体实现办法,这 7 种实现办法别离是:
- Executors.newFixedThreadPool:创立一个固定大小的线程池,可管制并发的线程数,超出的线程会在队列中期待。
- Executors.newCachedThreadPool:创立一个可缓存的线程池,若线程数超过解决所需,缓存一段时间后会回收,若线程数不够,则新建线程。
- Executors.newSingleThreadExecutor:创立单个线程数的线程池,它能够保障先进先出的执行程序。
- Executors.newScheduledThreadPool:创立一个能够执行提早工作的线程池。
- Executors.newSingleThreadScheduledExecutor:创立一个单线程的能够执行提早工作的线程池。
- Executors.newWorkStealingPool:创立一个抢占式执行的线程池(工作执行程序不确定)【JDK 1.8 增加】。
- ThreadPoolExecutor:手动创立线程池的形式,它创立时最多能够设置 7 个参数。
接下来咱们别离来看这 7 种线程池的具体应用。
1.FixedThreadPool
创立一个固定大小的线程池,可管制并发线程数。
应用 FixedThreadPool 创立 2 个固定大小的线程池,具体实现代码如下:
public static void fixedThreadPool() {
// 创立 2 个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 创立工作
Runnable runnable = new Runnable() {
@Override
public void run() {System.out.println("工作被执行, 线程:" + Thread.currentThread().getName());
}
};
// 线程池执行工作(一次增加 4 个工作)
// 执行工作的办法有两种:submit 和 execute
threadPool.submit(runnable); // 执行形式 1:submit
threadPool.execute(runnable); // 执行形式 2:execute
threadPool.execute(runnable);
threadPool.execute(runnable);
}
以上程序的执行后果如下图所示:
如果感觉以上办法比拟繁琐,还用应用以下简略的形式来实现线程池的创立和应用:
public static void fixedThreadPool() {
// 创立线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 执行工作
threadPool.execute(() -> {System.out.println("工作被执行, 线程:" + Thread.currentThread().getName());
});
}
2.CachedThreadPool
创立一个可缓存的线程池,若线程数超过工作所需,那么多余的线程会被缓存一段时间后才被回收,若线程数不够,则会新建线程。
CachedThreadPool 应用示例如下:
public static void cachedThreadPool() {
// 创立线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
// 执行工作
for (int i = 0; i < 10; i++) {threadPool.execute(() -> {System.out.println("工作被执行, 线程:" + Thread.currentThread().getName());
try {TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {}});
}
}
以上程序的执行后果如下图所示:
从上述后果能够看出,线程池创立了 10 个线程来执行相应的工作。
应用场景
CachedThreadPool 是依据短时间的任务量来决定创立的线程数量的,所以它适宜短时间内有突发大量工作的解决场景。
3.SingleThreadExecutor
创立单个线程的线程池,它能够保障先进先出的执行程序。
SingleThreadExecutor 应用示例如下:
public static void singleThreadExecutor() {
// 创立线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 执行工作
for (int i = 0; i < 10; i++) {
final int index = i;
threadPool.execute(() -> {System.out.println(index + ": 工作被执行");
try {TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {}});
}
}
以上程序的执行后果如下图所示:
单个线程的线程池有什么意义?
单个线程的线程池相比于线程来说,它的长处有以下 2 个:
- 能够复用线程:即便是单个线程池,也能够复用线程。
-
提供了工作治理性能:单个线程池也领有工作队列,在工作队列能够存储多个工作,这是线程无奈实现的,并且当工作队列满了之后,能够执行回绝策略,这些都是线程不具备的。
4.ScheduledThreadPool
创立一个能够执行提早工作的线程池。
应用示例如下:public static void scheduledThreadPool() { // 创立线程池 ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5); // 增加定时执行工作(1s 后执行) System.out.println("增加工作, 工夫:" + new Date()); threadPool.schedule(() -> {System.out.println("工作被执行, 工夫:" + new Date()); try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {}}, 1, TimeUnit.SECONDS); }
以上程序的执行后果如下图所示:
从上述后果能够看出,工作在 1 秒之后被执行了,实现了提早 1s 再执行工作。5.SingleThreadScheduledExecutor
创立一个单线程的能够执行提早工作的线程池,此线程池能够看作是 ScheduledThreadPool 的单线程池版本。
它的应用示例如下:public static void SingleThreadScheduledExecutor() { // 创立线程池 ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor(); // 增加定时执行工作(2s 后执行) System.out.println("增加工作, 工夫:" + new Date()); threadPool.schedule(() -> {System.out.println("工作被执行, 工夫:" + new Date()); try {TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {}}, 2, TimeUnit.SECONDS); }
以上程序的执行后果如下图所示:
从上述后果能够看出,工作在 2 秒之后被执行了。6.newWorkStealingPool
创立一个抢占式执行的线程池(工作执行程序不确定),此办法是 JDK 1.8 版本新增的,因而只有在 JDK 1.8 以上的程序中能力应用。
newWorkStealingPool 应用示例如下:public static void workStealingPool() { // 创立线程池 ExecutorService threadPool = Executors.newWorkStealingPool(); // 执行工作 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> {System.out.println(index + "被执行, 线程名:" + Thread.currentThread().getName()); }); } // 确保工作执行实现 while (!threadPool.isTerminated()) {}}
以上程序的执行后果如下图所示:
从上述后果能够看出,工作的执行程序是不确定的,因为它是抢占式执行的。7.ThreadPoolExecutor
ThreadPoolExecutor 是最原始、也是最举荐的手动创立线程池的形式,它在创立时最多提供 7 个参数可供设置。
ThreadPoolExecutor 应用示例如下:public static void myThreadPoolExecutor() { // 创立线程池 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10)); // 执行工作 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> {System.out.println(index + "被执行, 线程名:" + Thread.currentThread().getName()); try {Thread.sleep(1000); } catch (InterruptedException e) {e.printStackTrace(); } }); } }
以上程序的执行后果如下图所示:
ThreadPoolExecutor 相比于其余创立线程池的劣势在于,它能够通过参数来管制最大工作数和回绝策略,让线程池的执行更加通明和可控,所以在阿里巴巴《Java 开发手册》是这样规定的:【强制要求】线程池不容许应用 Executors 去创立,而是通过 ThreadPoolExecutor 的形式,这样的解决形式让写的同学更加明确线程池的运行规定,躲避资源耗尽的危险。
阐明:Executors 返回的线程池对象的弊病如下:
1)FixedThreadPool 和 SingleThreadPool:容许的申请队列长度为 Integer.MAX_VALUE,可能会沉积大量的申请,从而导致 OOM。
2)CachedThreadPool:容许的创立线程数量为 Integer.MAX_VALUE,可能会创立大量的线程,从而导致 OOM。
总结
线程池的创立形式总共有以下 7 种:
- Executors.newFixedThreadPool:创立一个固定大小的线程池,可管制并发的线程数,超出的线程会在队列中期待。
- Executors.newCachedThreadPool:创立一个可缓存的线程池,若线程数超过解决所需,缓存一段时间后会回收,若线程数不够,则新建线程。
- Executors.newSingleThreadExecutor:创立单个线程数的线程池,它能够保障先进先出的执行程序。
- Executors.newScheduledThreadPool:创立一个能够执行提早工作的线程池。
- Executors.newSingleThreadScheduledExecutor:创立一个单线程的能够执行提早工作的线程池。
- Executors.newWorkStealingPool:创立一个抢占式执行的线程池(工作执行程序不确定)【JDK 1.8 增加】。
- ThreadPoolExecutor:手动创立线程池的形式,它创立时最多能够设置 7 个参数。
而 线程池的创立举荐应用最初一种 ThreadPoolExecutor 的形式来创立,因为应用它能够明确线程池的运行规定,躲避资源耗尽的危险。
是非审之于己,毁誉听之于人,得失安之于数。
公众号:Java 面试真题解析
面试合集:https://gitee.com/mydb/interview