1. 程式人生 > >併發程式設計-synchronized關鍵字大總結

併發程式設計-synchronized關鍵字大總結

  0、synchronized 的特點:

  可以保證程式碼的原子性和可見性。

  1、synchronized 的性質:

  可重入(可以避免死鎖、單個執行緒可以重複拿到某個鎖,鎖的粒度是執行緒而不是呼叫)、不可中斷(其實也就是上面的原子性)

  2、synchronized 的分類:

  按照作用物件劃分為 物件鎖、類鎖

  按照作用位置劃分為 程式碼塊、方法(靜態和非靜態)

  按照具體細節劃分為 例項(普通方法)同步方法、靜態同步方法、例項方法中的同步程式碼塊、靜態方法中的程式碼塊。

  如果從類是 Class 物件的角度看,類鎖也是物件鎖,但上面這樣劃分的解釋性更好。

  3、使用 synchronized 進行執行緒同步的七種情況:

  七種情況的測試程式碼見 這裡 GitHub上

  1、多個執行緒同時訪問一個物件的同步方法,因為多個執行緒要競爭這個物件的物件鎖,沒有拿到物件鎖的執行緒進入阻塞狀態,所以多個執行緒將序列執行這個方法,執行緒安全。

  2、多個執行緒訪問的是多個物件的同步方法,因為每個執行緒都可以拿到對於物件的物件鎖,所以多個執行緒可並行(多核處理器)執行這個方法,各個執行緒互不影響。

  3、多個執行緒訪問的是 synchronized 的靜態方法,因為多個執行緒要競爭物件對應的類物件(Class 物件)的類鎖,沒有拿到類鎖的執行緒進入阻塞狀態,所以多個執行緒將序列執行這個方法,執行緒安全。

  4、多個執行緒同時訪問一個物件的同步方法與非同步方法,因為執行緒執行這個物件的同步方法需要拿到物件鎖,而非同步方法不需要鎖,所以多個執行緒將序列執行同步方法,同步方法執行緒安全,非同步方法多個執行緒可以並行訪問。

  5、訪問一個物件的不同的普通同步方法,因為物件鎖只有一把,執行緒執行同步方法時都需要拿到物件的物件鎖,所以只有當一個執行緒把所有的同步方法都執行完(物件鎖的可重入性)後,這個物件鎖才被釋放,能夠被其他執行緒拿到。

  6、多個執行緒同時訪問靜態 synchronized 和非靜態 synchronized 方法,因為執行緒訪問靜態 synchronized 方法時需要拿到物件對應的類物件的類鎖,訪問非靜態 synchronized 方法時需要拿到物件的物件鎖,所以多個執行緒訪問靜態 synchronized 方法和非靜態 synchronized 方法時需要競爭這兩把鎖。

  7、執行緒在執行同步方法時丟擲異常,會自動釋放鎖,以便其他執行緒可以拿到鎖繼續執行。

  4、synchronized 的相關原理:

  加解鎖原理、可重入原理、可見性原理

  從反編譯看加解鎖原理:

  對於用 Java 編寫的程式碼,最常見的同步形式可能是 synchronized 方法。通常不使用 monitorenter 和 monitorexit 實現同步方法,而是通過 ACC_SYNCHRONIZED 標誌在執行時常量池中進行簡單區分,該標誌由方法呼叫指令檢查。

  —— Java 虛擬機器規範

  同步程式碼塊形式:

  public class SynchronizedCodeBlock {

  public void method() {

  synchronized (this) {

  // 空

  }

  }

  }

  使用 javap -verbose SynchronizedCodeBlock.class 命令反編譯結果如下圖:

  

 

  同步程式碼塊形式,使用 monitorenter 和 monitorexit 指令顯式對程式碼塊加解鎖。

  同步方法形式:

  public class SynchronizedMethod {

  public synchronized void method() {

  // 空

  }

  }

  使用javap -verbose SynchronizedMethod.class 命令反編譯結果如下圖:

  

