所谓并发编程,所谓有其三
一、高速缓存的两面性
cpu->高速缓存->内存
高速缓存:平衡cpu和内存之间的速度差异,变量从内存首先加载到高速缓存然后以供cpu计算使用。
对于同一个cpu来说,存储于其高速缓存中的变量,对于使用其时间碎片的线程来说,都是原子可见的,任何的变更都能及时的感知到其所被使用的线程。
但是对于不同cpu来说,每个cpu都有对应的高速缓存,对于使用不同cpu时间碎片的线程来说,如果没有特殊的处理,是无法及时感知其它cpu高速缓存里的变量变更的。
因此涉及内存中共享变量的使用时,处理变量的对线程的可见性非常关键。
二、关于原子操作
上面我们说了原子可见性,所谓可见,只是关联线程能够及时的感知到变量的变化。
但是,具体到操作上,不同cpu对于同一个变量的操作,在没有保障的情况下,是无法做到原子性的,也就是,同一时间两个线程可能会在一个变量的同一个基础上做出同样的变更。,、例如,两个线程同时执行++操作,最终变量会丢失其中的一次期望变更。
三、关于指令重排序
所谓指令重排序,即编译器为了优化性能对需要执行的程序语段进行重排序。
当然,重排序在不涉及并发的操作中,是有益的,否则编译器也不会有着个功能。
但是,当进行并发编程时,我们就需要重新考虑我们的程序在经过编译器后是否还能按照我们的期望执行。
这里,我们首先来阐述下java中对象的创建过程:
参照:jvm之对象创建过程
我们可以看到,这个初始化的过程放在了最后,也就是先有了对象和内存的映射,然后进行对象的初始化。
这里再来论述下单例模式的一种方式:双重判断
if instance == null {
同步 {
if instance == null {
创建对象 instance
}
return instance
}
}
对象创建过程的非原子性及编译优化后的执行顺序,也就决定了并发线程在获取单例实例时,可能会产生获取到未初始对象的异常。
因此,为了避免这种的情景发生,我们需要一定的措施来禁止这种优化排序过程。
通常,我们会对单例实例对象添加 volatile 修饰:volatile instance
或者,在返回时,判断当前对象是否已初始化完成,如spring中的处理:
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
Map var4 = this.singletonObjects;
synchronized(this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
附:关于 long 和 double
long 和 double 在Java中都是占用8个字节,64位。
现阶段,操作系统并存的有32位和64位。
因此对于 long 和 double 变量的操作,在不同的操作系统是不同的。
64位系统能够完整操作一个 long 或者 double 变量,是原子的。
32位系统则会把 long 或者 double 变量分割为两块来存储操作,因此并发操作中,需要通过一定的手段来保障原子性。