一、背景
上篇文章通过图文并茂结合生活中的例子的方式,讲解了线程池的原理,其中演示并说明了自定义线程池的7大参数,针对参数之一最大线程创建数该如何定义呢?
二、线程和CPU的关系
在搞懂线程与CPU的关系前,先思考一个问题:Java真的可以开启线程吗?
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将此线程加入线程组
group.add(this);
boolean started = false;
try {
// 执行start0方法,真正执行线程
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
// 本地方法,调用底层的c++
private native void start0();
通过Thread线程的start()启动方法可以得知,只是将当前线程加入到一个线程组中,真正开启线程的方法是start0(),并且是一个native本地方法,实际上是调用底层的C++,Java是无法操作硬件,它是运行在虚拟机之上的。
线程是CPU调度和分配的基本单位(即CPU只能看到线程)
线程调度分2种:
- 分时:CPU分配给线程的执行时间都一样长,所有线程轮流使用CPU 的使用权,分配给自己的时间到了,即便程序没有运行完,也会切换到下一个线程
- 抢占式:优先让优先级高的线程使用 CPU,如线程优先级相同,则随机选择一个线程执行,Java使用的为抢占式调度
那么线程池的最大创建数的定义又跟cpu有什么关系呢?这跟运行什么类型的程序有关!
|CPU密集型
指计算密集型的程序。该任务需进行大量的计算,主要消耗CPU资源,CPU占用率相当高。虽然可以用多线程完成,但任务越多,切换线程需花费时间就越多,效率降低。线程的应用场景:主要是复杂算法。
所以,要最高效的利用CPU,最大线程数:N(CPU核数) + 1
为什么要+1呢?《Java并发编程实战》一书中给出的原因是:即使当CPU密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费。
你可以理解为:是一个备份的线程
|IO密集型
IO密集型任务指任务需要执行大量的IO操作,涉及到网络、磁盘IO操作,对CPU消耗较少。线程的应用场景主要是:数据库数据的交互,文件上传下载,网络数据传输等等
- 磁盘 IO :针对磁盘的读写操作,比如文件的读写,假如数据库在本地的话,也属于磁盘 IO
- 网络 IO :各种网络请求,比如 http 请求、远程数据库读写、远程 Redis 读写等等。
最大线程数:2*N(CPU核数)
课间小知识:如何查看CPU核心数呢?
一些单纯善良的小伙伴可能就马上打开Windows任务管理器,在那里数CPU核数了,emmmm.... 运维部门的大刀已经向你挥舞了!自己电脑上的核数不等于服务器上的CPU核数,而且部署到不同的服务器,CPU核数可能都不一样,正确的姿势应该是通过代码获取:
Runtime.getRuntime().availableProcessors());
三、总结
1. CPU密集型:
充分利用CPU核心数。比如8核,线程数设置:8+1,最大的效率就是同时跑8个线程,另外一个当备份线程。如线程数远超过CPU核心数,会导致频繁的切换线程从而耗费较多时间
2. IO密集型:
IO操作并不占用CPU,不要让所有CPU闲下来,IO 的速度比起 CPU 是很慢的,此时线程数等于CPU核心数的两倍是最佳的
优秀的判断力来自经验,但经验来自于错误的判断。