一、线程池基础
1.1、线程池的思想
我们首先理解什么是池化技术:
池化技术指的是提前准备一些资源,在需要时可以重复使用这些预先准备的资源。
池化技术的优点主要有两个:提前准备和重复利用。
下面是池化技术常见的应用场景:
- 连接池
连接池是池化技术的一个典型应用场景。数据库连接池、线程池和连接池等资源能够帮助系统实现可伸缩性和高并发,提高系统的吞吐量。 - 对象池
在面向对象的编程中,对象池可以有效降低对象的创建和销毁开销,提高系统的性能。例如,内存池将一块连续的内存分割成多个固定大小的对象块,通过对这些对象进行复用,减少了频繁的内存分配与释放。 - 线程池
线程池是一个管理线程的池。通过维护一组可用线程,线程池可以高效地执行并发任务,减少创建和销毁线程的开销,提高系统对并发请求的响应能力。 - 缓存池
缓存池是将常用的计算结果、数据或资源存储在内存中,以加快对这些数据的访问速度。通过缓存池,系统可以减少对慢速存储介质的访问,提高系统的响应速度和性能。
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,线程也属于宝贵的系统资源。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下Java的线程池。
1.2、什么是线程池
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;
我们其实可以这样理解:
**线程池:**其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:
1.3、为什么使用线程池
使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;
1.4、使用线程池有哪些优势
线程池是一种用于管理和复用线程的机制,它提供了一种执行大量异步任务的方式,并且可以在多个任务之间合理地分配和管理系统资源。
线程池的主要优点包括:
- 改善了资源利用率,降低了线程创建和销毁的开销。
- 提高了系统响应速度,因为线程池已经预先创建好了一些线程,可以更加快速地分配资源以响应用户请求。
- 提高了代码可读性和可维护性,因为线程池将线程管理和任务执行进行了分离,可以更加方便地对其进行调整和优化。
- 可以设置线程数目上限,避免了缺乏控制的线程创建造成的系统无法承受的负载压力。
1.5 、线程池应用场景
只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池;只不过在使用线程池的时候,注意一下设置合理的线程池大小即可;
二、线程池的基本使用
2.1、线程池的总体设计
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,ExecutorService,ThreadPoolExecutor这几个类。
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
2.2、 Executors工具类快速创建线程池
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线池。
package com.thread.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsDemo {
public static void main(String[] args) {
// 创建单一线程的连接池
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
// ExecutorService threadPool = Executors.newFixedThreadPool(3);
ExecutorService threadPool = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName() + "执行了业务逻辑");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
2.3、Executors原理
上述案例中的三个方法的本质都是ThreadPoolExecutor的实例化对象,只是具体参数值不同。
2.4、Executors创建线程池种类
在Java中,常见的线程池类型主要有四种,都是通过工具类Excutors创建出来的。
2.4.1、newFixedThreadPool
创建使用固定线程数的线程池
-
核心线程数与最大线程数一样,没有救急线程
-
阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
-
适用场景:适用于任务量已知,相对耗时的任务
-
案例:
package com.thread.threadpool.typesofthreadpools;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolCase {
static class FixedThreadDemo implements Runnable{
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 2; i++) {
System.out.println(name + ":" + i);
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建一个固定大小的线程池,核心线程数和最大线程数都是3
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
executorService.submit(new FixedThreadDemo());
Thread.sleep(10);
}
executorService.shutdown();
}
}
2.4.2、newSingleThreadExecutor
单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行
-
核心线程数和最大线程数都是1
-
阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
-
适用场景:适用于按照顺序执行的任务
-
案例:
package com.thread.threadpool.typesofthreadpools;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NewSingleThreadCase {
static int count = 0;
static class Demo implements Runnable {
@Override
public void run() {
count++;
System.out.println(Thread.currentThread().getName() + ":" + count);
}
}
public static void main(String[] args) throws InterruptedException {
// 单个线程池,核心线程数和最大线程数都是1
ExecutorService exec = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
exec.execute(new Demo());
Thread.sleep(5);
}
exec.shutdown();
}
}
2.4.2、newCachedThreadPool
可缓存线程池:
-
核心线程数为0
-
最大线程数是Integer.MAX_VALUE
-
阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
-
适用场景:适合任务数比较密集,但每个任务执行时间较短的情况
-
案例:
package com.thread.threadpool.typesofthreadpools;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolCase {
static class Demo implements Runnable {
@Override
public void run() {
String name = Thread.currentThread().getName();
try {
// 修改睡眠时间,模拟线程执行需要花费的时间
Thread.sleep(100);
System.out.println(name + "执行完了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUE
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
exec.execute(new Demo());
Thread.sleep(1);
}
exec.shutdown();
}
}
2.4.3、newScheduledThreadPool
提供了“延迟”和“周期执行”功能
-
适用场景:有定时和延迟执行的任务
-
案例:
package com.thread.threadpool.typesofthreadpools;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolCase {
static class Task implements Runnable {
@Override
public void run() {
try {
String name = Thread.currentThread().getName();
System.out.println(name + ", 开始:" + new Date());
Thread.sleep(1000);
System.out.println(name + ", 结束:" + new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUE
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
System.out.println("程序开始:" + new Date());
/**
* schedule 提交任务到线程池中
* 第一个参数:提交的任务
* 第二个参数:任务执行的延迟时间
* 第三个参数:时间单位
*/
scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);
scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);
scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
Thread.sleep(5000);
// 关闭线程池
scheduledThreadPool.shutdown();
}
}
2.5、为什么不建议用Executors创建线程池
参考阿里开发手册《Java开发手册-嵩山版》
三、ThreadPoolExecutor类
线程池实现类 ThreadPoolExecutor
是 Executor
框架最核心的类。
3.1、ThreadPoolExecutor基本使用
package com.thread.threadpool;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.RejectedExecutionHandler;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建线程工厂
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize: 核心线程数
10, // maximumPoolSize: 最大线程数
60, // keepAliveTime: 非核心线程存活时间
TimeUnit.SECONDS, // unit: 非核心线程存活时间单位
new LinkedBlockingQueue<>(100), // workQueue: 等待队列
threadFactory, // threadFactory: 创建线程使用工厂
handler // handler: 饱和拒绝策略
);
// 提交任务给线程池
for (int i = 0; i < 20; i++) {
executor.submit(new Task());
}
// 关闭线程池
executor.shutdown();
}
//向线程池提交了 20 个任务,执行完后关闭线程池。每个任务会在执行时打印当前线程的名称,并睡眠 2 秒。
static class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is executing task.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3.2、线程池的核心参数详解
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize,// 最大线程数
long keepAliveTime, // 最大空闲时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 饱和处理机制
)
{ ... }
我们可以通过下面的场景理解ThreadPoolExecutor中的各个参数:
- a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);
- 在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了一个新的线程);假设该银行总共就2个窗口(核心线程数量是2);
- 紧接着在a,b客户都没有结束的情况下c客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列)等候,并告知他: 如果1、2号工作人员空出,c就可以前去办理业务;
- 此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建的线程)在大堂站着,手持pad设备给d客户办理业务;
- 假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),
- 于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待e客户;
- 最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的员工人下班.(销毁线程)
- 但是为了保证银行银行正常工作(有一个allowCoreThreadTimeout变量控制是否允许销毁核心线程,默认false),即使正式工闲着,也不得提前下班,所以1、2号工作人员继续待着(池内保持核心线程数量);
-
corePoolSize
此值是用来初始化线程池中核心线程数,当线程池中线程数< corePoolSize时,系统默认是添加一个任务才创建一个线程池。当线程数 = corePoolSize时,新任务会追加到workQueue中。 -
maximumPoolSize
表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 < maximumPoolSize时候就会再次创建新的线程。
-
keepAliveTime
非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。 -
线程池中非核心线程保持存活的时间的单位
- TimeUnit.DAYS;天
- TimeUnit.HOURS;小时
- TimeUnit.MINUTES;分钟
- TimeUnit.SECONDS;秒
- TimeUnit.MILLISECONDS; 毫秒
- TimeUnit.MICROSECONDS; 微秒
- TimeUnit.NANOSECONDS; 纳秒
-
workQueue
等待队列是存放提交但尚未执行的任务的队列。线程池的工作流程如下:
-
当一个新的任务提交到线程池时,如果当前线程池中运行的线程数小于核心线程数(
corePoolSize
),则会创建一个新的线程来执行任务。 -
如果当前线程池中运行的线程数已经达到核心线程数,但小于最大线程数(
maximumPoolSize
),则任务会被放入等待队列中。 -
当核心线程都在忙碌,且等待队列已满时,如果线程数小于最大线程数,线程池会尝试创建新的线程来执行任务。
-
如果线程数已达到最大线程数,并且等待队列也满了,则根据拒绝策略处理这个新任务。
-
-
threadFactory
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。 -
handler
corePoolSize、workQueue、maximumPoolSize都不可用的时候执行的饱和策略。
总结:
corePoolSize 核心线程数目
maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
3.3、线程池的工作流程
- 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
- 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
3.4、线程池中任务队列
栈与队列简单回顾:
栈:先进后出,后进先出
队列:先进先出
java.util.concurrent 包里的 BlockingQueue是一个接口,继承Queue接口,Queue接口继承 Collection。
- 有界队列(ArrayBlockingQueue):是一个用数组实现的有界阻塞队列,按FIFO(先进先出队列)排序。
- 无界队列(LinkedBlockingQueue):是基于链表结构的阻塞队列,按FIFO排序,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,因此在任务数量很大且任务执行时间较长时,无界队列可以保证任务不会被丢弃,但同时也会导致线程池中线程数量不断增加,可能会造成内存溢出等问题。
- 延迟队列(DelayQueue):是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。
- 优先级队列(PriorityBlockingQueue):是具有优先级的无界阻塞队列。与无界队列类似,优先级队列可以保证所有任务都会被执行,但不同的是优先级队列可以对任务进行管理和排序,确保高优先级的任务优先执行。
- 同步队列(SynchronousQueue):是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于无界队列。
用的最多是ArrayBlockingQueue和LinkedBlockingQueue:
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默认无界,支持有界 | 强制有界 |
底层是链表 | 底层是数组 |
是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队会生成新 Node | Node需要是提前创建好的 |
两把锁(头尾) | 一把锁 |
左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方式
- LinkedBlockingQueue读和写各有一把锁,性能相对较好
- ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些
3.5、线程池的拒绝策略
一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但这种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。
ThreadPoolExecutor自带的拒绝策略如下:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
- DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。
以上内置的策略均实现了RejectedExecutionHandler接口,也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义的拒绝策略处理逻辑
}
}
3.6、线程池提交execute和submit有什么区别?
在Java中,线程池中一般有两种方法来提交任务:execute() 和 submit()
- execute() 用于提交不需要返回值的任务
- submit() 用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值
package com.thread.threadpool;
import java.util.concurrent.*;
public class SubmitExample {
public static void main(String[] args) {
// 创建一个包含5个线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 使用execute()提交不需要返回值的任务
executor.execute(new RunnableTask());
// 使用submit()提交需要返回值的任务
Future<Integer> future = executor.submit(new CallableTask());
try {
// 从Future对象中获取返回值
Integer result = future.get();
System.out.println("Callable task result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// 关闭线程池
executor.shutdown();
}
// Runnable任务,不需要返回值
static class RunnableTask implements Runnable {
@Override
public void run() {
System.out.println("Runnable task is running.");
}
}
// Callable任务,需要返回值
static class CallableTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Callable task is running.");
return 42; // 返回一个计算结果
}
}
}
3.7、优雅的关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
shutdown:将线程池状态置为shutdown,并不会立即停止:
- 停止接收外部submit的任务
- 内部正在跑的任务和队列里等待的任务,会执行完
- 等到第二步完成后,才真正停止
shutdownNow:将线程池状态置为stop。一般会立即停止,事实上不一定:
- 和shutdown()一样,先停止接收外部提交的任务
- 忽略队列里等待的任务
- 尝试将正在跑的任务interrupt中断
- 返回未执行的任务列表
shutdown 和shutdownnow区别如下:
- shutdownNow:能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
- shutdown:只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。
package com.thread.threadpool;
import java.util.List;
import java.util.concurrent.*;
public class ThreadPoolShutdownExample {
public static void main(String[] args) {
// 创建一个包含5个线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交一些任务到线程池
for (int i = 0; i < 10; i++) {
executor.submit(new Task());
}
// 使用shutdown方法关闭线程池
System.out.println("尝试使用shutdown方法关闭线程池...");
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("线程池未能在指定时间内终止。");
List<Runnable> droppedTasks = executor.shutdownNow();
System.out.println("线程池被立即关闭。有 " + droppedTasks.size() + " 个任务未执行。");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 重新创建一个线程池
executor = Executors.newFixedThreadPool(5);
// 提交一些任务到线程池
for (int i = 0; i < 10; i++) {
executor.submit(new Task());
}
// 使用shutdownNow方法关闭线程池
System.out.println("尝试使用shutdownNow方法关闭线程池...");
List<Runnable> notExecutedTasks = executor.shutdownNow();
System.out.println(notExecutedTasks.size() + " 个任务未执行。");
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("线程池未能在指定时间内终止。");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程池已关闭。");
}
static class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行任务。");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 被中断。");
}
}
}
}
3.8、线程池异常处理
在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。
常见的异常处理方式:
package com.threadpool;
import java.util.List;
import java.util.concurrent.*;
public class ThreadPoolExceptionHandling {
public static void main(String[] args) {
// 使用自定义的ThreadFactory设置UncaughtExceptionHandler
ThreadFactory threadFactory = new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + " 捕获到未检测的异常: " + e.getMessage());
}
});
return t;
}
};
// 创建自定义ThreadPoolExecutor
ExecutorService executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), threadFactory) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t != null) {
System.out.println("任务执行时抛出的异常: " + t.getMessage());
}
if (r instanceof Future<?>) {
try {
((Future<?>) r).get();
} catch (CancellationException ce) {
System.out.println("任务被取消: " + ce.getMessage());
} catch (InterruptedException ie) {
System.out.println("任务被中断: " + ie.getMessage());
} catch (ExecutionException ee) {
System.out.println("任务执行时抛出的异常: " + ee.getCause().getMessage());
}
}
}
};
// 提交Runnable任务使用try-catch捕获异常
executor.execute(new Runnable() {
public void run() {
try {
throw new RuntimeException("Runnable任务异常");
} catch (RuntimeException e) {
System.out.println("Runnable任务捕获到异常: " + e.getMessage());
}
}
});
// 提交Callable任务,使用Future.get()接受异常
Future<Integer> future = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
throw new Exception("Callable任务异常");
}
});
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("Callable任务捕获到异常: " + e.getCause().getMessage());
}
// 关闭线程池
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
System.out.println("线程池未能在指定时间内终止。");
List<Runnable> droppedTasks = executor.shutdownNow();
System.out.println("线程池被立即关闭。有 " + droppedTasks.size() + " 个任务未执行。");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.9、线程池的状态
线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED
//线程池状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
线程池各个状态切换图:
RUNNING
- 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
- 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
- 调用线程池的shutdownNow()方法,可以切换到STOP状态;
SHUTDOWN
- 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
- 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;
STOP
- 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
- 线程池中执行的任务为空,进入TIDYING状态;
TIDYING
- 该状态表明所有的任务已经运行终止,记录的任务数量为0。
- terminated()执行完毕,进入TERMINATED状态
TERMINATED
- 该状态表示线程池彻底终止
3.10、线程池的参数如何设置
首先线程池中有7个参数,其中最重要的 是核心线程数和最大线程池数,当然这些参数没有固定的一个数值, 不同业务场景线程任务肯定参数不一样,可以根据你的业务类型来针对性回答:
如果任务主要是IO密集型的, 比如存在大量读写操作 像(大量文件读写、 大量网络请求, 频繁的数据库访问等操作)
那核心线程数就可以设置为CPU核心数的两倍左右 ,因为涉及的 I/O 操作通常会导致 线程大部分的时间会处于阻塞状态,所以线程数就应当设置稍大一些。
那 最大线程数可以设置为CPU核心数的四倍左右以便在出现大量并发 I/O 操作时能够有足够的线程来处理。
如果任务是CPU密集型的, 像 一些计算操作, 通常就需要大量的计算资源,因为计算操作会长时间占用 CPU 资源,线程过多会导致cpu占用高,所以参数不宜设置过大 可以将:
核心线程数设置为 CPU 核心数+1,以充分利用 CPU 资源。
最大线程数可以设置为CPU核心数的2倍左右,因为 CPU 密集型任务不会涉及 I/O 等待。
当然,以上是一些经验数值,实际情况我们需要根据服务器的资源占用情况,结合运行时的 cpu、内存等指标进行调整, 还要结合压测工具得到一个比较合理的范围。
总结:
在设置核心线程数之前,需要先熟悉一些执行线程池执行任务的类型
- IO密集型任务
一般来说:文件读写、DB读写、网络请求等
推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)
- CPU密集型任务
一般来说:计算型代码、Bitmap转换、Gson转换等
推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)
java代码查看CPU核数