是時候來嘮一嘮synchronized關鍵字了,Java多執行緒的必問考點!

JavaBuild發表於2024-03-23

寫在開頭

在之前的博文中,我們介紹了volatile關鍵字,Java中的鎖以及鎖的分類,今天我們花5分鐘時間,一起學習一下另一個關鍵字:synchronized

synchronized是什麼?

首先synchronized是Java中的一個關鍵字,所謂關鍵字,就是Java中根據底層封裝所賦予的一種具有特殊語義的單詞,而synchronized譯為同步之意,可保證在同一時刻,被它修飾的方法或程式碼塊只能有一個執行緒執行,它的使用解決了併發多執行緒中的三大問題:原子性、可見性、順序性

很多小夥伴在過往的書籍中可能會看到說synchronized是一種重量級鎖,效能差,不建議在程式碼中使用,其實這是早期的synchronized特點,自JDK1.6之後,synchronized 引入了大量的最佳化如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷,這些最佳化讓 synchronized 鎖的效率提升了很多。因此, synchronized 還是可以在實際專案中使用的,像 JDK 原始碼、很多開源框架都大量使用了 synchronized 。

synchronized的使用

synchronized在Java中主要的3種使用方式:

  1. 修飾例項方法: 為當前物件例項加鎖,進入同步方法需要先獲取物件鎖;
  2. 修飾靜態方法: 為當前類加鎖,鎖定的是Class物件,進入同步方法需要先獲取類鎖;
  3. 修飾程式碼塊: 為指定物件加鎖,進入同步方法需要先獲取指定物件的鎖。

樣例:

//修飾例項方法,為當前例項加鎖
synchronized void method() {
    //業務程式碼
}
//修飾靜態方法,鎖為當前Class物件
synchronized static void method() {
    //業務程式碼
}
//修飾程式碼塊,鎖為括號裡面的物件
synchronized(this) {
    //業務程式碼
}

寫到這裡,突然想到了3個面試可能會考的知識點,列舉一下!

問題1:synchronized修飾程式碼塊可以給類加鎖嗎?

當然可以!我們前面說了修飾程式碼塊時,是給程式碼中的物件加鎖,這裡面的物件既可以是例項也可以是類。

問題2:靜態 synchronized 方法和非靜態 synchronized 方法之間的呼叫互斥麼?

不互斥!如果執行緒A呼叫一個例項物件的非靜態synchronized方法,執行緒B同時去呼叫這個例項物件所屬類的靜態synchronized方法並不會發生互斥,因為執行緒A此時拿到的是例項物件鎖,而執行緒B拿到的是當前類的鎖。

問題3:構造方法可以用 synchronized 修飾麼?

不可以!構造方法本身就是執行緒安全的,在Java開發規範裡也明確告訴我們

構造方法不能是抽象的(abstract)、靜態的(static)、最終的(final)、同步的(synchronized)。

synchronized的底層原理

在synchronized的底層(JVM層面),針對方法與程式碼塊的實現邏輯是不同的,因此我們在分析底層原理是也要分別來看。

1、當synchronized修飾方法時

public class Test {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

我們透過對編譯後的class檔案進行反編譯後,分析其底層實現。

知識點擴充套件:
我們透過javap命令進行反編譯,javap是Java class檔案分解器,可以反編譯,也可以檢視java編譯器生成的位元組碼等。
javap引數如下:

image

使用方式,既可以在電腦的命令列提示符中使用,也可以透過idea的terminal終端使用,我這裡採用idea中進行反彙編操作。參考命令:javap -c -v Test.class

【反彙編結果】

image

由上圖可看出同步方法透過加 ACC_SYNCHRONIZED 標識實現執行緒的執行權的控制,如果修飾的是例項方法,JVM會獲取物件鎖,如果修飾的是靜態方法,JVM會獲取當前類鎖。

2、當synchronized修飾程式碼塊時

public class Test {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized");
        }
    }
}

【反彙編結果】

image

與同步方法不同,同步程式碼塊中使用了monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置,並且monitorexit標識有2個,以保證在正常執行和異常情況下均可釋放鎖。

在命令執行到monitorenter時,執行緒會去嘗試獲取物件得鎖,這裡也可稱之為物件所對應的monitor所有權。寫到這裡,我們又要做一個知識點擴充套件啦。

知識點擴充套件:

在JVM中monitor的底層基於C++實現,被稱之為物件監視器,每個物件都會內建一個ObjectMonitor與之關聯,關聯的起始地址存於物件頭的MarkWord中。

image

ObjectMonitor幾個關鍵屬性:

  • _owner:指向持有ObjectMonitor物件的執行緒
  • _WaitSet:存放處於wait狀態的執行緒佇列
  • EntryList:存放處於等待鎖block狀態的執行緒佇列
  • recursions:鎖的重入次數
  • _count:用來記錄該執行緒獲取鎖的次數

當多個執行緒同時訪問同步程式碼時,會被放入EntryList中,根據執行緒優先順序嘗試獲取物件鎖,如果鎖的計數器為 0 則表示鎖可以被獲取,獲取到鎖的執行緒進入owner區域,count加1,這裡其實還有之前說的object中的wait/notify/notifyall的組合,也依賴monitor,所以他們才必須用在同步方法或程式碼塊中。
物件鎖的的擁有者執行緒才可以執行 monitorexit 指令來釋放鎖。在執行 monitorexit 指令後,將鎖計數器設為 0,表明鎖被釋放,其他執行緒可以嘗試獲取鎖。

總結

關於synchronized的介紹其實遠沒有結束,還有很多細節可以值得學習,我們會在後面的文章中逐漸補充,避免文章過長,讀者失去閱讀的耐心!

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

image

相關文章