可見性 原子性和有序性問題

2022-07-16 11:06:19 字數 4504 閱讀 2081

核心矛盾

這些年,我們的 cpu、記憶體、i/o 裝置都在不斷迭代,不斷朝著更快的方向努力。但是,在這個快速發展的過程中,有乙個核心矛盾一直存在,就是這三者的速度差異

我形象的描述了一下這三者的速度上的差異:所謂天上一天地上一年(愛因斯坦的相對論是有合理解釋的),cpu和記憶體之間的速度差異就是cpu天上一天,記憶體地上一年(假設 cpu 執行一條普通指令需要一天,那麼 cpu 讀寫記憶體得等待一年的時間)。記憶體和i/o裝置的速度差異就更大了,記憶體是天上一天,i/o裝置是地上十年。

木桶原理

乙隻水桶能裝多少水取決於它最短的那塊木塊

程式的大部分語句都要訪問記憶體,有些還要訪問i/o,根據木桶原理,程式整體的效能取決於最慢的操作,所以只是單方面的提高cpu的效能是無效的,才出現了一些提高cpu效能使用率的優化

如何提高cpu的效能?

cpu增加了快取,以均衡與記憶體的速度差異(計算機體系結構方面優化)

作業系統增加了程序、執行緒,以分時復用cpu,進而均衡cpu與i/o裝置的速度差異(作業系統方面優化)

編譯程式優化指令執行次序,使得快取能夠得到更加合理地利用(編譯程式帶來的優化)

很多時候人們總是會很開心享受眼前的好處,卻總是忽略了一句話:採用一項技術的同時,一定會產生乙個新的問題,併發程式很多的詭異問題的根源也從這裡開始

單核時期,所有的執行緒都是操作同乙個cpu的,所以cpu的快取也是執行緒之間可見的,如下圖所示:

執行緒1和執行緒2都是編輯同乙個cpu裡面的快取,所以執行緒1更新了變數a的值,執行緒2之後再訪問變數a,得到的一定是a的最新值

乙個執行緒對共享資源的修改,另乙個執行緒能夠立刻看到,稱之為可見性

多核時代,每個cpu都有自己單獨的快取,當多個執行緒在不同的cpu上執行時,這些執行緒操作的就是不同的cpu快取了,如下圖所示:執行緒1在cpu-1的快取上編輯變數a,執行緒2在cpu-2的快取上編輯變數a,這個時候執行緒1對變數a的操作對於執行緒2來說就不具備可見性

二話不說,上**解釋是最直接的方式

下面這段**建立了兩個執行緒,每個執行緒都會呼叫一次updatevar的方法,都會迴圈10000次的sharedvariable += 1操作,然後列印出共享變數的結果。

/**

* 可見性測試demo

* * @author bo

* @date 2019-03-25

*/public class visibilitytest

}public static void main(string args) throws interruptedexception );

thread thread2 = new thread(() -> );

// 啟動執行緒

thread1.start();

thread2.start();

// 等待兩個執行緒執行結束

thread1.join();

thread2.join();

system.out.println("執行後共享變數的值為:" + test.sharedvariable);}}

講道理,這裡應該是要輸出執行後共享變數的值為:20000的,因為單執行緒裡呼叫兩次updatevar方法,sharedvariable的值就是20000,但實際上,我執行了n次(手都要點的酸死),執行的結果是10000個到20000之間的隨機數。

我們假設執行緒1和執行緒2同時開始執行,那麼第一次都會將sharedvariable = 0讀取到各自的cpu快取裡,執行了updatevar方法後,各自快取中的sharedvariable的值都是1,同時寫入記憶體後,記憶體中的sharedvariable是1而不是我們講道理的2,這就是快取的可見性問題

這裡因為兩個執行緒的啟動時有時差的,假設兩個執行緒是同時執行的,這裡返回的結果應該是10000

作業系統帶來的多程序大家都體驗過,我們可以一邊聽歌一邊聊天就是多程序帶來的好處

作業系統允許程序執行一段時間,假設100ms,過來100ms作業系統就會重新選擇程序來執行,這個就是上面提到的分時服用cpu的原理,作業系統以100ms作為時間片進行任務切換

在乙個時間片內,如果乙個程序進行乙個io操作,加入讀乙個檔案,這個時候程序可以把自己標記為「休眠狀態」並讓出cpu的使用權,等待檔案讀進記憶體,作業系統會把這個休眠的程序喚醒,喚醒後的程序就有機會重新獲得cpu的使用權

這個就是分時復用cpu的過程,這樣很好的提高了cpu的使用率

