文章目录

  • 问题引入
  • 测试环境
  • 测试结果整理
  • 测试代码
  • 结论


问题引入

《深入理解Java虚拟机》一书中介绍了Java的线程模型,其中提到在jdk的早期版本中,采取了多个用户线程对应一个内核线程的模型
而如今的jdk版本(不论是Oracle JDK还是Open JDK,都默认用了HotSpot虚拟机),而现在的HotSpot虚拟机模型在实现了Java的线程模型时,采取的是java 虚拟机 多线程 多虚拟机 多线程对虚拟机有用吗_java 虚拟机 多线程 多虚拟机的线程模型 —— 每一个用户线程都对应一个内核级线程

对于不同线程模型的区别以及优缺点,可以看我的另一篇博客。
这里先留一个坑。

测试环境

  1. 机器:M1芯片的Mac,16 主存、8核处理器。
  2. 操作系统: mac OS BigSur,
  3. jdk环境:Oracle JDK 8
  4. jvm类型:HotSpot

上面有一个参数非常重要,CPU是8核的,这个对多线程的是否能加速程序的运行起到至关重要的影响。

测试结果整理

先看一下运行结果。

单个任务的执行时间大概是4.27秒左右,记做java 虚拟机 多线程 多虚拟机 多线程对虚拟机有用吗_java 虚拟机 多线程 多虚拟机_02,

记任务的个数为java 虚拟机 多线程 多虚拟机 多线程对虚拟机有用吗_java 虚拟机 多线程 多虚拟机_03

如果是串行执行的话,总耗时就是java 虚拟机 多线程 多虚拟机 多线程对虚拟机有用吗_线程模型_04

如果是多线程呢?

看下面两张图就知道。

java 虚拟机 多线程 多虚拟机 多线程对虚拟机有用吗_System_05

java 虚拟机 多线程 多虚拟机 多线程对虚拟机有用吗_线程模型_06

明显在,在线程数小于等于CPU的核数时,多个任务的运行就像是并行的一样(耗时也是4点几秒或者5.秒)。
背后的原因就是,HotSpot的线程模型就是1:1的,每一个用户线程都对应一个内核线程,然后每一个内核线程都可以分配到每一个核上跑。
所以,即使有8个任务,但是通过多线程,依旧可以在比单个任务多一点的时间内完成。

不过,多线程的加速有瓶颈的。

瓶颈一:
但并发任务数大于CPU的核数时,即使有有再多的线程,同一时刻也只有一个线程能占有处理器,虽然它们仍然是并发的,但并不是并行,因为这是通过CPU不断划分时间片到每一个线程的原因。
所以,当线程数大于核数时,基本上时间线性增长。

瓶颈二:
上面的展示的任务,任务与任务之间是没有依赖关系的。如果有依赖关系,那么只能选择等待。

于是,总结出下面的公式。

java 虚拟机 多线程 多虚拟机 多线程对虚拟机有用吗_System_07
其中,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去跑多线程程序,仍然可以加速。