0 创建线程池的核心问题
根据阿里巴巴的《Java开发规范》里的一条规定,
这条规定指出了,当我们想使用线程池的时候,最好不要偷懒,最好要自己手动创建线程池,那么问题就来了,手动创建线程池到底要如何去创建?
1 我的核心线程数量到底应该创建多少?
1.1 我们设置合适的线程数量是为了什么?
为了榨干硬件的性能,我们知道,一个程序在服务器上去除网络传输的时间,剩下的就是『计算』和『I/O』的时间了。这里的『I/O』既包括和 主存和辅存 交换数据的时间,也包括网络数据传输到服务器,服务器拷贝的内核空间。我们设置合适的线程数量就是为了可以充分利用每个CPU,磁盘的交换数据的效率达到最大。
1.2 根据程序的类型分类讨论
- 比如 计算100000个随机数的加法,这个就是实打实的计算密集型的任务。
- 比如 文件上传任务,这个就是典型的『I/O』密集型的任务。
- 还有第三种『I/O、 计算混合型任务』,也就是目前的大部分程序,都是属于这种,两种耗时的任务都有涉及。
我们逐个讨论
- 如果是计算密集型的任务,那么设置的线程数为:服务器CPU数 + 1。
为什么?因为如果是一个任务是计算密集型的,那么最理想的情况就是,所有的CPU都跑满,这样每个CPU的资源都得到了充分的利用。至于为什么要需要在CPU的个数上+1,网上比较流行的解释就是,考虑到即便是CPU密集型的任务,其执行线程也可能也有可能在某个时间因为某个原因出现等待(比如说缺页中断等等)。 - 如果是IO密集型的任务,那么最好的方式的就是计算IO和计算所花费的时间比。如果 CPU 计算和 I/O 操作的耗时是 1:2,那么合适的线程就是3,至于为什么。。。这里用下图来说明
图片来自 :极客时间——10 | Java线程(中):创建多少线程才是合适的?
所以,如果是一个CPU,那么合适的线程数量就是:
1 +(IO耗时 / CPU耗时)
不过现在都是多核的CPU,所以合适的设置的线程数量就是:
CPU数 * ( 当只有一个CPU合适的线程数量 )
当然,我们工作中很难每次都完美的统计到IO和计算所用的时间比,所以,很多前辈们根据自己的工作经验,就有了一个比较通用的线程数量计算,对于I/O 密集型的应用,最佳线程数为:2 * CPU 的核数 + 1。
注意,这里还有一个重点,就是:如果你线程数越多那么切换线程的代价也就越多,所以这里的核心线程数量设置为 1, 这样,可以保证,在任务提交的过程中,我们可以保证使用少量的线程的情况下完成任务。
- 如果是混合型任务,那么,我们就把任务拆分成计算型任务和IO型任务,将这些子任务提交给各自的类型的线程池执行就行。
2 用默认的创建线程工厂还是自己实现?
阿里巴巴的《Java开发规范》中有规定:
所以这里重点就是,你需要给创建的线程有意义的名字,这里就直接规定了,你不能使用默认方法来创建线程了。那么我们要怎么创建有意义的线程名称的线程?目前有两种主流的方法。
2.1 用Guava的来创建
@Slf4j
public class ThreadPoolExecutorDemo00 {
public static void main(String[] args) {
ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder()
.setNameFormat("我的线程 %d");
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
10,
60,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(100),
threadFactoryBuilder.build());
IntStream.rangeClosed(1, 100)
.forEach(i -> {
executor.submit(() -> {
log.info("id: {}", i);
});
});
executor.shutdown();
}
}
来看看效果:
2.2 自己实现ThreadFactory
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
10,
60,
TimeUnit.MINUTES,
new ArrayBlockingQueue<>(100),
new MyNewThreadFactory("我的线程池"));
IntStream.rangeClosed(1, 100)
.forEach(i -> {
executor.submit(() -> {
log.info("id: {}", i);
});
});
executor.shutdown();
}
public static class MyNewThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final String namePrefix;
MyNewThreadFactory(String whatFeatureOfGroup) {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "From MineNewThreadFactory-" + whatFeatureOfGroup + "-worker-thread-";
}
@Override
public Thread newThread(Runnable r) {
String name = namePrefix + poolNumber.getAndIncrement();
Thread thread = new Thread(group, r,
name,
0);
if (thread.isDaemon()) {
thread.setDaemon(false);
}
if (thread.getPriority() != Thread.NORM_PRIORITY) {
thread.setPriority(Thread.NORM_PRIORITY);
}
return thread;
}
}
来看看效果:
3 拒绝策略到底用哪个?
3.1 先来看看四个基本的拒绝策略
-
(1) CallerRunsPolicy :任务不给线程池执行,给提交任务的主线程执行,看代码:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { r.run(); } }
-
(2) AbortPolicy :直接扔出异常,以及拒绝掉任务,同时这个也是默认的拒绝策略 ,也就是说,如果你在创建线程池的时候不设置拒绝策略的话,那么默认的拒绝策略就是这个。直接看代码:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); }
-
(3) DiscardPolicy:单纯的拒绝,别的啥也不做,看代码:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { }
-
(4) DiscardOldestPolicy:把最老的任务抛弃,然把当前的任务放入阻塞队列中这个其实很好理解,直接看源码理解最好理解了
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { if (!e.isShutdown()) { e.getQueue().poll(); e.execute(r); } }
小总结一波,可以看出(1)、(4) 的拒绝策略,虽然称之为拒绝了,但是仍然会执行任务。但是(2)、(3) 就会直接拒绝任务,使得任务出现丢失。
3.2 自己实现拒绝策略
比如我们想要实现一个拒绝策略,想要我们提交的任务最终可以提交到队列中,采用阻塞等待的策略来完成,那么我们要怎么写代码?其实根据JDK的代码,我们可以写出自己的拒绝策略,首先要实现 RejectedExecutionHandler
这个接口。
public static class EnqueueByBlockingPolicy implements RejectedExecutionHandler {
public EnqueueByBlockingPolicy() { }
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (e.isShutdown()) {
return;
}
try {
e.getQueue().put(r);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
当然在工作项目中不不建议你这么写,因为这样拒绝策略会导致主线程阻塞,而且没有设置超时退出。如果你真要用这样方式的拒绝,最好使用 BlockingQueue#offer(E, long, TimeUnit)
这个方法,这样毕竟有超时退出。但是使用 offer
不能保证你肯定会提交任务到队列;具体拒绝策略的代码,要结合实际需求。
3.3 总结
如果你需要保证的你提交的任务不丢失,确认执行,那么建议使用 策略 CallerRunsPolicy
, DiscardOldestPolicy
,甚至使用在 3.2 中的这个自己实现的拒绝策略,如果你的任务不重要,保证自己的程序的稳定性比较重要,那么就建议使用DiscardPolicy
,AbortPolicy
。