并发的源头
- 可见性
- 原子性
- 有序性
- 共享数据
可见性
可见性是由于CPU的高速缓存导致的,当线程A操作内存上的变量时,存到CPU缓存中的,当线程B去拿数据时候,不能够读到线程A CPU缓存数据 ,导致结果不一致。该问题为可见性问题
可见性的 解决方案是,在共享变量上加 volatile关键字
原子性
原子性是由于CPU的时间切片导致,当一个共享变量count = 0,执行代码count ++,当A线程拿到执行时间片的时候,把count = 0从内存读取到CPU寄存器中,此时发生时间切片,线程B拿到时间片做了count + 1的操作,并且把count = 1的值写到了内存中,此时CPU再次切换到A线程,A线程寄存器的count还是0,执行count + 1,count值依旧是1,写入内存中。我们原本希望count = 2.但此时count = 1.
原子性的解决方案就是加锁, synchronized或则Lock
有序性
有序性就是按照顺序执行,但是编译器会进行内部优化,可能不会按照我们代码的顺序执行。就会照成有序性问题,看下面的例子
public class Singleton(){
private Singleton singleton;
public Singleton getInstance(){
if(singleton == null){
synchronzied(Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
return singleton;
}
}
}
该例子是经典的单例模式,双重检验。看一下 singleton = new Singleton();
我们理解的该行代码的执行顺序应该是
1. 分配一块内存M
2. 在内存M上初始化Singleton对象
3. 然后把M的地址指向singleton变量
实际的执行顺序可能是
1. 分配一块内存M
2. 把M的地址指向singleton变量
3. 在内存M上初始化Singleton对象
当两个线程A, B都调用这块代码的时候,当A线程执行到上图的2.singleton变量不再是null,但是并没有对象初始化,此时时间切片B线程访问就会singleton != null,在使用该变量的时候就会造成NullPointException