问题引入
《深入理解Java虚拟机》一书中介绍了Java的线程模型,其中提到在jdk的早期版本中,采取了多个用户线程对应一个内核线程的模型。
而如今的jdk版本(不论是Oracle JDK还是Open JDK,都默认用了HotSpot虚拟机),而现在的HotSpot虚拟机模型在实现了Java的线程模型时,采取的是
1
:
1
1:1
1:1的线程模型 —— 每一个用户线程都对应一个内核级线程。
对于不同线程模型的区别以及优缺点,可以看我的另一篇博客。
这里先留一个坑。
测试环境
- 机器:M1芯片的Mac,16 主存、8核处理器。
- 操作系统: mac OS BigSur,
- jdk环境:Oracle JDK 8
- jvm类型:HotSpot
上面有一个参数非常重要,CPU是8核的,这个对多线程的是否能加速程序的运行起到至关重要的影响。
测试结果整理
先看一下运行结果。
单个任务的执行时间大概是4.27秒左右,记做
T
T
T,
记任务的个数为
n
n
n。
如果是串行执行的话,总耗时就是
n
∗
T
n*T
n∗T。
如果是多线程呢?
看下面两张图就知道。
明显在,在线程数小于等于CPU的核数时,多个任务的运行就像是并行的一样(耗时也是4点几秒或者5.秒)。
背后的原因就是,HotSpot的线程模型就是1:1的,每一个用户线程都对应一个内核线程,然后每一个内核线程都可以分配到每一个核上跑。
所以,即使有8个任务,但是通过多线程,依旧可以在比单个任务多一点的时间内完成。
不过,多线程的加速有瓶颈的。
瓶颈一:
但并发任务数大于CPU的核数时,即使有有再多的线程,同一时刻也只有一个线程能占有处理器,虽然它们仍然是并发的,但并不是并行,因为这是通过CPU不断划分时间片到每一个线程的原因。
所以,当线程数大于核数时,基本上时间线性增长。
瓶颈二:
上面的展示的任务,任务与任务之间是没有依赖关系的。如果有依赖关系,那么只能选择等待。
于是,总结出下面的公式。
t
=
{
T
,
n
<
=
M
n
/
M
∗
T
,
n
>
M
t = \begin{cases} T ,n <= M \\ n/M*T,n>M \end{cases}
t={T,n<=Mn/M∗T,n>M
其中,T是单个任务的耗时,n是任务数,M的CPU的核数。
测试代码
package my;
import java.util.ArrayList;
import java.util.List;
class Job {
final int mod = 100000007;
void f() {
int a = 1, b = 1;
for (int i = 0; i < 1000000000; i++) {
int c = b;
b = (a + b) % mod;
a = c % mod;
}
System.out.println("斐波那契数列的第1000000000项取模后为:" + a);
}
}
class SingleJob {
Job job;
public SingleJob() {
this.job = new Job();
}
double exec() {
long start = System.currentTimeMillis();
job.f();
long end = System.currentTimeMillis();
return (double) (end - start) / 1000.0;
}
}
class MultiThreadRunning {
int count;
List<Thread> list = new ArrayList<>();
public MultiThreadRunning(int count) {
this.count = count;
}
private static class JobTread extends Job implements Runnable {
int id;
public JobTread(int id) {
this.id = id;
}
@Override
public void run() {
super.f();
System.out.println("任务线程" + id + "结束");
}
}
double exec() {
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
Thread thread = new Thread(new JobTread(i + 1));
list.add(thread);
thread.start();
}
// 轮询
while (!isAllFinished()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
return (double) (end - start) / 1000.0;
}
boolean isAllFinished() {
for (Thread thread : list) {
if (thread.isAlive()) return false;
}
return true;
}
}
class SequentialRunning {
int count;
public SequentialRunning(int count) {
this.count = count;
}
double exec() {
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
// System.out.print("任务开始:");
new Job().f();
}
long end = System.currentTimeMillis();
return (double) (end - start) / 1000.0;
}
}
public class TestMultiThreadFast {
public static void main(String[] args) {
System.out.println("单个任务耗时 " + new SingleJob().exec() + " 秒");
System.out.println("\n --------- \n");
// 这里的 numOfJobs 取不同的值去测试
int numOfJobs = 32;
double totalTime = new SequentialRunning(numOfJobs).exec();
// 注意让顺序执行的先执行
System.out.println("顺序执行耗时 " + totalTime + " 秒");
System.out.println("平均任务耗时 " + (totalTime / numOfJobs) + " 秒");
System.out.println("\n --------- \n");
totalTime = new MultiThreadRunning(numOfJobs).exec();
System.out.println("多线程共耗时 " + totalTime + " 秒");
System.out.println("平均任务耗时 " + (totalTime / numOfJobs) + " 秒");
}
}
结论
通过这个例子,终于认识多线程是否能加速,依赖于这么几个因素。
① 线程的模型(操作系统是否支持内核级线程)
② CPU的核数(或者说,处理器的个数)
③ 任务是IO密集型,还是CPU密集型,如果是IO密集型,即使是单个CPU去跑多线程程序,仍然可以加速。