天天看點

Java synchronized關鍵字實作原理及Lock對比

當多個線程同時通路共享資源時,就需要使用同步機制來保證線程的安全性。Java中的synchronized關鍵字就是一種同步機制,它可以讓某個代碼塊或方法在同一時刻隻能被一個線程通路。

synchronized實作原理: 在Java中,每個對象都有一個内部鎖(或稱為螢幕鎖或互斥鎖),當一個線程要通路被synchronized修飾的代碼塊或方法時,它必須先獲得這個對象的内部鎖。如果這個鎖已經被其他線程持有,則該線程将被阻塞,直到其他線程釋放了這個鎖。

synchronized的實作原理主要涉及到以下三個部分:

1. monitorenter指令:當線程嘗試擷取對象的鎖時,JVM會執行monitorenter指令,如果擷取成功,則進入臨界區,否則進入阻塞狀态等待鎖的釋放。

1. monitorexit指令:當線程退出臨界區時,JVM會執行monitorexit指令,釋放對象的鎖,以便其他線程能夠擷取鎖并進入臨界區。

1. 對象頭中的Mark Word:在Java中,每個對象都有一個對象頭,其中包含了對象的中繼資料資訊,比如對象的哈希碼和鎖狀态等。在synchronized實作中,對象頭中的Mark Word用于記錄鎖狀态,當一個線程獲得了對象的鎖時,JVM會将Mark Word中的鎖狀态設定為“已鎖定”,當線程釋放鎖時,JVM會将鎖狀态設定為“未鎖定”。

下面是synchronized的源碼實作:

```

public synchronized void method() { // 代碼塊 }

```

編譯後的位元組碼:

```

public void method();

Code:

0: aload_0

1: monitorenter

2: aload_0

3: invokevirtual #1 // Method java/lang/Object.hashCode:()I

6: aload_0

7: monitorexit

8: return

......

```

在方法前面加了synchronized關鍵字後,編譯器會在方法的位元組碼中插入monitorenter和monitorexit指令,確定在執行方法時隻能有一個線程獲得鎖。

2. 修飾代碼塊

```

public void method() {

synchronized (this) {

// 代碼塊

}

}

```

編譯後的位元組碼:

```

public void method();

Code:

0: aload_0

1: dup

2: astore_1

3: monitorenter

4: aload_1

5: monitorexit

6: goto 14

9: astore_2

10: aload_1

11: monitorexit

12: aload_2

13: athrow

14: return

Exception table:

from to target type

4 6 9 any

9 12 9 any

```

在代碼塊前面加了synchronized關鍵字後,編譯器會在代碼塊的位元組碼中插入monitorenter和monitorexit指令,保證在執行代碼塊時隻能有一個線程獲得鎖。

需要注意的是,在使用synchronized時,應該盡量避免對整個方法或類進行加鎖,而是應該盡可能地縮小同步範圍,以提高程式的并發性能。

另外,從JDK1.5開始,Java提供了一種新的同步機制——Lock,它相對于synchronized來說更加靈活和高效,但也更加複雜。在實際開發中,應該根據具體情況選擇合适的同步機制。

Lock機制相對于synchronized來說,具有以下優點:

1. 可重入性:與synchronized類似,Lock也具有可重入性,同一個線程可以多次擷取同一把鎖而不會發生死鎖。

1. 精确控制鎖的釋放:Lock允許程式員手動釋放鎖,而synchronized隻能在代碼塊執行完畢或抛出異常時自動釋放鎖。這個特性在某些場景下非常有用,比如死鎖恢複。

1. 公平性:在synchronized中,無法保證多個線程擷取鎖的公平性,有可能會出現饑餓現象。而在Lock中,可以通過構造函數指定是否采用公平鎖,來保證線程擷取鎖的公平性。

1. 支援多個條件變量:與synchronized隻能支援一個條件變量相比,Lock可以支援多個條件變量,這在某些複雜的同步場景下非常有用。

下面是一個使用Lock實作同步的例子:

```

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class SynchronizedExample {

private int count = 0;

private Lock lock = new ReentrantLock();

public void increment() {

lock.lock();

try {

count++;

} finally {

lock.unlock();

}

}

public int getCount() {

lock.lock();

try {

return count;

} finally {

lock.unlock();

}

}

}

```

在這個例子中,我們使用ReentrantLock實作了一個線程安全的計數器,使用lock()方法擷取鎖,在try塊中執行操作,使用unlock()方法釋放鎖。在getCount()方法中也使用了lock()和unlock()方法來保證線程安全。在實際開發中,可以根據具體的需求選擇合适的Lock實作類。

除了ReentrantLock之外,Java中還提供了其他幾種Lock實作類,下面介紹一下常用的幾種:

1. ReentrantLock:重入鎖,與synchronized類似,具有可重入性,支援公平和非公平鎖,預設是非公平鎖。

1. ReadWriteLock:讀寫鎖,允許多個線程同時讀共享資料,但隻允許一個線程寫共享資料,寫鎖是排他的。Java中提供了ReadWriteLock接口和ReentrantReadWriteLock實作類,後者支援重入和公平非公平鎖。

1. StampedLock:樂觀讀鎖,相對于ReadWriteLock,StampedLock具有更高的并發性能。StampedLock有三種模式:寫模式、讀模式和樂觀讀模式,其中寫模式和讀模式與ReadWriteLock類似,樂觀讀模式則不需要加鎖,隻需要使用樂觀讀方法tryOptimisticRead()擷取版本号,然後讀取資料,最後再根據版本号判斷資料是否被修改過。

1. Condition:條件變量,與synchronized中的wait()、notify()和notifyAll()方法類似,Condition也提供了類似的await()、signal()和signalAll()方法,用于等待某個條件的滿足和通知等待的線程。Condition需要與Lock一起使用。

在使用Lock機制時,需要注意以下幾點:

1. 必須手動釋放鎖,否則可能會導緻死鎖。

1. 應該使用try-finally塊來保證鎖一定會被釋放。

1. 應該根據具體情況選擇合适的Lock實作類和鎖的公平性。

1. 不要在鎖的作用域内執行耗時操作,以避免降低程式的并發性能。

綜上所述,synchronized和Lock都是Java中用于實作同步的機制,它們各有優缺點,在實際開發中應該根據具體情況選擇合适的機制。同時,對于多線程程式設計,還需要注意線程安全、死鎖、饑餓等問題。

繼續閱讀