多线程-出现非线程安全的底层原因
一:什么是非线程安全
一提到多线程,有经验的程序员就会考虑线程安全问题,那在什么情况下会出现线程安全的问题呢?
很多人可以轻而易举的总结出:当多个线程同时竞争共享变量时会出现线程安全问题。 但是对于底层为什么会出现这种情况却不清楚了。
二:非线程安全的源头
出现非线程安全的源头归因于:原子性、可见性、有序性。
2.1:导致原子性的源头:
很多初学者会认为i++是一个原子性操作,而i++的在cpu指令中,却分为三个步骤。
1、从内存中读数据到cpu寄存器
2、在cpu寄存器中执行+1指令
3、写回到内存中。
这三个指令在任何一步都可以在线程上下文切换时被中断,所以i++遇到上下文切换时就会导致原子性问题。
2.2:导致可见性的源头:
因为在硬件系统中,cpu和内存的速度差相差过多,所以为了平衡两者的速度差,两者之间加了一个cpu缓存。
在多核时代,每个cpu中的线程同时去读写一个共享变量时,因为操作数据是在cpu中,所以当每个cpu中的缓存数据不能及时更新到内存中此时会导致可见性问题。
2.3:导致有序性的源头:
由于编译器、cpu、内存系统在做编译优化时,会做指令重排序,所以程序可能并不会按照我们编写代码的顺序执行。一个经典的案例,就是单例模式下的双重检查。
public class Singleton{
static Singleton instance;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchornized(Singleton.class){
if(instance==null){
//不能保证有序性
instance=new Singleton();
}
}
}
return instance;
}
}
上面的单例看着很完美,用synchornized做了同步操作,此时执synchornized代码块中的内容时,只能一个线程去执行。然而出现上下文切换时会导致线程安全问题。
因为 instance=new Singleton(); 并不是一个原子性操作,也是分为三步。
1、在堆内存中为对象开辟一块空间。
2、在空间中初始化该对象。
3、将instance引用指向该堆内存空间。
然而当编译优化时,出现指令重排序,顺序变为1->3->2,在第3步此时instance指向不为空,出现上下文切换,此时另外一个线程在判断 if(instance==null) ,发现不为空,直接返回一个没有被初始化的对象,就会导致线程安全性问题。
三:总结
所以此时可以总结出,在多线程中不做额外的处理是不能保证可见性、有序性、原子性,当多个线程同时竞争共享变量时会出现线程安全性问题,所以如何保证原子性、可见性、有序性是并发编程的关键之处。
上一篇: Java基础(1)--面试