中级08 - Java多线程原理
2019-09-27 本文已影响0人
晓风残月1994
多线程赋予了计算机同时完成很多件事情的能力。这等价于将它的计算能力提高了许多倍。
- 为什么需要多线程
- 开启一个新线程
- 为什么多线程难
- 多线程的适用场景
一、为什么需要多线程
主要是 CPU 同学太优秀了,能者多劳,能力越大,责任越大,CPU 不 996 甚至 007 真的是太浪费了,以辛勤劳动为荣,以好逸恶劳为耻。
其次,Java 的执行模型是同步/阻塞的,Java 程序默认情况下只有一个线程来处理问题,这非常自然且符合人类直觉。但诸如 IO 此等耗时操作,存在严重性能问题,所以可以开启多个线程来干活。
以下单线程代码,在我的机器上用时9231ms:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
public class Test {
public static void main(String[] args) {
long t0 = System.currentTimeMillis();
slowFileOperation();
slowFileOperation();
slowFileOperation();
slowFileOperation();
long t1 = System.currentTimeMillis();
System.out.println("用时" + (t1 - t0) + "ms");
}
private static void slowFileOperation() {
try {
File tmp = File.createTempFile("tmp", "");
try (FileOutputStream fos = new FileOutputStream(tmp)) {
for (int i = 0; i < 100000; i++) {
fos.write('a');
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
二、开启一个新线程
同样的测试块,但是分别使用不同线程同时进行,现在除了主线程外,还分别启动了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");
}
![](https://img.haomeiwen.com/i7038854/fbb9a227a916d33e.png)
开启多个线程后,本次用时4742ms。
查看一下Thread
的源码可知,调用Thread
实例的start()
方法会开始执行该线程,JVM 会调用该线程的run()
方法,而run()
内部则会尝试调用Runnable
中的run()
方法。最后会多一个执行流,有自己的方法栈,方法栈是线程私有的。
- 方法栈(局部变量)是线程私有的
- 静态变量和类变量是被所有线程共享的
三、为什么多线程难
同一份代码被多个线程执行时,无法保证线程执行顺序,如果存在共享变量,那么更加无法保证最终结果。
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
的输出无法保证。
四、多线程的适用场景
计算机操作一般分为两种类型:
- CPU 密集型:多线程带来的提升有限,比如解压缩文件;
- IO 密集型:IO 操作之慢在 CPU 眼中仿佛是静止一般,CPU 当然不能干等着 IO 操作,要继续去做别的事,所以适合采用多线程技术。