Java的ConcurrentHashMap是使用的分段鎖?
了不起在前兩天的時候給大家講述了關于這個 Java 的公平鎖,非公平鎖,共享鎖,獨占鎖,樂觀鎖,悲觀鎖,遞歸鎖,讀寫鎖,今天我們就再來了解一下其他的鎖,比如,輕量級鎖,重量級鎖,偏向鎖,以及分段鎖。
輕量級鎖
Java的輕量級鎖(Lightweight Locking)是Java虛擬機(JVM)中的一種優化機制,用于減少多線程競爭時的性能開銷。在多線程環境中,當多個線程嘗試同時訪問共享資源時,通常需要某種形式的同步以防止數據不一致。Java提供了多種同步機制,如synchronized關鍵字和ReentrantLock,但在高并發場景下,這些機制可能導致性能瓶頸。
輕量級鎖是JVM中的一種鎖策略,它在沒有多線程競爭的情況下提供了較低的開銷,同時在競爭變得激烈時能夠自動升級到更重量級的鎖。這種策略的目標是在不需要時避免昂貴的線程阻塞操作。
不過這種鎖并不是通過Java語言直接暴露給開發者的API,而是JVM在運行時根據需要自動應用的。因此,我們不能直接通過Java代碼來實現一個輕量級鎖。
但是我們可以使用Java提供的synchronized關鍵字或java.util.concurrent.locks.Lock接口(及其實現類,如ReentrantLock)來創建同步代碼塊或方法,這些同步機制在底層可能會被JVM優化為使用輕量級鎖。
示例代碼:
public class LightweightLockExample {
private Object lock = new Object();
private int sharedData;
public void incrementSharedData() {
synchronized (lock) {
sharedData++;
}
}
public int getSharedData() {
synchronized (lock) {
return sharedData;
}
}
public static void main(String[] args) {
LightweightLockExample example = new LightweightLockExample();
// 使用多個線程來訪問共享數據
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
example.incrementSharedData();
}
}).start();
}
// 等待所有線程執行完畢
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 輸出共享數據的最終值
System.out.println("Final shared data value: " + example.getSharedData());
}
}
這個示例中的同步塊在JVM內部可能會使用輕量級鎖(具體是否使用取決于JVM的實現和運行時環境)
在這個例子中,我們有一個sharedData變量,多個線程可能會同時訪問它。我們使用synchronized塊來確保每次只有一個線程能夠修改sharedData。在JVM內部,這些synchronized塊可能會使用輕量級鎖來優化同步性能。
請注意,這個例子只是為了演示如何使用synchronized關鍵字,并不能保證JVM一定會使用輕量級鎖。實際上,JVM可能會根據運行時的情況選擇使用偏向鎖、輕量級鎖或重量級鎖。
重量級鎖
在Java中,重量級鎖(Heavyweight Locking)是相對于輕量級鎖而言的,它涉及到線程阻塞和操作系統級別的線程調度。當輕量級鎖或偏向鎖不足以解決線程間的競爭時,JVM會升級鎖為重量級鎖。
重量級鎖通常是通過操作系統提供的互斥原語(如互斥量、信號量等)來實現的。當一個線程嘗試獲取已經被其他線程持有的重量級鎖時,它會被阻塞(即掛起),直到持有鎖的線程釋放該鎖。在阻塞期間,線程不會消耗CPU資源,但會導致上下文切換的開銷,因為操作系統需要保存和恢復線程的上下文信息。
在Java中,synchronized關鍵字和java.util.concurrent.locks.ReentrantLock都可以導致重量級鎖的使用,尤其是在高并發和激烈競爭的場景下。
我們來看看使用synchronized可能會涉及到重量級鎖的代碼:
public class HeavyweightLockExample {
private final Object lock = new Object();
private int counter;
public void increment() {
synchronized (lock) {
counter++;
}
}
public int getCounter() {
synchronized (lock) {
return counter;
}
}
public static void main(String[] args) {
HeavyweightLockExample example = new HeavyweightLockExample();
// 創建多個線程同時訪問共享資源
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
example.increment();
}
}).start();
}
// 等待所有線程完成
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 輸出計數器的值
System.out.println("Final counter value: " + example.getCounter());
}
}
在這個示例中,多個線程同時訪問counter變量,并使用synchronized塊來確保每次只有一個線程能夠修改它。如果線程間的競爭非常激烈,JVM可能會將synchronized塊內部的鎖升級為重量級鎖。
我們說的是可能哈,畢竟內部操作還是由 JVM 具體來操控的。
我們再來看看這個ReentrantLock來實現:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int counter;
public void increment() {
lock.lock(); // 獲取鎖
try {
counter++;
} finally {
lock.unlock(); // 釋放鎖
}
}
public int getCounter() {
return counter;
}
public static void main(String[] args) {
// 類似上面的示例,創建線程并訪問共享資源
}
}
在這個示例中,ReentrantLock被用來同步對counter變量的訪問。如果鎖競爭激烈,ReentrantLock內部可能會使用重量級鎖。
需要注意的是,重量級鎖的使用會帶來較大的性能開銷,因此在設計并發系統時應盡量通過減少鎖競爭、使用更細粒度的鎖、使用無鎖數據結構等方式來避免重量級鎖的使用。
偏向鎖
在Java中,偏向鎖(Biased Locking)是Java虛擬機(JVM)為了提高無競爭情況下的性能而引入的一種鎖優化機制。它的基本思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程只需要檢查Mark Word的鎖標記位為偏向鎖以及當前線程ID等于Mark Word的Thread ID即可,這樣就省去了大量有關鎖申請的操作。
他和輕量級鎖和重量級鎖一樣,并不是直接通過Java代碼來控制的,而是由JVM在運行時自動進行的。因此,你不能直接編寫Java代碼來顯式地使用偏向鎖。不過,你可以編寫一個使用synchronized關鍵字的簡單示例,JVM可能會自動將其優化為使用偏向鎖(取決于JVM的實現和運行時的配置)。
示例代碼:
public class BiasedLockingExample {
// 這個對象用作同步的鎖
private final Object lock = new Object();
// 共享資源
private int sharedData;
// 使用synchronized關鍵字進行同步的方法
public synchronized void synchronizedMethod() {
sharedData++;
}
// 使用對象鎖進行同步的方法
public void lockedMethod() {
synchronized (lock) {
sharedData += 2;
}
}
public static void main(String[] args) throws InterruptedException {
// 創建示例對象
BiasedLockingExample example = new BiasedLockingExample();
// 使用Lambda表達式和Stream API創建并啟動多個線程
IntStream.range(0, 10).forEach(i -> {
new Thread(() -> {
// 每個線程多次調用同步方法
for (int j = 0; j < 10000; j++) {
example.synchronizedMethod();
example.lockedMethod();
}
}).start();
});
// 讓主線程睡眠一段時間,等待其他線程執行完畢
Thread.sleep(2000);
// 輸出共享數據的最終值
System.out.println("Final sharedData value: " + example.sharedData);
}
}
在這個示例中,我們有一個BiasedLockingExample類,它有兩個同步方法:synchronizedMethod和lockedMethod。synchronizedMethod是一個實例同步方法,它隱式地使用this作為鎖對象。lockedMethod是一個使用顯式對象鎖的方法,它使用lock對象作為鎖。
當多個線程調用這些方法時,JVM可能會觀察到只有一個線程在反復獲取同一個鎖,并且沒有其他線程競爭該鎖。在這種情況下,JVM可能會將鎖偏向到這個線程,以減少獲取和釋放鎖的開銷。
然而,請注意以下幾點:
- 偏向鎖的使用是由JVM動態決定的,你不能強制JVM使用偏向鎖。
- 在高并發環境下,如果鎖競爭激烈,偏向鎖可能會被撤銷并升級到更重的鎖狀態,如輕量級鎖或重量級鎖。
- 偏向鎖適用于鎖被同一個線程多次獲取的場景。如果鎖被多個線程頻繁地爭用,偏向鎖可能不是最優的選擇。
由于偏向鎖是透明的優化,因此你不需要在代碼中做任何特殊的事情來利用它。只需編寫正常的同步代碼,讓JVM來決定是否應用偏向鎖優化。
分段鎖
在Java中,"分段鎖"并不是一個官方的術語,但它通常被用來描述一種并發控制策略,其中數據結構或資源被分成多個段,并且每個段都有自己的鎖。這種策略的目的是提高并發性能,允許多個線程同時訪問不同的段,而不會相互阻塞。
而在 Java 里面的經典例子則是ConcurrentHashMap,在早期的ConcurrentHashMap實現中,內部采用了一個稱為Segment的類來表示哈希表的各個段,每個Segment對象都持有一個鎖。這種設計允許多個線程同時讀寫哈希表的不同部分,而不會產生鎖競爭,從而提高了并發性能。
然而,需要注意的是,從Java 8開始,ConcurrentHashMap的內部實現發生了重大變化。它不再使用Segment,而是采用了一種基于CAS(Compare-and-Swap)操作和Node數組的新設計,以及紅黑樹來處理哈希沖突。這種新設計提供了更高的并發性和更好的性能。盡管如此,"分段鎖"這個概念仍然可以用來描述這種將數據結構分成多個可獨立鎖定的部分的通用策略。
我們看一個分段鎖實現安全計數器的代碼:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SegmentedCounter {
private final int size;
private final Lock[] locks;
private final int[] counters;
public SegmentedCounter(int size) {
this.size = size;
this.locks = new Lock[size];
this.counters = new int[size];
for (int i = 0; i < size; i++) {
locks[i] = new ReentrantLock();
}
}
public void increment(int index) {
locks[index].lock();
try {
counters[index]++;
} finally {
locks[index].unlock();
}
}
public int getValue(int index) {
locks[index].lock();
try {
return counters[index];
} finally {
locks[index].unlock();
}
}
}
在這個例子中,SegmentedCounter類有一個counters數組和一個locks數組。每個計數器都有一個與之對應的鎖,這使得線程可以獨立地更新不同的計數器,而不會相互干擾。當然,這個簡單的例子并沒有考慮一些高級的并發問題,比如鎖的粒度選擇、鎖爭用和公平性等問題。在實際應用中,你可能需要根據具體的需求和性能目標來調整設計。
所以,你學會了么?