當多個線程同時通路共享資源時,就需要使用同步機制來保證線程的安全性。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中用于實作同步的機制,它們各有優缺點,在實際開發中應該根據具體情況選擇合适的機制。同時,對于多線程程式設計,還需要注意線程安全、死鎖、饑餓等問題。