java synchronized實現可見性對比volatile


問題: 

 大家可以先看看這個問題,看看這個是否有問題呢? 那里有問題呢?

public class ThreadSafeCache {
    int result;
    
    public int getResult() {
        return result;
    }
    
    public synchronized void setResult(int result) {
        this.result = result;
    }
    
}

  如果你在這個問題上面停留超過5s的話,那么表示你對這塊某些知識還有點模糊,需要再鞏固下,下面我們一起來分析下!

1. 結論

  多線程並發的同時進行set、get操作,A線程調用set方法,B線程並不一定能對這個改變可見!!!

2. 分析  

  這個類非常簡單,里面有一個屬性,有2個方法:get、set方法,一個用來設置屬性值,一個用來獲取屬性值,在設置屬性方法上面加了synchronized。

  隱式信息:多線程並發的同時進行set、get操作,A線程調用set方法,B線程可以里面感知到嗎???

  說到這里,問題就變成了synchronized在剛剛說的上下文下面能否保證可見性!!!

3. 關鍵詞synchronized的用法

  • 指定加鎖對象:對給定對象加鎖,進入同步代碼前需要獲得給定對象的鎖。

  • 直接作用於實例方法:相當於對當前實例加鎖,進入同步代碼前要獲得當前實例的鎖。

  • 直接作用於靜態方法:相當於對當前類加鎖,進入同步代碼前要獲得當前類的鎖。

  synchronized它的工作就是對需要同步的代碼加鎖,使得每一次只有一個線程可以進入同步塊(其實是一種悲觀策略)從而保證線程之間得安全性。

  從這里我們可以知道,我們需要分析的屬於第二類情況,也就是說多個線程如果同時進行set方法的時候,由於存在鎖,所以會一個一個進行set操作,並且是線程安全的,但是get方法並沒有加鎖,表示假如A線程在進行set的同時B線程可以進行get操作。並且可以多個線程同時進行get操作,但是同一時間最多只能有一個set操作。

4. Java 內存模型 happens-before原則  

 JSR-133 內存模型使用 happens-before 的概念來闡述操作之間的內存可見性。在 JMM 中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在 happens-before 關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。

 與程序員密切相關的 happens-before 規則如下:

  • 程序順序規則:一個線程中的每個操作,happens-before 於該線程中的任意后續操作。

  • 監視器鎖規則:對一個監視器的解鎖,happens-before 於隨后對這個監視器的加鎖。

  • volatile 變量規則:對一個 volatile 域的寫,happens-before 於任意后續對這個 volatile 域的讀。

  • 傳遞性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

注意,兩個操作之間具有 happens-before 關系,並不意味着前一個操作必須要在后一個操作之前執行!happens-before 僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。

其中有監視器鎖規則:對一個監視器的解鎖,happens-before 於隨后對這個監視器的加鎖。這一條,僅僅只是針對synchronized的set方法,而對於get並沒有這方面的說明。

其實在這種上下文下面一個synchronized的set方法,一個普通的get方法,a線程調用set方法,b線程並不一定能對這個改變可見!

5. volatile

 volatile可見性
  前面happens-before原則就提到:volatile 變量規則:對一個 volatile 域的寫,happens-before 於任意后續對這個 volatile 域的讀。volatile從而保證了多線程下的可見性!!!

 volatile 禁止內存重排序
  下面是 JMM 針對編譯器制定的 volatile 重排序規則表:

 為了實現 volatile 的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

 下面是基於保守策略的 JMM 內存屏障插入策略:

  • 在每個 volatile 寫操作的前面插入一個 StoreStore 屏障。

  • 在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障。

  • 在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障。

  • 在每個 volatile 讀操作的后面插入一個 LoadStore 屏障。

 下面是保守策略下,volatile 寫操作 插入內存屏障后生成的指令序列示意圖:

 

 下面是在保守策略下,volatile 讀操作 插入內存屏障后生成的指令序列示意圖:

 

  上述 volatile 寫操作和 volatile 讀操作的內存屏障插入策略非常保守。在實際執行時,只要不改變 volatile 寫-讀的內存語義,編譯器可以根據具體情況省略不必要的屏障。

  雙重檢查鎖實現單例中就需要用到這個特性!!!

