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