京東面試題:說說synchronized和volatile的區別

語言: CN / TW / HK

theme: orange highlight: dracula


今天來介紹一下Java面試中最常會被面試官提到的問題,也是Java多執行緒中經常被問到的問題:synchronized和volatile的區別,希望能夠幫助到Java相關方面的求職者。

Java記憶體模型(JMM)

提到這兩個有關於執行緒的關鍵字,那麼我們不得不提到Java的記憶體模型了(JMM),下面我們先看一下 Java記憶體模型 在處理多執行緒方面的工作原理圖。

Java記憶體模型(JMM).png

Java記憶體模型(java Memory Model) 描述了Java程式中各種變數(執行緒共享變數)的訪問規則,以及在JVM中將變數儲存到記憶體和從記憶體中讀取出變數這樣的底層細節。

首先介紹兩個概念

可見性:一個執行緒對共享變數值的修改,能夠及時地被其他執行緒看到。
共享變數:如果一個變數在多個執行緒的工作記憶體中都存在副本,那麼這個變數就是這幾個執行緒的共享變數。

共享變數可見性實現的原理

執行緒1對共享變數的修改要想被執行緒2及時看到,必須要經過如下兩個步驟:

  • 把工作記憶體1中更新過的共享變數重新整理到主記憶體中
  • 將主記憶體中最新的共享變數的值更新到工作記憶體2中

下圖為一個共享變數實現可見性原理的一個示例:

共享變數可見性實現原理.png

其中,執行緒對共享變數的操作,遵循以下兩條規則:

  • 執行緒對共享變數的所有操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫
  • 不同執行緒之間無法直接訪問其他執行緒工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體來完成

可見性

要實現共享變數的可見性,必須保證兩點:

  • 執行緒修改後的共享變數值能夠及時從工作記憶體重新整理到主記憶體中
  • 其他執行緒能夠及時把共享變數的最新值從主記憶體更新到自己的工作記憶體中

可見性的實現方式 - synchronized - volatile

synchronoized實現可見性

  • 原子性(同步)
  • 可見性

JMM關於synchronized的兩條規定

  • 執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體中
  • 執行緒加鎖時,將清空工作記憶體中共享變數的值,從而使用共享變數時,需要從主記憶體中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)

注意:執行緒解鎖前對共享變數的修改在下次加鎖時對其他執行緒可見

執行緒執行互斥程式碼的過程

  1. 獲得互斥鎖
  2. 清空工作記憶體
  3. 從主記憶體拷貝變數的最新副本到工作的記憶體
  4. 執行程式碼
  5. 將更改後的共享變數的值重新整理到主記憶體
  6. 釋放互斥鎖

重排序

程式碼書寫的順序與實際執行的順序不同,指令重排序是編譯器或處理器為了提高程式效能而做的優化

  • 編譯器優化的重排序(編譯器優化)
  • 指令級並行重排序(處理器優化)
  • 記憶體系統的重排序(處理器優化)

volatile實現可見性

  • volatile關鍵字能夠保證volatile變數的可見性
  • 不能保證volatile變數複合操作的原子性

volatile如何實現記憶體可見性

深入來說:通過加入記憶體屏障和禁止重排序優化來實現的

  • volatile變數執行寫操作時,會在寫操作後加入一條store屏障指令
  • volatile變數執行讀操作時,會在讀操作前加入一條load屏障指令

執行緒寫volatile變數的過程

  1. 改變執行緒工作記憶體中volatile變數副本的值
  2. 將改變後的副本的值從工作記憶體重新整理到主記憶體

執行緒讀volatile變數的過程

  1. 從主記憶體中讀取volatile變數的最新值到執行緒的工作記憶體中
  2. 從工作記憶體中讀取volatile變數的副本

下圖是volatile不能實現原子性的示例:

``` volatile不能保證volatile變數複合操作的原子性:

private int number = 0;                                  
number++; // 不是原子操作

加入synchronized,變為原子操作: synchronized(this) {    number++; } ```

實現原子操作解決方案

保證number自增操作的原子性: - 使用synchronized關鍵字 - 使用ReentrantLock - 使用AtomicInteger

可重入鎖案例

// Reentranlock public int increase(){     lock.lock();     try {          number++;     } finally {          lock.onlock();     } }

volatile適用場合

要在多執行緒中安全的使用volatile變數,必須同時滿足:

  • 對變數的寫入操作不依賴其當前值
    • 不滿足:number++、count=count*5 等
    • 滿足:boolean變數、記錄溫度變化的變數等
  • 該變數沒有包含在具有其他變數的不變式中

總結

synchronized和volatile的區別

  1. volatile不需要加鎖,比synchronized更輕量級,不會阻塞執行緒;
  2. 從記憶體可見性角度,volatile讀相當於加鎖,volatile寫相當於解鎖;
  3. synchronized既能夠保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性。