固然原子性、可見性不只限於Java的併發編程中,這三種性質的問題是在全部併發編程中廣泛擁有的。java
在Java中,對基本數據的讀取與賦值操做是原子性的。算法
你們都知道原子是天然界中很基本的單位。數據庫
那什麼是原子性呢?學習過數據庫相關知識的人應該都知道數據庫事務中有ACID四個基本要素,其中A就是咱們所說的原子性(Atomicity),數據庫事務中,原子性的意思就是:一個事務,要麼執行,要麼不執行。簡單說就是不存在執行到一半的狀況,一個事務就是最小的單元。編程
「要麼有,要麼無。」緩存
首先咱們須要知道: 在Java中,對基本數據的讀取與賦值操做是原子性的。多線程
好比下面的代碼:併發
int i = 1;
把1賦值給i這個操做就是原子性的。學習
那就有同窗要問了:這就一句代碼,怎麼看都是一步操做到位,這不是明擺着就是「原子性」嗎?atom
非也非也,咱們假設賦值這個int類型的值時,是先賦值給低16位,再賦值給高16位,這樣一來,就成了兩步操做。若是恰好賦值給低16位的時候線程斷了,那麼i所賦的值就不必定是你想賦的值了。線程
是否是立刻感受到了原子性的重要性?
這裏須要注意的是Java中,自增語句不是原子性的:
i++;
實際上,自增是兩個操做,首先讀取i的值,而後再是賦值給i,兩個步驟都是具備原子性的,可是兩個原子性操做在一塊兒就再也不擁有原子性。
正確的方法可使用java.util.concurrent.atomic包中的好比AtomicInteger類,它可讓賦值讀取具備原子性。
在Java中,解決可見性問題的方法就是在你所需多線程訪問的變量在添加volatile關鍵詞。
咱們試想一下,當CPU老是去訪問物理內存去獲取變量,而後頻繁地去修改物理內存上的值,是否是太麻煩了?這將致使CPU花大部分的時間在獲取和修改物理內存的值。
因此,如今的CPU內部廣泛它本身的內存空間,咱們稱之爲 「CPU緩存」 。
那CPU緩存有什麼做用?百度百科中這樣描述:
CPU緩存(Cache Memory)是位於CPU與內存之間的臨時存儲器,它的容量比內存小的多可是交換速度卻比內存要快得多。高速緩存的出現主要是爲了解決CPU運算速度與內存讀寫速度不匹配的矛盾,由於CPU運算速度要比內存讀寫速度快不少,這樣會使CPU花費很長時間等待數據到來或把數據寫入內存。在緩存中的數據是內存中的一小部分,但這一小部分是短期內CPU即將訪問的,當CPU調用大量數據時,就可先緩存中調用,從而加快讀取速度。
一句話說就是,讓CPU處理指令更快更快更快更快更快更快更快更快。
** 可是與之而來的就是併發編程在多核CPU中的可見性問題。 ** 由於如今的電腦都是多核處理器,也就是一個CPU內部是由多個CPU組合而成。
不妨咱們作一個這樣的假設:
// Thread1 int i = 1; i = 10; // Thread2 int j = i;
咱們假定線程1在CPU1中執行,線程2在CPU2中執行。
具體流程以下:
首先線程1定義了i並將1賦值給了i,此時i被載入到主存(也就是物理上的內存)。
而後線程1執行「i=10」語句,CPU1會將修改後的i放入CPU緩存中,注意:並無直接載入到主存。
線程2中先獲取i的值,CPU2緩存中沒有i,因此就去獲取主存中的i值,然而主存中仍是i=1,這樣,j就被賦值爲1。
流程結束。如今咱們應該知道了什麼是可見性問題,並能感覺到問題的嚴重性。
** 事實上,很難模擬出這樣的結果。 ** 由於咱們沒法讓某個線程指定某個特定CPU,這是系統底層的算法,我想JVM應該也是無法控制的。還有最重要的一點,就是你沒法預測CPU緩存什麼時候會將值傳給主存,可能這個時間間隔很是短,短到你沒法觀察到。還有就是線程的執行的順序問題,由於多線程你沒法控制哪一個線程的某句代碼會在另外一個線程的某句代碼後面立刻執行。
那麼如何才能避免上述狀況?
Java提供volatile關鍵詞,只要將這個關鍵詞修飾在你考慮會出現這個問題的變量聲明前,就能夠避免可見性問題。
volatile int i = 1;
當添加該關鍵詞後,JVM會得知這個變量須要確保在應用中的可見性,而後通知系統:」這個變量直接在主存讀取修改,別放到CPU緩存裏了「。
「yes,sir!」
而後就不須要考慮可見性問題了。
固然解決可見性問題還有一招更靈,就是讓這個域徹底由synchronized方法或代碼塊來維護,那就沒必要在將其設置爲volatile了。 由於同步會致使向主存中刷新。
注意:當一個域的值依賴於它以前的值時(例如遞增一個計數器),volatile就沒法工做了。