线程池标准创建方式是通过标准构造器ThreadPoolExecutor去构造工作线程池。 构造器ThreadPoolExecutor的代码如下:
public ThreadPoolExecutor(int corePoolSize, //核心线程数,即使线程空闲(Idle),也不会回收
int maximumPoolSize, //线程数的上限
long keepAliveTime,
TimeUnit unit, //线程最大空闲时长
BlockingQueue<Runnable> workQueue, //任务的排队队列
ThreadFactory threadFactory, //新线程的产生方式
RejectedExecutionHandler handler //拒绝策略)
那么corePoolSize和maximumPoolSize 的该如何设置呢?在设置这两个值之前,首先需要通过任务类型对线程池进行分类, 可以分为IO密集型任务,CPU 密集型任务和混合型任务。
IO 密集型任务
此类任务主要是执行IO操作,由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU 常处于空闲状态。Netty就是典型的例子。
由于IO密集型任务的CPU使用率较低,导致线程的CPU空余时间比较多,因此线程数通常需要开CPU核数两倍的线程。当IO线程空闲时,可以启用其他线程继续使用CPU,以提高CPU的使用率。Netty的Reactor实现类的IO处理线程数默认正好为CPU核数的两倍。
IO密集型任务线程池演示代码如下:
/懒汉式单例创建线程池:用于IO密集型任务
public class IoIntenseTargetThreadPoolLazyHolder {
/**
* 有界队列size
*/
public static final int QUEUE_SIZE = 128;
//线程池: 用于IO密集型任务
public static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
IO_MAX, //CPU核数*2
IO_MAX, //CPU核数*2
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new ThreadUtil.CustomThreadFactory("io"));
public static ThreadPoolExecutor getInnerExecutor() {
return EXECUTOR;
}
static {
log.info("线程池已经初始化");
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread("IO密集型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
测试代码如下:
@Test
public void testIoIntenseTargetThreadPool() {
ThreadPoolExecutor pool = IoIntenseTargetThreadPoolLazyHolder.getInnerExecutor();
;
for (int i = 0; i < 2; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
for (int j = 1; j < 10; j++) {
log.info(getCurThreadName() + ", 轮次:" + j);
}
log.info(getCurThreadName() + " 运行结束.");
}
});
}
ThreadUtil.sleepMilliSeconds(1000);
}
}
在IO密集型线程池有以下几个要点:
(1)允许核心线程池销毁
IO线程池调用了allowCoreThreadTimeOut(…)方法,并传入了参数true,则keepAliveTime参数所设置的Idle 超时策略也将被应用与核心线程,当池中的线程长时间空闲时,可以自行销毁。
(2) 使用有界队列
使用有界队列而不是无界队列缓冲任务,如果128太小可以根据具体需要进行增大,但是不能使用无界队列。
(3)优先创建线程,而不是优先加入队列
corePoolSize、maximumPoolSize 保持一致,使得在接收到新任务时,如果没有空闲工作线程,则优先创建线程去执行线程,而不是优先加入阻塞队列,等待现有工作线程空闲后再执行。
(4) 使用内部静态类懒汉式单例模式创建线程池
使用懒汉式单例模式创建线程池,如果代码没有用到此线程,也不会立即出创建,只有在getInstance()被调用时才去加载内部类并且初始化单例,该方式解决了线程安全问题,也解决了写法繁琐问题。
(5)使用JVM关闭时的钩子函数,优雅的自动关闭线程池
CPU 密集型任务
此类任务主要是执行计算任务,由于响应时间很快,CPU一直在运行,这种任务CPU的利用率很高。
cpu密集型任务虽然可以并行完成,但是并行任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以要最高效地利用cpu,cpu密集型任务并行执行的数量等于cpu核数,即线程数等于cpu核数。
示例代码如下:
public class CpuIntenseTargetThreadPoolLazyHolder {
//线程池: 用于CPU密集型任务
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
MAXIMUM_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new CustomThreadFactory("cpu"));
public static ThreadPoolExecutor getInnerExecutor() {
return EXECUTOR;
}
static {
log.info("线程池已经初始化");
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread("CPU密集型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
@Test
public void testCpuIntenseTargetThreadPool() {
ThreadPoolExecutor pool = CpuIntenseTargetThreadPoolLazyHolder.getInnerExecutor();
for (int i = 0; i < 2; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
for (int j = 1; j < MAX_TURN; j++) {
Print.cfo(getCurThreadName() + ", 轮次:" + j);
}
Print.cfo(getCurThreadName() + " 运行结束.");
}
});
}
ThreadUtil.sleepMilliSeconds(Integer.MAX_VALUE);
}
混合型任务
此类任务既要执行逻辑计算,又要进行IO操作。相对来说, 由于执行IO操作的耗时比较长,CPU利用率也不是很高, Web服务器的HTTP请求处理操作属于此类,一次请求处理会包括DB操作、PRC操作、缓存操作等多种耗时操作。一般来说,一次Web请求的CPU计算耗时往往较少,大致在100~500毫秒,而其他耗时操作会占用500到1000毫米。
例如,Tomcat服务器的线程池,一次Web请求的大概由CPU操作+多次数据库操作+多次远程调用组成, CPU计算耗时往往比较少,大致在100ms-500ms之间, 而其他耗时操作会占用500ms-1000ms甚至更多的时间。所以Tomcat服务器的线程池就是典型的混合型任务线程池。
在为混合型任务创建线程池是,可以通过一个比较成熟的估算公式,如下
最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*cpu核数
简化后:
最佳线程数=(线程等待时间/线程cpu时间+1)*cpu核数
举个例子, 比如web服务器处理HTTP请求时,假设平均线程CPU运行为100ms,而线程等待时间为900ms,CPU核数为8核,根据以上公式估算:
(900ms+100ms)/100ms*8=10*8=80
所以需要线程数为80;
从公式得出: 等待时间所占的比例越高,需要的线程数就越多;cpu韩式所占的比例越高,需要的线程就越少。
公式计算出来的是理论值,仅供生产环境中使用参考,还需要结合系统网络环境和硬件情况(cpu、内存、硬盘读写速度)不断尝试,获取一个符合实际的线程数值。
混合型任务创建参考线程池的代码示例:
//懒汉式单例创建线程池:用于混合型任务
public class MixedTargetThreadPoolLazyHolder {
//首先从环境变量 mixed.thread.amount 中获取预先配置的线程数
//如果没有对 mixed.thread.amount 做配置,则使用常量 MIXED_MAX 作为线程数
private static final int max = (null != System.getProperty(MIXED_THREAD_AMOUNT)) ?
Integer.parseInt(System.getProperty(MIXED_THREAD_AMOUNT)) : MIXED_MAX;
//线程池: 用于混合型任务
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
max,
max,
KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue(QUEUE_SIZE),
new CustomThreadFactory("mixed"));
public static ThreadPoolExecutor getInnerExecutor() {
return EXECUTOR;
}
static {
log.info("线程池已经初始化");
EXECUTOR.allowCoreThreadTimeOut(true);
//JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread("混合型任务线程池", new Callable<Void>() {
@Override
public Void call() throws Exception {
//优雅关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}
}
测试代码如下:
@Test
public void testMixedThreadPool() {
System.getProperties().setProperty(MIXED_THREAD_AMOUNT, "80");
// 获取自定义的混合线程池
ExecutorService pool =
ThreadUtil.getMixedTargetThreadPool();
for (int i = 0; i < 1000; i++) {
try {
sleepMilliSeconds(10);
pool.submit(new CreateThreadPoolDemo.TargetTask());
} catch (RejectedExecutionException e) {
e.printStackTrace();
}
}
//等待10s
sleepSeconds(10);
Print.tco("关闭线程池");
}
测试结果如下:
如何设置最大线程数你会了没?