【Java进阶知识】多线程导致的不安全现象
在我们设计多线程时,要考虑的问题不外乎就两个——安全、效率!
效率很好理解,那么对于安全呢?多线程为什么会不安全?这篇博客总结自己对多线程安全的初步讲解
1.多线程体现出来的不安全
如果现在让我们给定一个类的静态变量COUNT,并且创建20个线程,在每个线程中都对该变量进行+10000操作,那么,在执行完这个整个操作后,我们期望COUNT返回的值应该是20,0000 那是不是就会这样书写代码:
public class UnsafeThread {
public static int COUNT; //静态变量大写,基本数据类型在初始化出来后都有默认值
public static void main(String[] args) {
//开启20个线程,每个线程对COUNT进行++操作10000次,预期结果200000
for(int i = 0; i < 20; i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 10000; j++){
COUNT ++;
}
}
}).start();
}
while(Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(COUNT);
}
}
执行代码,我们得到的结果是20,0000吗?
事实上并不是!并且每次执行的结果都不相同,但都比20,0000小,显然,我们的结果是有问题的
造成这一现象的原因就是线程的不安全导致的!因为我们创建出的20个线程相互之间是平行的,它们的执行也是不分先后的。事实上每次的COUNT++操作都要经历三个步骤:
- 从主存上读取到COUNT当前的值
- 对COUNT 进行+1操作
- 将修改后的COUNT值重新写回主存中
而分为三个步骤就导致一个很大的问题:假设线程1正在进行第二步,对COUNT修改时,此时线程2也开始执行此代码块,当它从主存读取数据时,读到的是线程1未修改之前的数据,对此数据进行操作。此时就导致对COUNT操作不同步,使得很多线程对COUNT的操作最终无效。
所以,简单的来说如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的,否则,它就是不安全的。
2.不安全的原因
(1)原子性:
为了理解原子性我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
当然要注意,一条 java 语句不一定是原子的,也不一定只是一条指令。比如刚才我们看到的 n++,其实是由三步操作组成的。那么不保证原子性会给多线程带来什么问题呢?显然,就如同上面的示例一样,如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
(2)可见性
主内存-工作内存
如图所示,为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中执行,但这样会造成一个问题,共享变量在多线之间不能及时看到改变,这就是可见性。
(3)代码顺序性
什么是代码重排序?
例如,有这样一系列操作:
- 去前台取下 U 盘
- 去教室写 10 分钟作业
- 去前台取下快递
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台,这种就叫做指令重排序。
显然单线程情况是没问题的,但在多线程场景下就有问题了,什么问题呢。可能快递是在你写作业的10分钟内被另一个线程放过来的,或者被人变过了,如果指令重排序了,代码就会是错误的。
当然,解决多线程不安全也有很多方法,我们以后逐渐学习。