《包你懂系列》類鎖和物件鎖到底有什麼區別,面試重點題型

古時的風箏發表於2020-04-07

我是風箏,公眾號「古時的風箏」,一個不只有技術的技術公眾號,一個在程式圈混跡多年,主業 Java,另外 Python、React 也玩兒的 6 的斜槓開發者。 Spring Cloud 系列文章已經完成,可以到 我的 github 上檢視系列完整內容。也可以在公眾號內回覆「pdf」獲取我精心製作的 pdf 版完整教程。

類鎖和例項鎖是常見的面試題,時不時都會看到有同學討論,在類上加鎖和在 new 出來的類例項上加鎖到底有什麼區別呢?

來,今天讓你徹底搞明白類鎖和物件鎖的區別和使用方式。同學,快跟上。

有鎖才有自由

生活中不存在絕對的自由,絕對的自由通常對應的無序和混沌,只有在道德、法律、倫理的約束下的相對自由,才能使人感受到自由。

而在多執行緒程式設計中,鎖是至關重要的,鎖就是道德,就是法律約束,沒有鎖的多執行緒環境將會是混亂的,所有執行緒都在爭奪資源,最後的結果就是導致系統崩潰,而有了鎖之後,多執行緒環境才能穩定高效的工作。

synchronized 關鍵字

synchronized 是我們所說的重量級鎖,所說的重量級是相對於那些自旋鎖(AQS)而言的,比如可重入鎖ReentrantLock。很多人談 synchronized 色變,說它效能差、太重,貌似言之鑿鑿。放在多年前確實如此,但是 Java 1.7、1.8 已經對 synchronized 做了很大優化,其效能和那些輕量級鎖幾乎沒有差距。

所以,我們再程式中其實可以放心的使用它,即使沒有用過,也肯定在一些原始碼裡見過,比如 Netty 中就有很多地方用到了它。

下面開始進入今天的主題,類鎖和例項鎖。看名字就已經很明顯了,類鎖就是所在類上的鎖,例項就是鎖在類例項上的鎖。

例項鎖

類宣告後,我們可以 new 出來很多的例項物件。這時候,每個例項在 JVM 中都有自己的引用地址和堆記憶體空間,這時候,我們就認為這些例項都是獨立的個體,很顯然,在例項上加的鎖和其他的例項就沒有關係,互不影響了。

通常我們使用例項鎖的方式有下面三種:

1、 鎖住實體裡的非靜態變數

非靜態變數是例項自身變數,不會與其他例項共享,所以鎖住實體內宣告的非靜態變數可以實現物件鎖。鎖住同一個變數的方法塊共享同一把鎖。

2、鎖住 this 物件

this 指的是當前物件例項本身,所以,所有使用 synchronized(this)方式的方法都共享同一把鎖。

3、直接鎖非靜態方法

最簡單、最直觀的一種方式,直接加在方法返回型別前。

使用物件鎖的情況,只有使用同一例項的執行緒才會受鎖的影響,多個例項呼叫同一方法也不會受影響。

下面來做個測試,開啟 5 個執行緒,每個執行緒都 new 一個新的例項來分別呼叫上面三種方式的方法,方法完成的動作就是輸出執行緒名稱,然後休眠 10 秒鐘。

