1. 程式人生 > >為什麼volatile關鍵字保證不了執行緒安全

為什麼volatile關鍵字保證不了執行緒安全

在當前高併發的時代,不懂一點高併發多執行緒都不好意思出去,即使沒地方使用,網上大多數相關文件部落格也都講解了這些部分。

    我並不想具體介紹什麼是volatile,我寫這篇部落格目的是說明白為什麼volatile保證不了執行緒安全。想要執行緒安全必須保證原子性,可見性,有序性。而volatile只能保證可見性和有序性

    在說明這個問題之前,首先還是要說明下cpu和記憶體,cpu和記憶體直接是有快取記憶體的,一般分為多級。cpu首先是要從記憶體中讀取一個數據進快取,然後從快取中讀取進行操作,將結果返回給快取,再把快取寫回記憶體。

    如果同一個變數i=0,有兩個執行緒執行i++方法,執行緒1把i從記憶體中讀取進快取,而現線上程2也把i讀取進快取,兩個執行緒執行完i++後,執行緒1寫回記憶體,i = 1,執行緒2也寫回記憶體i = 1,兩次++結果最終值為1,這就是著名的快取一致性問題。為了解決這個問題,前人給了兩種方案

    匯流排鎖

    快取一致性協議

    cpu為了和各個硬體打交道方便,設計師們把每個硬體都連線一個線到cpu,但是發現這樣太麻煩了,所以改為所有硬體都掛在總線上,cpu通過匯流排和各個硬體打交道。如果使用匯流排鎖,就阻塞了其他cpu和其他硬體互動(記憶體之類,磁碟,等等),i++這條語句就必須執行完了,其他cpu才能執行,否則只能一個cpu去和硬體互動。這也是一種解決辦法,問題也明顯,特別效率低下。

    為了解決這個問題提出快取一致性協議,具體協議就不講,簡單解釋一下,如果我寫入之後發現這是共享變數就使得其他cpu快取了的值失效,讓它再次去記憶體中讀取。

下面這段話摘自《深入理解Java虛擬機器》:

  “觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令”

  lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:

  1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;

  2)它會強制將對快取的修改操作立即寫入主存;

  3)如果是寫操作,它會導致其他CPU中對應的快取行無效。

    解釋一下這段話的內容,重排序的指程式碼的真正執行過程可能不是程式碼書寫的順序,這是為了是cpu流水線作業提高cpu的利用率而優化的一門技術。

    而lock字首指令(記憶體屏障),一個屏障會把這個屏障前寫入的資料重新整理到記憶體,這樣任何試圖讀取該資料的執行緒將得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆CPU執行的。如果你的欄位是volatile,Java記憶體模型將在寫操作後插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。

    這樣咋一看貌似可以保證執行緒的安全性呀,為啥不能保證呢

    這樣如果有一個變數i = 0用volatile修飾,兩個執行緒對其進行i++操作,如果執行緒1從記憶體中讀取i=0進了快取,然後把資料讀入暫存器,之後時間片用完了,然後執行緒2也從記憶體中讀取i進快取,因為執行緒1還未執行寫操作,記憶體屏障是插入在寫操作之後的指令,意味著還未觸發這個指令,所以快取行是不會失效的。然後執行緒2執行完畢,記憶體中i=1,然後執行緒1又開始執行,然後將資料寫回快取再寫回記憶體,結果還是1。