5.多線程之內存可見性

共享變量在線程間的可見性

共享變量:如果一個變量在多個線程的工作內存中都存在副本,
那麼這個變量就是這幾個線程的共享變量
可見性:一個線程對共享變量值的修改,能夠及時的被其他線程看到
Java內存模型(JMM,Java Memory Model):
描述了java程序中各種變量(線程共享變量)的訪問規則,以及在JVM中將
變量存儲到內存和從內存中讀取出變量這樣的底層細節。
所有的變量都存儲在主內存中
每個線程都有自己獨立的工作內存,裏面保存該線程使用到的變量的副本
(主內存中該變量的一份拷貝)

    兩條規定:    
       線程對共享變量的所有操作都必須在自己的工作內存(working memory,是cache和寄存器的一個抽象,而並不是內存中的某個部分,這個解釋源於《Concurrent Programming in Java: Design Principles and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有不少人覺得working memory是內存的某個部分,這可能是有些譯作將working memory譯爲工作內存的緣故,爲避免混淆,這裏稱其爲工作存儲,每個線程都有自己的工作存儲)中進行,不能直接從相互內存中讀寫
     不同線程之間無法直接訪問其他線程工作內存中的變量,
     線程間變量值得傳遞需要通過主內存來完成
    共享變量可見性的實現原理
     把工作內存1中更新過的共享變量刷新到主內存中
     將主內存中最新的共享變量的值更新到工作內存2中
    Java語言層面支持的可見性實現方式:
     synchronized
     volatile
     final也可以保證內存可見性

synchronized實現可見性

可以實現互斥鎖(原子性),即同步。但很多人都忽略其內存可見性這一特性
JMM關於synchronized的兩條規定:
線程解鎖前,必須把共享變量的最新值刷新到主內存中
線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從內存中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)
線程解鎖前對共享變量的修改在下棋甲所示對其他線程可見
·導致共享變量在線程間不可見的原因:
1.線程的交叉執行(保證原子性,使用synchronized關鍵字)
2.重排序結合線程交叉執行(原子性)
3.共享變量更新後的值沒有在工作內存與主內存間及時更新(可見性)
在Java運行過程中,執行引擎會盡量揣摩用戶的意圖,所以很多時候都會看到正確的結果,但是哪怕只有一次不可預期的結果出現影響也是非常大的,所以,在需要內存可見性的時候,我們一定要保證線程的安全

volatile實現可見性

  ·指令重排序
        代碼書寫的順序與實際執行的順序不同,指令衝排序是編譯器或處理器
        爲了提高程序性能而做的優化
           1.編譯器優化的重排序(編譯器優化)
           2.指令級並行重排序(處理器優化)
           3.內存系統的重排序(處理器優化)
        重排序不會給單線程帶來內存可見性的問題(因爲as-if-serial語義)
        多線程中程序交錯執行時,重排序可能會造成內存可見性問題
  ·as-if-serial語義
         無論怎樣重排序,程序執行的結果應該玉帶啊順序執行的結果一致(Java
       編譯器、運行時和處理器都會保證Java在單線程下遵循as-if-serial語義)

  ·volatile關鍵字使用注意事項
       1.能夠保證volatile變量的可見性(原理與synchronized關鍵字原理差不多)
            深入來說,通過加入內存屏障和禁止重排序優化來實現內存可見性。當對volatile變量執行寫操作時,會在寫操作後加入一條store屏障指令;當對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令
       2.不能保證volatile變量複合操作的原子性
             int num = 0;
             num++;//++操作非原子操作,分3步執行
             保證原子性的方法:
                  synchronized關鍵字
                  ReentrantLock可傳入鎖對象
                  AtomicInterger對象
        3.volatile適用場合
           在多線程中安全的使用volatile變量必須同時滿足兩個條件:
           ①對變量的寫入操作不依賴其當前值,如number++不可以,
                                              boolean變量可以
           ②該變量沒有包含在具有其他變量的不變式中,如果有多個
    volatile變量,則每個volatile變量必須獨立於其他的volatile變量

synchronized和volatile比較

   volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程,效率更高
   從內存可見性角度講,volatile讀相當於加鎖,volatile寫相當於解鎖
   synchronized技能保證可見性,又能保證原子性,而volatile只能保證可見性,不能保證原子性。
   如果能用volatile解決問題,還是應儘量使用volatile,因爲它的效率更高 

一個需要注意的點:
問:即使沒有保證可見性的措施,很多時候共享變量一人能夠在主內存和工作內存見得到及時的更新?
答:一般只有在短時間內高併發的情況下才會出現變量得不到及時更新的情況,因爲CPU在執行時會很快的刷新緩存,所以一般情況下很難看到這種問題,而且也與硬件性能有很大的關係,所以,結果都是不可預測的,正式因爲不可預測,所以我們纔要保證線程的安全問題
另:java中long、double是64位的,其讀寫會分成兩次32位的操作,並不是原子操作,但很多商用虛擬機都進行了優化,所以,瞭解即可

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