方塊人 Java併發——volatile關鍵字

方塊人 Java併發——volatile關鍵字
什麼是內存可見性?
  這裏就要提一下JMM(Java內存模型)。當線程在運行的時候,並不是直接直接修改電腦主內存中的變量的值。線程間通訊也不是直接把一個線程的變量的值傳給另一個線程,讓其刷新變量。下面是一副抽象的結構圖。

  線程A要想和線程B通信,其實是通過改變主內存中的共享變量的值。具體的工作原理就是,線程A不能直接修改主內存中的值,而是在主內存和線程A中需要一個緩存區(每個線程都有自己的一個緩衝區),再將共享變量的副本拷入緩衝區,在緩衝區裏修改完之後再通過一些指令將副本改變的值刷新進主內存。這裏刷新就會出現很多問題,有可能線程A修改了共享變量的值沒有刷新進去,當B需要使用共享變量的時候就會用舊值。所以這就是多線程存在一個比較大的問題。前面介紹的synchronized關鍵字,以及現在介紹的volatile,後面會介紹的CAS,其實都會解決這個內存可見性的問題。不過實現原理不同。要保證內存可見性也就是每一個線程修改一次共享變量的值,都需要讓主內存中的變量刷新,並且讓其他線程也刷新共享變量副本。

volatile如何實現內存可見性?
  其實volatile的可見性底層原理主要是內存屏障和禁止重排序。內存屏障是在volatile變量讀操作之前加入load指令,從主內存中讀取最新的共享變量,而不是使用之前的副本值,同樣寫的時候也是加入store指令,將本地內存中的共享變量值強制刷新到主內存中。這樣就保證了每個線程拿到的volatile變量是最新的值。下面是兩個示意圖

StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。

StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能

LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。也就是

LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。

  上面四個屏障不但會強制變量刷新,而且會防止指令之間的重排序(JVM對於沒有數據依賴關係的指令會進行重排序,指令重排序也會導致變量的值讀取是錯誤的)。volatile的底層就是這樣實現,較synchronized更爲輕量級,畢竟synchronized是用來修飾方法和代碼塊的,而volatile保證的只是一個變量,所以更爲輕量級。

如何優化volatile關鍵字?
  我們先來弄清楚對於英特爾酷睿i7、酷睿、Atom和 NetBurst,以及Core Solo和Pentium M處理器的L1、L2或L3緩存的高速緩存行是64個字節寬,不支持部分填充緩存行。要想優化volatile變量運行速度,只需要將變量追加到64個字節

  也就是如果一個volatile變量存入高速緩存且不足64個字節長度,這時候可能在同一個緩存行中就會再存入另一個volatile變量,而由於緩存一致性機制,當處理第一個volatile變量的時候,整個緩存行是鎖定的,這時候第二個volatile變量或者緩存行其他變量需要被其他處理器操作就必須等到第一個volatile變量操作完才能進行。如果這時volatile變量是64個字節獨佔一個緩存行的時候,那麼就不會有上面的影響發生。

  那麼什麼時候我們都需要追加64個字節嗎?有下面兩種情況不需要追加

  1、緩存行非64字節寬的處理器。如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節寬。

  2、共享變量不被頻繁地寫。可以看出追加字節的方式是需要性能消耗的,如果共享變量不被頻繁地寫的話,鎖定的概率小,不需要追加字節。

總結
  volatile是修飾變量的一個關鍵字,寫在基本數據類型之前。用來保證這個變量在多線程的環境下具有可見性。是通過內存屏障和禁止指令重排序來實現的,但是不具有原子性。CAS和synchronized纔會保證原子性。
原文地址https://www.cnblogs.com/Cubemen/p/10767255.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章