6. 模擬

  通過上面的分析,其實這個題目涉及到的內容都提到了,並且進行了解答。

  雖然你知道的原因,但是想模擬並不是一件容易的事情!,下面我們來模擬看看效果:

public class ThreadSafeCache {
    int result;

    public int getResult() {
        return result;
    }

    public synchronized void setResult(int result) {
        this.result = result;
    }

    public static void main(String[] args) {
        ThreadSafeCache threadSafeCache = new ThreadSafeCache();

        for (int i = 0; i < 8; i++) {
            new Thread(() -> {
                int x = 0;
                while (threadSafeCache.getResult() < 100) {
                    x++;
                }
                System.out.println(x);
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        threadSafeCache.setResult(200);
    }
}

效果:

 

  程序會一直卡在這邊不動,表示set修改的200,get方法並不可見!!!

  添加volatile 關鍵詞觀察效果

  其實例子中synchronized關鍵字可以去掉,僅僅用volatile即可。

 

效果:

 

代碼很快正常結束了!

  結論:多線程並發的同時進行set、get操作,A線程調用set方法,B線程並不一定能對這個改變可見!!!,上面的代碼中,如果對get方法也加synchronized也是可見的,還是happens-before的監視器鎖規則:對一個監視器的解鎖,happens-before 於隨后對這個監視器的加鎖。只是volatile比synchronized更輕量級,所以本例直接用volatile。但是對於符合非原子操作i++這里還是不行的還是需要synchronized。 


 synchronized實現可見性對比volatile

  首先先介紹一下JMM(JAVA內存模型),上圖:

 

  java內存模型的工作原理如上圖所示,一些被定義的變量都存放在主內存中,當一個線程想要修改一個變量的值時,那么這個變量會從主內存中拷貝到線程的工作內存(CPU緩存)中。之后線程對變量值做了更改,又會重新拷貝回主內存中。大家通過描述也可以看出來這些操作是分步執行的,這樣就無法保證可見性和原子性。對於這種情況java也給出了很多解決辦法,今天跟大家分享一下我對synchronized以及volatile的理解。

 

 大家知道synchronized是通過加互斥鎖來實現原子性的,JMM關於synchronized的兩條規定:

  (1) 線程解鎖前,必須把共享變量的最新之刷新到主內存中
  (2) 線程加鎖前,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖與解鎖需要時同一把鎖)
  

  我來簡單描敘一下線程執行互斥代碼的過程:

    1、獲得互斥鎖
    2、清空工作內存
    3、從主內存拷貝變量的最新副本到工作內存
    4、執行代碼
    5、將更改后的共享變量的值刷新到主內存
    6、釋放互斥鎖
  synchronized從而實現類原子性,也具備內存可見性。

  這里多說一下Lock,其實原理跟synchronized類似,但是比synchronized更加靈活,我們會在下一篇博客中詳細探討synchronized的缺陷以及Lock的基本用法。

  volatile是如何實現內存可見性的呢?

  深入來說:是通過加入內存屏障和禁止重排序優化來實現的。(重排序指單線程中在保證執行結果不變的前提下java虛擬機為了提升處理速度可能會將指令重排,達到最合理化)

  對volatile變量執行寫操作時,會在寫操作后加入一條store屏障指令
  改變線程工作內存中的volatile變量副本的值
  將改變后的副本的值從工作內存刷新到主內存
 

  對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令
  從主內存中讀取volatile變量的最新值到線程的工作內存中
  從工作內存中讀取volatile變量的副本
  簡單來說:volatile變量在每次被線程訪問時,都強迫從sy主內存中重讀變量的值,而當該變量發生變化時,又會強迫線程將最新的值刷新到主內存。這樣在任何時刻,不同的線程總能看到該變量的最新值。從而保證了變量的內存可見性。

 

 synchronized和volatile的比較

  volatile不需要加鎖,比synchronized更加輕量級,不會阻塞線程
  從內存可見性講,volatile讀相當於加鎖,volatile寫相當於解鎖
  synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM