根据摩尔定律所说:集成电路上可容纳的晶体管数量每 18 个月翻一番,因此 CPU 上的晶体管数量会越来越多。
但随着时间的推移,集成电路上可容纳的晶体管数量已趋向饱和,摩尔定律也渐渐失效,因此多核 CPU 逐渐变为主流,与之相对应的多线程编程也开始变得普及和流行起来,这当然也是很久之前的事了,对于现在而言多线程编程已经成为程序员必备的职业技能了,那接下来我们就来盘一盘“线程池”这个多线程编程中最重要的话题。
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。
池化思想在计算机的应用也比较广泛,比如以下这些:
线程池的优势主要体现在以下 4 点:
同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
知道了什么是线程池以及为什要用线程池之后,我们再来看怎么用线程池。
线程池的创建方法总共有 7 种,但总体来说可分为 2 类:
ThreadPoolExecutor
创建的线程池;Executors
创建的线程池。线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors
创建的,1 种是通过 ThreadPoolExecutor
创建的):
单线程池的意义从以上代码可以看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创建的都是单线程池,那么单线程池的意义是什么呢?答:虽然是单线程池,但提供了工作队列,生命周期管理,工作线程维护等功能。
那接下来我们来看每种线程池创建的具体使用。
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
使用示例如下:
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());
});
}
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
使用示例如下:
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 个线程来执行相应的任务。
创建单个线程数的线程池,它可以保证先进先出的执行顺序。
使用示例如下:
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) {
}
});
}
}
执行结果如下:
创建一个可以执行延迟任务的线程池。
使用示例如下:
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 秒之后被执行了,符合我们的预期。
创建一个单线程的可以执行延迟任务的线程池。
使用示例如下:
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 秒之后被执行了,符合我们的预期。
创建一个抢占式执行的线程池(任务执行顺序不确定),注意此方法只有在 JDK 1.8+ 版本中才能使用。
使用示例如下:
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 个参数可供设置。
使用示例如下:
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 最多可以设置 7 个参数,如下代码所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 省略...
}
7 个参数代表的含义如下:
核心线程数,线程池中始终存活的线程数。
最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。
最大线程数可以存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
单位是和参数 3 存活时间配合使用的,合在一起用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有以下 7 种可选:
一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含以下 7 种类型:
较常用的是 LinkedBlockingQueue
和 Synchronous
,线程池的排队策略与 BlockingQueue
有关。
线程工厂,主要用来创建线程,默认为正常优先级、非守护线程。
拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
默认策略为 AbortPolicy
。
ThreadPoolExecutor 关键节点的执行流程如下:
线程池的执行流程如下图所示:
我们来演示一下 ThreadPoolExecutor 的拒绝策略的触发,我们使用 DiscardPolicy
的拒绝策略,它会忽略并抛弃当前任务的策略,实现代码如下:
public static void main(String[] args) {
// 任务的具体方法
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("当前任务被执行,执行时间:" + new Date() +
" 执行线程:" + Thread.currentThread().getName());
try {
// 等待 1s
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建线程,线程的任务队列的长度为 1
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
new ThreadPoolExecutor.DiscardPolicy());
// 添加并执行 4 个任务
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
}
我们创建了一个核心线程数和最大线程数都为 1 的线程池,并且给线程池的任务队列设置为 1,这样当我们有 2 个以上的任务时就会触发拒绝策略,执行的结果如下图所示: 从上述结果可以看出只有两个任务被正确执行了,其他多余的任务就被舍弃并忽略了。其他拒绝策略的使用类似,这里就不一一赘述了。
除了 Java 自身提供的 4 种拒绝策略之外,我们也可以自定义拒绝策略,示例代码如下:
public static void main(String[] args) {
// 任务的具体方法
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("当前任务被执行,执行时间:" + new Date() +
" 执行线程:" + Thread.currentThread().getName());
try {
// 等待 1s
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 创建线程,线程的任务队列的长度为 1
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 执行自定义拒绝策略的相关操作
System.out.println("我是自定义拒绝策略~");
}
});
// 添加并执行 4 个任务
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
}
程序的执行结果如下:
经过以上的学习我们对整个线程池也有了一定的认识了,那究竟该如何选择线程池呢?
我们来看下阿里巴巴《Java开发手册》给我们的答案:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
所以综上情况所述,我们推荐使用 ThreadPoolExecutor
的方式进行线程池的创建,因为这种创建方式更可控,并且更加明确了线程池的运行规则,可以规避一些未知的风险。
本文我们介绍了线程池的 7 种创建方式,其中最推荐使用的是 ThreadPoolExecutor
的方式进行线程池的创建,ThreadPoolExecutor
最多可以设置 7 个参数,当然设置 5 个参数也可以正常使用,ThreadPoolExecutor
当任务过多(处理不过来)时提供了 4 种拒绝策略,当然我们也可以自定义拒绝策略,希望本文的内容能帮助到你。原创不易,觉得不错就点个赞再走吧!
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8