引子
首先来看一段代码:
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
运行结果:
第一次
第二次
第三次
我们发现,以上代码有bug,我们的预期是10w,显然上述的结果不是我们想要的.
初次分析
为什么会出现上述的问题呢?
我们来分析一下.
如果我们把代码修改成这样:
t1.start();
t1.join();
t2.start();
t2.join();
运行结果就没问题了.
这个写法本质上相当于,t1先执行,t1执行完了,t2再执行,此时t1和t2是串行执行的.
而刚刚的代码:
t1.start();
t2.start();
t1.join();
t2.join();
这个写法,t1和t2是并发执行的.
由此我们可以知道这是由于多个线程并发执行引起的bug.
这样的bug就称为"线程安全问题"或者叫做"线程不安全".
再次分析
那这个bug到底是怎么出现的呢,换句话说,为什么会出现线程不安全呢?
线程不安全是一个综合性的问题.竖起耳朵仔细听啦~
代码中的 count++; 操作,其实在CPU视角来看,是3个指令:
- 把内存中的数据读取到CPU寄存器里
- 把CPU寄存器里的数据 +1
- 把寄存器的值,写回内存
为了方便叙述,给上面三步分别起名为 load , add , save.
(由于不同架构的CPU有不同的指令集,所以对于这三个操作,不同的CPU里对应指令的名称肯定是不同的~)
由于CPU是"抢占式执行,随机调度"的,也就是说CPU在调度执行线程的时候,不定啥时候就会把线程给切换走.
(指令是CPU执行的最基本单位,它不会执行一半就被调走)
但是由于count++ 是三个指令,可能会出现CPU执行了其中的1个指令或者两个指令或者三个指令就被调度走的情况(无法预测).
基于上面的情况,两个线程同时对count进行++,就容易出现bug
正常情况如下
上述的执行顺序,只是一种可能的调度顺序.由于调度过程是"随机"的,因此就会产生很多其他的执行顺序~
比如:
甚至:
相信你看完下面的图,肯定就会理解了~
通过上述分析,我们发现对于多线程代码来说,最大的困难就在于线程在操作系统中是"随机调度,抢占式执行"的,这简直就是"罪魁祸首,万恶之源"啊.
总结
最后总结一下为什么最开始的代码有bug吧:
- 线程在操作系统中是"随机调度,抢占式执行"的
- 多个线程,同时修改了同一个变量
- 修改操作,不是"原子的"
怎么规避呢?
第一条原因我们一般不能改,但是我们可以改第二条,比如说改成使用一个线程,或者使用多个线程修改不同的变量等等.
我们写多线程代码需要在所有可能的线程调度的顺序下,确保每一种情况都没有bug.这也是多线程代码最困难也最复杂的地方,学习这一块时要有心里预期啊~