但是由於程序之間是不能進行記憶體空間的共享的,所以程序要做任務切換就要切換記憶體對映位址,而乙個程序建立的所有執行緒,都是共享乙個記憶體空間的,所以執行緒做任務切換成本就很低了,我們接下來講的就是對於執行緒的任務切換,也就是執行緒切換

我們現在使用的都是高階程式語言,高階程式語言裡一條語句需要多條cpu指令完成。舉個簡單的列子,上面的**中,sharedvariable += 1,在作業系統中至少需要三條cpu指令才能完成

在作業系統中,進行任務切換會發生在任何一條cpu指令執行完,假設任務切換發生在第一步,如下圖就是兩個執行緒的執行過程

這個時候就導致本來是兩次 +1 操作,結果到記憶體的實際值仍然是1

在化學上我們稱化學反應不可再分的基本微粒成為原子,如果我們不熟悉作業系統的執行規則,我們會預設為sharedvariable += 1就是乙個原子,可能知道有執行緒切換會發生在這個操作之前,也或者這個操作之後,就是不會發生在這個操作中間,這就是我們經常忽略的原子性問題

所謂原子性,我們把乙個或者多個操作在cpu執行的過程中不被中斷的特性成為原子性,就像化學反應上的原子一樣,是最小的單位了,不可分割

編譯器為了效能的優化,有時候會改變程式中語句的先後順序,有序性指的是程式按照**的先後順序執行,可能大家很疑惑這個怎麼也會導致併發問題呢?請看下面的乙個例項

public class singleton 

}return instance;

}}

上面這段**是乙個典型的雙重檢查建立例項物件,在獲取例項getinstance方法中,先判斷了例項instance == null,如果為空就鎖定singleton.class類,然後再檢查例項是否為空,如果還為空就建立乙個singleton例項。

此時兩個執行緒1和2同時呼叫getinstance方法,它們會同時都發現了例項為空,於是同時對singleton.class類進行了加鎖,這裡jvm保證只有乙個執行緒加鎖成功(不要問我為什麼,就是這樣的),這裡假設執行緒1成功獲取到了鎖,執行緒1就會去建立singleton例項,然後釋放鎖,執行緒2檢查instance == null時發現不為空,就不會再例項化了,這是不是很完美的**。

但實際上,這裡會出現乙個很隱蔽的問題,問題就在這個new singleton()中,按照例項化的順序,new操作是這樣的:

分配一塊記憶體區域m;

在記憶體m上初始化singleton物件;

然後m的位址賦值給instance變數。

但是實際上編譯器認為第2步和第3步對結果並沒有影響,所以根據實際情況進行了優化,變成了:

分配一塊記憶體區域m;

然後m的位址賦值給instance變數。

在記憶體m上初始化singleton物件;

這個時候如果執行緒1獲取到鎖後,開始例項化singleton物件,進行第2步操作結束後,作業系統進行了任務切換,此時執行緒2在進行第乙個instance == null檢查時,發現instance變數已經不為空,所以就直接返回了這個例項,但實際上這個例項還沒有進行初始化,當程式中使用這個例項進行獲取這個例項的成員變數時,就會出現npe異常了,這個就是有序性導致的併發問題

只要我們能夠深刻理解可見性、原子性、有序性在併發場景下的原理,很多併發bug都是可以解決、可以診斷的

在 32 位的機器上對 long 型變數進行加減操作存在併發隱患,到底是不是這樣的呢?

因為long型變數是64位,在32位cpu上執行寫操作,會被分成兩次寫操作,所以這個操作並不是原子性的,如果執行緒1在完成第一次寫操作後,出現執行緒切換,執行緒2將這個變數進行其他操作,就會導致執行緒1的這次操作存在問題

原子性,可見性,有序性

1.原子性 read,load,assign,use,store write 基本型別的訪問,讀寫 long,double 非原子性協定 monitorenter,monitorexit jvm lock,unlock,synchronized 2.可見性 volatile synchronized...

併發程式設計中的原子性問題,可見性問題,有序性問題。

原子性問題 在乙個執行緒中,對乙個32的二進位制數進行賦值操作,當低16位的資料寫入後,發生了中斷,而此時又有乙個執行緒去讀取這個寫入的資料,必定得到的是乙個錯誤的資料。在j a中這種情況是不存在的,因為對基本資料型別的寫入和賦值保證了原子性 i 10 但僅限制於對基本資料型別,而變數的賦值就不能保...

關於原子性, 可見性,有序性的思考

原子性是指 不會有中間狀態存在,要麼什麼都沒改變,要麼全都改變 對資料操作的原子性 在併發程式設計中,原子性存在的根本原因是,多個執行緒操作共享變數,由於執行緒間切換排程,導致乙個執行緒操作了另乙個執行緒 半成品 的資料,這是導致多執行緒環境下結果不可 的乙個原因.synchronized 的原子性...