前段时间在公司分享过两次java线程池的实现原理,但是貌似大家理解的不是很深入,在应用的时候发现被培训的人并没有抓住核心点,并不理解线程池的核心原理,所以再完整的梳理一遍源码,希望可以帮助大家理解线程池的核心逻辑。本篇先仅讲解一下Executors创建线程池的示例及适用场景,线程池的原理浅析请参考我的另一篇文章https://blog.csdn.net/leandzgc/article/details/103111658
固定大小线程池 Executors.newFixedThreadPool
单线程线程池 Executors.newSingleThreadExecutor
缓存线程池 Executors.newCachedThreadPool
定时线程池 Executors.newScheduledThreadPool
偷窃线程池 Executors.newWorkStealingPool(jdk1.8新增,还没仔细研究,不深入展开讲解)
原生线程池 new ThreadPoolExecutor()
其实通过ExecutorService创建的前4种线程池是不推荐使用的,这一规范在阿里开发手册中也可以找到。原因是因为:他们的底层用的也是ThreadPoolExecutor,且特殊情况下默认参数会导致程序异常,非常不稳定。所以除非全程逻辑可控,否则请使用原生的ThreadPoolExecutor来创建多线程。
适用场景:定长线程池,适合任务量并发及执行耗时相对平稳的场景(生产和消费速度相对平稳且对等,仅平稳但不对等也不适合直接使用该方法)。
用法示例:
package com.thunisoft.test.ThreadPoolTest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
public class ThreadPoolTest {
private static int N_NUM_10 = 10;
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void testFixedThreadPool() {
/**
* 业务场景假设:该应用为后台应用,主要用于定期检索消息队列中待发送短信,并把未发送的短信通过接口发送给短信平台。
* 同时因短信平台方处理效率有限,只支持单应用10个绝对并发,强制要求调用方限制调用连接数。
*/
//1、创建线程池--因为要限制最大活跃线程数数量,所以可以选择使用固定线程池,保证活跃线程数不会超过阈值(不考虑各独立线程处理各自未与短信平台交互时的相对并发,那样例子写起来就复杂了)
//1.1、可使用最简单的方法
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(N_NUM_10);
//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
// ExecutorService fixedThreadPoolCustomThreadName = Executors.newFixedThreadPool(N_NUM_10,
// // 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
// new BasicThreadFactory.Builder().namingPattern("sendMsg-%d").build());
//2、使用线程池
// 省略从队列中获取数据的步骤,这里应该触发很多次submit方法
fixedThreadPool.submit(new Runnable() {
// 使用匿名内部类来创建线程
public void run() {
// 这里直接从队列里面获取需要发送的短信,并根据返回结果放入发送成功的队列或者放入发送失败的队列,逻辑直接结束
// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
}
});
}
}
核心参数解析:
int nThreads:线程数大小,用于控制线程池的核心线程数及最大线程数。为啥就一个参数?对,没错,通过该方法创建的线程池核心线程数及最大线程数是一样的。
ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。
其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数与最大线程数一致,空闲存活时间为0,构建了一个默认的队列。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
// ...
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue(),
threadFactory);
}
注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的待处理任务实例被放入默认队列中,而队列构建时未给最大值,可存放Integer.MAX_VALUE个对象,引发OOM。
适用场景:单个活跃线程线程池,适合长期固定循环执行的场景(不一定是相同的线程任务对象,只要这一组任务需要统一管理,且还不想不可控的创建N多线程来浪费系统资源,就可以使用该方法创建线程池)。
用法示例:
package com.thunisoft.test.ThreadPoolTest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
public class ThreadPoolTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void testSingleThreadPool() {
/**
* 业务场景假设:该应用为后台应用,主要用于分批从消息队列中获取数据,并解析并持久化到指定的数据库表中。
* 业务数据的类型不固定,只是遵守了同一组协议,所以任务的处理对象可能是不同类型的(例如有json的,有xml的,有需要调用httpApi二次获取的等)
*/
//1、创建线程池--其实适用场景跟new Thread差不多,但是线程池具有崩溃自动恢复机制,而且人家还有个队列来管理任务,这些是直接实例化Thread比不了的
//1.1、可使用最简单的方法
ExecutorService signleThreadPool = Executors.newSingleThreadExecutor();
//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
// ExecutorService signleThreadPoolCustomThreadName = Executors.newSingleThreadExecutor(
// // 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
// new BasicThreadFactory.Builder().namingPattern("td-%d").build());
//2、使用线程池
// 省略从队列中获取数据的步骤,这里应该触发很多次submit方法
signleThreadPool.submit(new Runnable() {
// 使用匿名内部类来创建线程
public void run() {
// 这里直接从队列里面获取需要处理的数据,清洗后持久化到数据库,逻辑直接结束
// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
}
});
}
}
核心参数解析:
ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。
其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数与最大线程数都是1,空闲存活时间为0,构建了一个默认的队列。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
// ...
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue(),
threadFactory));
}
注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的待处理任务实例被放入默认队列中,而队列构建时未给最大值,可存放Integer.MAX_VALUE个对象,引发OOM。
适用场景:缓存线程池,适合任务量并发相对稳定且执行耗时较短的场景(因为该方法创建的线程池没有核心线程数,且线程的空闲活跃时间为60秒。如果并发和执行耗时波动都很大,个人感觉用这个方法也不太好,因为线程池同样会频繁的创建和销毁线程对象)。
用法示例:
package com.thunisoft.test.ThreadPoolTest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
public class ThreadPoolTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void testCachedThreadPool() {
/**
* 业务场景假设:该应用为后台应用,主要用于将订单状态同步到数据库中。
*/
//1、创建线程池--通过主键更新速度本身很快(毫秒级,到达秒级就要确认索引或分库分表是否合理了),且订单业务本身在某一时间段内是相对稳定的
// (没找到数据支撑,一般应该是在中午休息时间或下班时间及各活动时间订单量会飙升,其他时间段始终处于一个相对稳定的水平)
// 这时候就特别适合使用该方法,在某个时间段内始终在复用资源,不会频繁的创建线程、销毁线程
//1.1、可使用最简单的方法
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
// ExecutorService cachedThreadPoolCustomThreadName = Executors.newCachedThreadPool(
// // 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
// new BasicThreadFactory.Builder().namingPattern("sendMsg-%d").build());
//2、使用线程池
// 省略从队列中获取数据的步骤,这里应该触发很多次submit方法
cachedThreadPool.submit(new Runnable() {
// 使用匿名内部类来创建线程
public void run() {
// 这里直接从队列里面获取需要处理的数据,直接生成数据并持久化到数据库,逻辑直接结束
// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
}
});
}
}
核心参数解析:
ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。
其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数是0,最大线程数是Integer.MAX_VALUE,空闲存活时间为60秒,构建了一个阻塞队列。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
// ...
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue(),
threadFactory);
}
注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的线程实例,最大可达Integer.MAX_VALUE个线程实例,引发OOM或栈溢出。
其实还有个Executors.newSingleThreadScheduledExecutor()和Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory),单核心定时线程池,只是把核心线程数给了个默认值1而已
适用场景:延迟线程池,适合需要循环延迟执行的场景。
用法示例:
package com.thunisoft.test.ThreadPoolTest;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
public class ThreadPoolTest {
private static int N_NUM_10 = 10;
private static int N_NUM_60 = 60;
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void testCachedThreadPool() {
/**
* 业务场景假设:该应用为后台应用,程序启动后,每隔10秒校验一下指定应用端口的存活情况。
*/
//1、创建线程池--该方法可实现延迟执行和延迟执行+定时循环两种方式,具体选择哪一种请自行根据实际业务选择
//1.1、可使用最简单的方法
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(N_NUM_10);
//1.2、还可以指定生成的线程名称,自定义一个线程工厂即可
// ScheduledExecutorService scheduledThreadPoolCustomThreadName = Executors.newScheduledThreadPool(N_NUM_10,
// // 这里可以在实例化线程时传递自己的参数,例如给每个业务组的线程都增加一个前缀,在运行时很容易区分出来所属线程组
// new BasicThreadFactory.Builder().namingPattern("cs-%d").build());
//2、使用线程池
// 省略从队列中获取数据的步骤,这里应该触发很多次schedule方法
// 特殊之处在于ScheduledExecutorService继承了ExecutorService,如果依然调用submit则失去了定时执行的特性
// 这里直接从队列里面获取需要处理的机器,并进行存活情况校验及存活或不存活时的处理,逻辑直接结束
// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
// 使用匿名内部类来创建线程
public void run() {
// 这里直接从队列里面获取需要处理的数据,直接生成数据并持久化到数据库,逻辑直接结束
// 不用考虑线程复用,为啥呢?因为线程复用是线程池帮我们实现的,具体原理稍后再讲
}
// 第一次任务延迟60秒(第一个参数),然后每隔10秒(第二个参数)执行一次
}, N_NUM_60, N_NUM_10, TimeUnit.SECONDS);
}
}
核心参数解析:
int nThreads:线程数大小,用于控制线程池的核心线程数及最大线程数。为啥就一个参数?对,没错,通过该方法创建的线程池核心线程数及最大线程数是一样的。
ThreadFactory threadFactory(可选):线程池创建线程时的构建工厂,可以在线程池创建线程时改变线程实例的默认属性(可以看一下Thread的构造函数)。
其实该方法的内部是帮我们实例化了一个ThreadPoolExecutor,并保证核心线程数为预设值,最大线程数为Integer.MAX_VALUE,空闲存活时间为0,构建了一个延迟队列。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
// ...
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
// ScheduledThreadPoolExecutor 类定义,继承了ThreadPoolExecutor,没啥本质区别
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
// 省略部分代码...
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
}
注意事项:若无法保证任务生产逻辑的任务量是可控的,请不要使用该方法创建线程池,否则可能会出现构建过多的线程实例,最大可达Integer.MAX_VALUE个线程实例,引发OOM或栈溢出。
适用场景:java自带线程池,适合你任何需要统一管理单个线程声明周期的场景,因为上面四种线程池的底层都是基于这个类的实例。
用法示例:网上一搜一大堆,或者等我下篇文章写出来后贴上来
注意事项:合理传参是比较重要的,否则会引发OOM或栈溢出。
虽然Executors创建线程池很方便,但是还是推荐大家尽量少用。因为生产环境出现好几次因为直接使用Executors创建线程池且提交任务逻辑设计不合理,导致jvm崩溃的问题。
//规则描述:禁止直接使用Executors实例化线程池,推荐通过ThreadPoolExecutor实例化,并合理配置最大线程数(不易过大)及阻塞队列大小(必须指定队列大小,且不易过大)。
//规则说明:通过Executors创建的线程池封装过度,不合理的使用会导致队列超长或工作线程数过多从而导致OOM
//修改范围:查找源码中所有的Executors.new开头的代码,并以new ThreadPoolExecutor的方式重构
//修改示例:
// 不推荐的写法
// Executors帮我们默认构建了一个new LinkedBlockingQueue(),队列大小为Integer.MAX_VALUE
// 存活任务对象的最大数量=最大线程数(这里给了4)+队列大小(默认是2的31次方-1就是2147483647)=2147483651个存活任务对象(不考虑运行时的垃圾回收)
// 若任务生产速度很快,但任务消费很慢,就会导致队列数据暴涨,创建过多的任务对象且无法释放,最终导致OOM
ExecutorService executorsNoncompliant = Executors.newFixedThreadPool(N_NUM_4,
new BasicThreadFactory.Builder().namingPattern("NoncompliantThreadPool-%d").build());
// 推荐的写法(注意合理控制每个参数值)
// 存活任务对象的最大数量=最大线程数(这里给了4)+队列大小(默认给了1000)=1004个存活任务对象(不考虑运行时的垃圾回收)
// 若任务生产速度很快,但任务消费很慢,最多只能生产1004个对象便会触发线程池的拒绝策略(默认为抛出异常给调用者)
// 注意考虑触发拒绝策略时的逻辑处理,是让主进程等待还是终止任务还是怎么办,千万不要不处理,否则也有可能把jvm搞挂
ExecutorService executorsCompliant = new ThreadPoolExecutor(N_NUM_4, N_NUM_4, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue(N_NUM_1000),
new BasicThreadFactory.Builder().namingPattern("CompliantThreadPool-%d").build());
另外附上阿里编码规约中针对该方法的使用说明
https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E5%8D%8E%E5%B1%B1%E7%89%88%EF%BC%89.pdf