所謂並發編程,所謂有其三

一、高速快取的兩面性

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 變數分割為兩塊來存儲操作,因此並發操作中,需要通過一定的手段來保障原子性。