圖片描述

 

  同步方法形式,使用 ACC_SYNCHRONIZED 標記隱式對方法加解鎖。

  方法測試可重入性原理:

  程式碼:

  public class ReentrancyTest {

  public synchronized void method1() {

  System.out.println(執行緒 + Thread.currentThread().getName() + 執行 method1);

  method2(); // 測試可重入性

  }

  public synchronized void method2() {

  System.out.println(執行緒 + Thread.currentThread().getName() + 執行 method2);

  }

  public static void main(String[] args) throws InterruptedException {

  Reentrancy reentrancy = new Reentrancy();

  // 方法體執行物件

  Runnable run = ()- {

  reentrancy.method1();

  };

  Thread thread1 = new Thread(run);

  Thread thread2 = new Thread(run);

  // 啟動執行緒

  thread1.start();

  thread2.start();

  // 主執行緒等待子執行緒執行完畢

  thread1.join();

  thread2.join();

  System.out.println(Finished);

  }

  }

  輸出結果為:

  執行緒Thread-0執行 method1

  執行緒Thread-0執行 method2

  執行緒Thread-1執行 method1

  執行緒Thread-1執行 method2

  Finished

  JVM 負責跟蹤物件被加鎖的次數,執行緒第一次給物件加鎖的時候,monitor 的計數變為 1,每當這個相同的執行緒在此物件上再次獲得鎖時,計數為遞增。當任務離開時,monitor 的計數減 1,當計數為 0 時,鎖被完全釋放。

  圖說明可見性原理:

  JMM 是 Java 記憶體模型的縮寫,直接看圖。

  

 

  5、synchronized 的缺陷:

  效率低:鎖釋放情況少(一種是程式碼正常執行結束釋放鎖,另一種是產生異常釋放鎖),試圖獲得鎖時不能設定超時,不能中斷一個正在試圖獲得鎖的執行緒。

  不夠靈活(相比於讀寫鎖,讀操作不需要加鎖,寫操作才需要加鎖),加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個物件)可能是不夠的。

  無法知道是否成功獲取到了鎖。

  6、synchronized 常見的面試題:

  1、使用注意點:鎖物件不能為空(如果是物件,可以用物件自身的鎖,還可以自己造一個鎖來讓執行緒競爭)、鎖的作用域不易過大、避免死鎖(可以用 synchronized 模擬死鎖)。

  2、如何選擇 Lock 和 Synchronized 關鍵字?

  一切選擇都要根據具體的業務需要,選擇更合適的方式,減少出錯。如果有現成的包(java.util.concurrent)和類(原子類、ConcurrentHashMap、CountDownLatch),就不要自己造輪子了,直接拿來用,Java 原生支援的效率也會更高些,如果真的要用到 Lock 或者 Synchronized 獨有的特性,再來考慮這兩個。

  3、多執行緒訪問同步方法的各種具體情況(也就是上面的七種情況)。

  7、對 synchronized 的思考(面試可能也會被問到):

  1、多個執行緒等待同一個 synchronized 鎖時,JVM 如何選擇下一個獲取鎖的是哪個執行緒?

  這個問題就涉及到內部鎖的排程機制,執行緒獲取 synchronized 對應的鎖,也是有具體的排程演算法的,這個和具體的虛擬機器版本和實現都有關係,所以下一個獲取鎖的執行緒是事先沒辦法預測的。

  2、synchronized 使得同時只有一個執行緒可以執行,效能較差,有什麼方法可以提升效能?

  優化 synchronized 的使用範圍,讓臨界區的程式碼在符合要求的情況下儘可能的小。

  使用其他型別的 lock(鎖),synchronized 使用的鎖經過 jdk 版本的升級,效能已經大幅提升了,但相對於更加輕量級的鎖(如讀寫鎖)還是偏重一點,所以可以選擇更合適的鎖。

  3、如何想要更加靈活的控制鎖的獲取和釋放,怎麼辦?

  可以根據需要實現一個 Lock 介面,這樣鎖的獲取和釋放就能完全被我們控制了。

  4、什麼是鎖的升級、降級?

  JDK6 之後,不斷優化 synchronized,提供了三種鎖的實現,分別是偏向鎖、輕量級鎖、重量級鎖,還提供自動的升級和降級機制。對於不同的競爭情況,會自動切換到合適的鎖實現。當沒有競爭出現時,預設使用偏斜鎖,也即是在物件頭的 Mark Word 部分設定執行緒ID,來表示鎖物件偏向的執行緒,但這並不是互斥鎖;當有其他執行緒試圖鎖定某個已被偏斜過的鎖物件,JVM 就撤銷偏斜鎖,切換到輕量級鎖,輕量級鎖依賴 CAS 操作物件頭的 Mark Word 來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖;否則進一步升級為重量級鎖。鎖的降級發生在當 JVM 進入安全點後,檢查是否有閒置的鎖,並試圖進行降級。鎖的升級和降級都是出於效能的考慮。

  5、什麼是 JVM 裡的偏向鎖、輕量級鎖、自旋鎖、重量級鎖?

  偏向鎖:線上程競爭不激烈的情況下,減少加鎖和解鎖的效能損耗,在物件頭中儲存獲得鎖的執行緒ID資訊,如果這個執行緒再次請求鎖,就用物件頭中儲存的ID和自身執行緒ID對比,如果相同,就說明這個執行緒獲取鎖成功,不用再進行加解鎖操作了,省去了再次同步判斷的步驟,提升了效能。

  輕量級鎖:再執行緒競爭比偏向鎖更激烈的情況下,線上程的棧記憶體中分配一段空間作為鎖的記錄空間(輕量級鎖對應的物件的物件頭欄位的拷貝),執行緒通過CAS競爭輕量級鎖,試圖把物件的物件頭欄位改成指向鎖記錄的空間,如果成功就說明獲取輕量級鎖成功,如果失敗,則進入自旋(一定次數的迴圈,避免執行緒直接進入阻塞狀態)試圖獲取鎖,如果自旋到一定次數還不能獲取到鎖,則進入重量級鎖。

  自旋鎖:獲取輕量級鎖失敗後,避免執行緒直接進入阻塞狀態而採取的迴圈一定次數去嘗試獲取鎖。(執行緒進入阻塞狀態和非阻塞狀態都是涉及到系統層面的,需要在使用者態到核心態之間切換,非常消耗系統資源)實驗證明,鎖的持有時間一般是非常短的,所以一般多次嘗試就能競爭到鎖。

  重量級鎖:在 JVM 中又叫做物件監視器(monitor),鎖物件的物件頭欄位指向的是一個互斥量,多個執行緒競爭鎖,競爭失敗的執行緒進入阻塞狀態(作業系統層面),並在鎖物件的一個等待池中等待被喚醒,被喚醒後的執行緒再次競爭鎖資源。

  6、使用 synchronized 實現雙重校驗鎖的單例模式?

  單例物件加上 volatile 關鍵字,可以禁止JVM對指令重排序,因為下面第一處的程式碼 doubleCheckSingleton = new DoubleCheckSingleton(); 分為三個步驟:1、為 doubleCheckSingleton 分配記憶體空間;2、初始化 doubleCheckSingleton 物件的值;3、doubleCheckSingleton 指向分配的記憶體地址,所以執行的步驟可能變成 1、3、2,可能建立多個單例物件,所以給單例物件加上 volatile 關鍵字是很有必要的。

  雙重校驗是指兩次檢查,一次是檢查單例物件是否建立好了,如果還沒有建立好,就第一次建立單例物件時,並在建立過程中鎖住單例類(類鎖),第二次的檢查避免了一個執行緒在建立單例物件的過程中,也有其他執行緒也已經通過第一次非 null 判斷,當這個執行緒釋放類鎖後,其他執行緒不知道單例物件已經建立好了,而再次建立。在第一次建立例項物件時才需要雙重校驗,synchronized 才有用武之地,後面只需要一次校驗,提高了效能。

  程式碼:

  public class DoubleCheckSingleton {

  // 單例物件

  private volatile static DoubleCheckSingleton doubleCheckSingleton;

  // 建構函式私有化

  private DoubleCheckSingleton() {}

  // 獲取單例物件的方法

  public static DoubleCheckSingleton getInstance() {

  if(doubleCheckSingleton == null) {

  synchronized (DoubleCheckSingleton.class) {

  if (doubleCheckSingleton == null) {

  doubleCheckSingleton = new DoubleCheckSingleton(); // 第一處

  }

  }

  }

  return doubleCheckSingleton;

  }

  }

  作者:Wizey

  連結:https://www.imooc.com/article/270620

  來源:慕課網

  本文首次釋出於慕課網 ,轉載請註明出處,謝謝合作