多线程赋予了计算机同时完成很多件事情的能力。这等价于将它的计算能力提高了许多倍。

  • 为什么需要多线程
  • 开启一个新线程
  • 为什么多线程难
  • 多线程的适用场景

1. 为什么需要多线程

主要是 CPU 同学太优秀了,能者多劳,能力越大,责任越大,CPU 不 996 甚至 007 真的是太浪费了,以辛勤劳动为荣,以好逸恶劳为耻。

其次,Java 的执行模型是同步/阻塞的,Java 程序默认情况下只有一个线程来处理问题,这非常自然且符合人类直觉。但诸如 IO 此等耗时操作,存在严重性能问题,所以可以开启多个线程来干活。

以下单线程代码,在我的机器上用时9231ms:

  1. import java.io.File;
  2. import java.io.FileOutputStream;
  3. import java.io.IOException;
  4. import java.io.UncheckedIOException;
  5. public class Test {
  6. public static void main(String[] args) {
  7. long t0 = System.currentTimeMillis();
  8. slowFileOperation();
  9. slowFileOperation();
  10. slowFileOperation();
  11. slowFileOperation();
  12. long t1 = System.currentTimeMillis();
  13. System.out.println("用时" + (t1 - t0) + "ms");
  14. }
  15. private static void slowFileOperation() {
  16. try {
  17. File tmp = File.createTempFile("tmp", "");
  18. try (FileOutputStream fos = new FileOutputStream(tmp)) {
  19. for (int i = 0; i < 100000; i++) {
  20. fos.write('a');
  21. }
  22. }
  23. } catch (IOException e) {
  24. throw new UncheckedIOException(e);
  25. }
  26. }
  27. }

2. 开启一个新线程

同样的测试块,但是分别使用不同线程同时进行,现在除了主线程外,还分别启动了3个线程,实现了Runnable接口的run()方法是新线程的入口,就像主线程的main()方法一样。

public static void main(String[] args) {
    long t0 = System.currentTimeMillis();
    // 写法1
    new Thread(new Runnable() {
        @Override
        public void run() {
            slowFileOperation();
        }
    }).start();
    // 写法2
    new Thread(() -> slowFileOperation()).start();
    // 写法3
    new Thread(Test::slowFileOperation).start();

    slowFileOperation();

    long t1 = System.currentTimeMillis();
    System.out.println("用时" + (t1 - t0) + "ms");
}

中级08 -  Java多线程原理 - 图1
开启多个线程后,本次用时4742ms。

查看一下Thread的源码可知,调用Thread实例的start()方法会开始执行该线程,JVM 会调用该线程的run()方法,而run()内部则会尝试调用Runnable中的run()方法。最后会多一个执行流,有自己的方法栈,方法栈是线程私有的。

  • 方法栈(局部变量)是线程私有的
  • 静态变量和类变量是被所有线程共享的

3. 为什么多线程难

同一份代码被多个线程执行时,无法保证线程执行顺序,如果存在共享变量,那么更加无法保证最终结果。

CPU的速度相比内存和硬盘来说太快了,存在多线程时,把每个线程想象成钟表上的一个刻度,CPU就是不停高速旋转的指针,每个线程只被执行了片刻(实际是纳秒级别的切换速度),CPU 又立即执行下一个线程了,因为CPU足够快,所以从使用者角度才一般察觉不到这种切换。

看下面多线程的例子,其中由于共享变量的非原子操作导致输出乱序问题:

public class Test {
    private static int i = 0;

    public static void main(String[] args) {
        long t0 = System.currentTimeMillis();

        for (int j = 0; j < 100; j++) {
            new Thread(Test::slowFileOperation).start();
        }

        long t1 = System.currentTimeMillis();

        System.out.println("用时" + (t1 - t0) + "ms");
    }

    private static void modifySharedVariable() {
        i++;
        System.out.println("i = " + i);
    }
}

因为i++并不是一个原子操作,CPU 执行时分为三步:

取 i 的值
把 i 的值加 1
把修改后的值写回 i

程序运行过程中,CPU 可能会切换到别的线程,所以最终i的输出无法保证。

4. 多线程的适用场景

计算机操作一般分为两种类型:

  • CPU 密集型:多线程带来的提升有限,比如解压缩文件
  • IO 密集型:IO 操作之慢在 CPU 眼中仿佛是静止一般,CPU 当然不能干等着 IO 操作,要继续去做别的事,所以适合采用多线程技术