public class ObjectLock {

private Object lock = new Object();

/**
* 鎖住非靜態變數
* @throws InterruptedException
*/

public void lockObjectField() throws InterruptedException{
synchronized (lock){
System.out.println(Thread.currentThread().getName());
Thread.sleep(10*1000);
}
}

/**
* 鎖住 this 物件 this 就是當前物件例項
* @throws InterruptedException
*/

public void lockThis() throws InterruptedException{
synchronized (this){
System.out.println(Thread.currentThread().getName());
Thread.sleep(10*1000);
}
}

/**
* 直接鎖住非靜態方法
* @throws InterruptedException
*/

public synchronized void methodLock() throws InterruptedException{
System.out.println(Thread.currentThread().getName());
Thread.sleep(10*1000);
}

public static void main(String[] args){
for (int i = 0; i < 5; i++) {
Thread worker = new Thread(new ObjectLockWorker());
worker.setName("kite-" + i);
worker.start();
}
}

public static class ObjectLockWorker implements Runnable{
@Override
public void run() {
try {
ObjectLock objectLock = new ObjectLock();
// 方式 1
objectLock.lockObjectField();
// 方式 2
//objectLock.lockThis();
// 方式 3
//objectLock.methodLock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
複製程式碼
我們預測的結果就是每個執行緒都會立刻輸出執行緒名稱,然後各自休眠 10 秒。

分別呼叫方式1、2、3,效果都是一樣的,我們看到輸出結果和我們預測的是一樣的,5 個執行緒都立即輸出執行緒名,然後等待 10 秒,整個程式退出。

類鎖

類鎖是載入類上的,而類資訊是存在 JVM 方法區的,並且整個 JVM 只有一份,方法區又是所有執行緒共享的,所以類鎖是所有執行緒共享的。

使用類鎖的方式有如下方式:

1、鎖住類中的靜態變數

因為靜態變數和類資訊一樣也是存在方法區的並且整個 JVM 只有一份,所以加在靜態變數上可以達到類鎖的目的。

2、直接在靜態方法上加 synchronized

因為靜態方法同樣也是存在方法區的並且整個 JVM 只有一份,所以加在靜態方法上可以達到類鎖的目的。

3、鎖住 xxx.class

對當前類的 .class 屬性加鎖,可以實現類鎖。

類鎖是所有執行緒共享的鎖,所以同一時刻,只能有一個執行緒使用加了鎖的方法或方法體,不管是不是同一個例項。

下面同樣來做個測試,開啟 5 個執行緒,除了呼叫靜態方法的方式,其他兩種方式中每個執行緒都 new 一個新的例項來分別呼叫,方法內完成的動作就是輸出執行緒名稱,然後休眠 10 秒鐘。

public class ClassLock {

private static Object lock = new Object();

/**
* 鎖住靜態變數
* @throws InterruptedException
*/

public void lockStaticObjectField() throws InterruptedException{
synchronized (lock){
System.out.println(Thread.currentThread().getName());
Thread.sleep(10*1000);
}
}

/**
* 鎖住靜態方法
* @throws InterruptedException
*/

public static synchronized void methodLock() throws InterruptedException{
System.out.println(Thread.currentThread().getName());
Thread.sleep(10*1000);
}

/**
* 鎖住 xxx.class
* @throws InterruptedException
*/

public void lockClass() throws InterruptedException{
synchronized (ClassLock.class){
System.out.println(Thread.currentThread().getName());
Thread.sleep(10*1000);
}
}

public static void main(String[] args){
for (int i = 0; i < 5; i++) {
Thread worker = new Thread(new ClassLockWorker());
worker.setName("kite-" + i);
worker.start();
}
}

public static class ClassLockWorker implements Runnable{
@Override
public void run() {
try {
ClassLock classLock = new ClassLock();
// 方式 1
classLock.lockStaticObjectField();
// 方式 2
//ClassLock.methodLock();
// 方式 3
//classLock.lockClass();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
複製程式碼
我們預測的結果就是剛開始只有1個執行緒搶到鎖,然後輸出執行緒名,之後等待 10 秒中,之後是下一個搶到鎖的執行緒,輸出執行緒名,然後等待 10 秒。直到最後一個搶到鎖的執行緒,整個過程歷時大約 50 秒。

分別呼叫方式1、2、3,觀察執行結果,和我們預測的是一致的。

總結

  1. 使用物件鎖的情況,只有使用同一例項的執行緒才會受鎖的影響,多個例項呼叫同一方法也不會受影響。

  2. 類鎖是所有執行緒共享的鎖,所以同一時刻,只能有一個執行緒使用加了鎖的方法或方法體,不管是不是同一個例項。

創作不易,小小的贊,大大的暖,快來溫暖我。不用客氣了,讚我!

我是風箏,公眾號「古時的風箏」,一個在程式圈混跡多年,主業 Java,另外 Python、React 也玩兒的很 6 的斜槓開發者。可以在公眾號中加我好友,進群裡小夥伴交流學習,好多大廠的同學也在群內呦。

《包你懂系列》類鎖和物件鎖到底有什麼區別,面試重點題型

相關文章