线程池:首先创建一些线程,它们的集合称为线程池。目的类似于,数据库连接池。
创建线程池:Executors工具类创建、手动创建(ThreadPoolExecutor)两种;生产中一般使用手动创建,因为
ThreadPoolExecutor参数介绍
- int corePoolSize
- int maximumPoolSize
- long keepAliveTime
- TimeUnit unit
- BlockingQueue<Runnable> workQueue
- ThreadFactory threadFactory
- RejectedExecutionHandler handler
拒绝策略拓展
JDK中拒绝策略有四种,但这四种拒绝策略并不是很适用;工作中需要自定义,因为比如:默认的四种不会存盘,只会在日志中进行输出,用户的数据就会丢失
JDK中的实现
AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
CallerRunsPolicy 让调用者运行任务
DiscardPolicy 放弃本次任务
DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
其它框架也提供了实现
- Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
- Netty 的实现,是创建一个新线程来执行任务
- ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
- PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略线程上下文切换
阻塞队列拓展
Java阻塞队列 BlockingQueue 深度解析-优快云博客
线程池的执行流程
核心线程--> 阻塞队列 --> 救急线程;
默认工厂方法中的线程池
Executors中包含很多默认的方法,队列长度或者线程数不做限制,会有 OOM 风险。因此不推荐使用。
说白了就是:使用有界队列,控制线程创建数量
newFixedThreadPool
核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间 阻塞队列是无界的,可以放任意数量的任务;
适用于任务量已知,相对耗时的任务
newCachedThreadPool
核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着 全部都是救急线程(60s 后可以回收)
实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交 货)
适用于整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线 程。 适合任务数比较密集,但每个任务执行时间较短的情况
newSingleThreadExecutor
自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一 个线程,保证池的正常工作
Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改 FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因 此不能调用 ThreadPoolExecutor 中特有的方法
Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
package concurrent;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j(topic = "c.TestExecutors")
public class TestExecutors {
public static void main(String[] args) throws InterruptedException {
test2();
}
public static void test2() {
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(() -> {
log.debug("1");
int i = 1 / 0;
});
pool.execute(() -> {
log.debug("2");
});
pool.execute(() -> {
log.debug("3");
});
}
private static void test1() {
ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
private AtomicInteger t = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "mypool_t" + t.getAndIncrement());
}
});
pool.execute(() -> {
log.debug("1");
});
pool.execute(() -> {
log.debug("2");
});
pool.execute(() -> {
log.debug("3");
});
}
}
结果
即便线程1发生了异常,但是后续的线程2 和 3 也可以打印出来
newScheduledThreadPool
Creates a thread pool that can schedule commands to run after a given delay, or to execute periodically.
两个常用方法:
scheduleAtFixedRate
scheduleWithFixedDelay
常用方法
提交任务
submit()
关闭线程池
shutdown() 已提交任务会执行完
shutdownNow() interrupt方式中断正在执行的任务
调用完 shutdownNow
和 shuwdown
方法后,并不代表线程池已经完成关闭操作,它只是异步的通知线程池进行关闭处理。如果要同步等待线程池彻底关闭后才继续往下执行,需要调用awaitTermination
方法进行同步等待。
在调用 awaitTermination()
方法时,应该设置合理的超时时间,以避免程序长时间阻塞而导致性能问题。另外。由于线程池中的任务可能会被取消或抛出异常,因此在使用 awaitTermination()
方法时还需要进行异常处理。awaitTermination()
方法会抛出 InterruptedException
异常,需要捕获并处理该异常,以避免程序崩溃或者无法正常退出。
池大小
CPU密集型运算
线程数 = CPU核心数+1
解释:+1是保证XXXXX
IO密集型运算
线程数 = 核数 * 期望CPU利用率 * 总时间(CPU计算时间 + 等待时间)/ CPU计算时间
例如,4核CPU计算时间是50%,其他等待时间 50%, 期望CPU被100%利用,套用公式:
4 * 100% * 100% / 50% = 4 * 2 = 8
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。
IO 密集型:但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
正确处理线程池异常
主要介绍几种处理异常的方式,根据实际需求选择.如果不专门处理,即便异常的话可能也发现不了。如下所示
默认处理方式
直接说结论,需要分两种情况:
- 使用
execute()
提交任务:当任务通过execute()
提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。 - 使用
submit()
提交任务:对于通过submit()
提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由submit()
返回的Future
对象中。当调用Future.get()
方法时,可以捕获到一个ExecutionException
。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。
简单来说:使用execute()
时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()
时,异常被封装在Future
中,线程继续复用。
线程内部处理
Callable + Future
通过Future的 get()可以获取到具体的异常信息
三种情况代码如下
package concurrent;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j(topic = "c.ThreadPoolException")
public class ThreadPoolException {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*默认的情况*/
log.debug("===========method1 start===========");
method1();
/*内部处理*/
log.debug("===========method2 start===========");
method2();
/*外部处理*/
log.debug("===========method3 start===========");
method3();
}
private static void method1() {
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
log.debug("task");
int i = 1 / 0;
return "线程体执行完了";
});
log.debug("task 执行完了");
}
private static void method2() {
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(() -> {
log.debug("task");
try {
log.debug("task");
int i = 1 / 0;
} catch (Exception e) {
log.error("try catch捕获error:", e);
}
return "线程体执行完了";
});
log.debug("task 执行完了");
}
private static void method3() throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<String> future = pool.submit(() -> {
log.debug("task");
int i = 1 / 0;
return "线程体执行完了";
});
String s = future.get();
log.debug("线程执行完了的输出: {}", s);
log.debug("task 执行完了");
}
}
使用规范
正确声明线程池
线程池必须手动通过 ThreadPoolExecutor
的构造函数来声明,避免使用Executors
类创建线程池,会有 OOM 风险
命名
初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题
不同类别的业务用不同的线程池
线程池尽量不要放耗时任务
线程池本身的目的是为了提高任务执行效率,避免因频繁创建和销毁线程而带来的性能开销。如果将耗时任务提交到线程池中执行,可能会导致线程池中的线程被长时间占用,无法及时响应其他任务,甚至会导致线程池崩溃或者程序假死。
因此,在使用线程池时,我们应该尽量避免将耗时任务提交到线程池中执行。对于一些比较耗时的操作,如网络请求、文件读写等,可以采用 CompletableFuture
等其他异步操作的方式来处理,以避免阻塞线程池中的线程。