所謂併發程式設計,所謂有其三

WindWant發表於2020-05-10

一、快取記憶體的兩面性

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

 

相關文章