目录
线程不安全会发生什么
首先,我们先来看看这段代码:
class Counter{
volatile int count;
public Counter(){
this.count = 0;
}
public void add(){
count++;
}
public int get(){
return count;
}
}
//创建两个线程,分别让count自增10000次
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for(int i = 0; i < 10000; i++){
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.get());
}
}
本以为结果会是2w,但每次运行的结果都不一样,这就是线程不安全导致的结果
线程安全发生的原因
为啥会出现上述情况,和线程的调度随机性密切相关
count++操作,本质上是三个cpu指令构成:
1.load,把内存中的数据读取到cpu寄存器中.
2.add,将寄存器中的值,进行+1运算
3.save,把寄存器中的值写回到内存中
当两个或多个线程进行操作的时候,顺序会有很多种变化
例如:
如果是这样的话,那两个线程之间互不冲突,可以得到正确的值
但如果是上述情况的话,t1不知道t2将count的值进行了改变;
所以t1最后返回的值只是自增了一次的count
总结下来,线程不安全的原因有以下几点
1.抢占式执行()
由于随机调度的原因,线程中的代码执行到任意一行,都随时可能会被切换出去,这样就可能导致在切换出去后切换回来的过程中值发生修改
2.多个线程同时修改同一个变量
在一个线程在对一个数据进行修改时,其他线程也进行了修改,这个线程可能无法及时更新这个数据
这是从宏观角度来看
3.修改操作不是原子的
原子是不可分割的最小单位
像上述++操作,里面可以拆分为三个操作 load,add,save,所以不是原子的,在进行某个操作的时候cpu随时可能会去执行别的操作
而某个操作对应单个cpu指令,就是原子的
但这个操作对应多个cpu指令,大概率就不是原子的了
这个就是从微观角度来看
4.内存可见性
这个原因和上述案例不同
我们先写出一个bug来:
public class ThreadDemo {
public static boolean bool = true;
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread t1 = new Thread(() -> {
while(bool){
}
System.out.println("t1结束");
});
t1.start();
Scanner scanner = new Scanner(System.in);
bool = scanner.nextBoolean();
}
}
上述代码中,当我们从键盘输入false的时候,t1应该输出"t1结束",然后结束线程;
但我们输入false后,t1并没有结束.
原因就是内存可见性的问题:
t1在读取bool的时候,需要从内存上load到cpu寄存器上,然后进行compare;
虽然读取内存的速度很快,但读取寄存器的速度更快;
所以在频繁的读取下,编译器就做了一个大胆的决定:
将load指令省略,直接从编译器里读;
这就造成了bug产生的原因
5.指令重排序
指令重排序是编译器优化的策略
它会调整代码执行的顺序,让程序更高效,前提是整体的逻辑保持不变
在单线程下容易保证,但是在多线程的情况下就不好说了
例如:
class Student{
public void learn(){
System.out.println("学习");
}
public void eat(){
System.out.println("吃饭");
}
}
public class ThreadDemo {
public static Student student;
public static void main(String[] args) throws InterruptedException, ExecutionException {
//t1进行初始化
Thread t1 = new Thread(() -> {
student = new Student();
});
t1.start();
//t2进行判断
Thread t2 = new Thread(() -> {
if(student != null){
student.learn();
student.eat();
}
});
t2.start();
}
}
在t1进行初始化的时候大体可以分成三个步骤
1.申请内存空间
2.调用构造方法(初始化内存的数据)
3.把对象的引用赋值给student(内存地址的赋值)
这个时候可能就会发生指令重排序:
步骤1肯定是先执行,但步骤2和步骤3的先后顺序就是未知的
如果是在单线程里,步骤2和3的顺序变化不会有什么影响;
但在多线程里面就会因为指令重排序出现问题:
假设步骤1执行完后先执行的步骤3,但此时t2开始启动:
由于t1的步骤3已经完成,student已经是非空了,但student还没有被成功初始化,这个内存里什么都没有
此时调用student的learn和eat方法就不知道会发生什么,很可能就会产生bug