Java自旋鎖與讀寫鎖的實現(xiàn)原理
Java自旋鎖與讀寫鎖
一、自旋鎖的核心概念
自旋鎖(Spin Lock)是一種非阻塞鎖,它的核心思想是:當(dāng)線程獲取鎖失敗時,不會立刻進入阻塞(休眠)狀態(tài),而是在一個循環(huán)中不斷嘗試獲取鎖(“自旋”),直到成功獲取鎖或者達到自旋上限。
二、對比傳統(tǒng)阻塞鎖(如 synchronized)
- 阻塞鎖:獲取鎖失敗 → 線程進入等待隊列 → 內(nèi)核態(tài)切換 → 等待被喚醒 → 再次競爭鎖(上下文切換成本高)
- 自旋鎖:獲取鎖失敗 → 循環(huán)重試(用戶態(tài)) → 成功獲取鎖(無上下文切換)
自旋鎖適合鎖持有時間極短的場景,因為短時間的自旋消耗的 CPU 資源,遠小于線程上下文切換的開銷;但如果鎖持有時間長,自旋會浪費大量 CPU 資源(空轉(zhuǎn))。
三、Java 中自旋鎖的實現(xiàn)原理
Java 中實現(xiàn)自旋鎖的核心是CAS(Compare And Swap,比較并交換) 操作,這是一種原子操作,保證多線程下的線程安全。
CAS 有三個核心參數(shù):
V:要修改的變量(鎖的狀態(tài)標(biāo)識)A:預(yù)期值(期望的鎖狀態(tài),比如未被占用)B:新值(修改后的鎖狀態(tài),比如被當(dāng)前線程占用)
只有當(dāng) V == A 時,才會將 V 更新為 B,否則不做操作,返回 false。
import java.util.concurrent.atomic.AtomicReference;
/**
* 簡單的自旋鎖實現(xiàn)
* 核心:利用AtomicReference的CAS操作保證鎖的原子性競爭
*/
public class SimpleSpinLock {
// 用AtomicReference存儲持有鎖的線程,初始為null(無鎖狀態(tài))
private final AtomicReference<Thread> lockOwner = new AtomicReference<>();
// 獲取鎖(自旋邏輯)
public void lock() {
Thread currentThread = Thread.currentThread();
// 自旋:CAS嘗試將lockOwner從null改為當(dāng)前線程,失敗則循環(huán)重試
while (!lockOwner.compareAndSet(null, currentThread)) {
// 空循環(huán)(自旋),也可以加入自旋次數(shù)限制、yield()等優(yōu)化
// Thread.yield(); // 讓出CPU,減少空轉(zhuǎn)消耗
}
System.out.println(currentThread.getName() + " 獲取到鎖");
}
// 釋放鎖
public void unlock() {
Thread currentThread = Thread.currentThread();
// 只有持有鎖的線程才能釋放鎖
if (lockOwner.get() == currentThread) {
lockOwner.compareAndSet(currentThread, null);
System.out.println(currentThread.getName() + " 釋放了鎖");
} else {
throw new IllegalMonitorStateException("當(dāng)前線程未持有鎖,無法釋放");
}
}
// 測試自旋鎖
public static void main(String[] args) {
SimpleSpinLock spinLock = new SimpleSpinLock();
// 創(chuàng)建5個線程競爭鎖
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
spinLock.lock();
// 模擬持有鎖的短時間操作(自旋鎖適合短任務(wù))
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
spinLock.unlock();
}
}, "線程" + i).start();
}
}
}四、Java 中的內(nèi)置自旋鎖優(yōu)化
JDK 中并沒有直接暴露 “自旋鎖” 類,但在底層對synchronized和Lock(如 ReentrantLock)做了自旋優(yōu)化:
- synchronized 的自旋優(yōu)化:JDK1.6 后,synchronized 引入了 “偏向鎖→輕量級鎖→重量級鎖” 的升級過程,其中輕量級鎖階段會采用自旋(默認自旋 10 次,可通過 JVM 參數(shù)
-XX:PreBlockSpin調(diào)整)。 - LockSupport.parkNanos():ReentrantLock 的底層(AQS)會在自旋一定次數(shù)后,調(diào)用
parkNanos()讓線程短暫休眠,避免無限自旋浪費 CPU(自適應(yīng)自旋)。
一、讀寫鎖的核心概念
讀寫鎖(ReadWriteLock)是一種共享 - 排他鎖,它把對資源的訪問分成了 “讀操作” 和 “寫操作”,核心設(shè)計目標(biāo)是:
- 讀鎖(共享鎖):多個線程可以同時獲取讀鎖,互不阻塞(讀 - 讀共享)。
- 寫鎖(排他鎖 / 獨占鎖):只有一個線程能獲取寫鎖,且寫鎖會阻塞所有讀鎖和其他寫鎖(寫 - 讀、寫 - 寫互斥)。
讀寫鎖的核心規(guī)則
| 場景 | 是否允許 |
|---|---|
| 已有讀鎖,加讀鎖 | 允許(共享) |
| 已有讀鎖,加寫鎖 | 不允許(互斥) |
| 已有寫鎖,加讀鎖 | 不允許(互斥) |
| 已有寫鎖,加寫鎖 | 不允許(互斥) |
二、Java 中的讀寫鎖實現(xiàn):ReentrantReadWriteLock
Java 在java.util.concurrent.locks包中提供了ReadWriteLock接口,其核心實現(xiàn)類是ReentrantReadWriteLock(可重入讀寫鎖),它具備以下特性:
- 可重入:讀線程獲取讀鎖后可再次獲取讀鎖,寫線程獲取寫鎖后可再次獲取寫鎖 / 讀鎖。
- 公平 / 非公平模式:默認非公平,可通過構(gòu)造函數(shù)指定公平模式。
- 鎖降級:寫鎖可降級為讀鎖(寫鎖→讀鎖),但讀鎖不能升級為寫鎖。
“緩存模擬” 案例(典型的多讀少寫場景),展示讀寫鎖的使用:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 基于讀寫鎖實現(xiàn)的線程安全緩存
* 場景:多讀少寫,讀操作并行,寫操作獨占
*/
public class ReadWriteLockCache {
// 緩存容器
private final Map<String, Object> cache = new HashMap<>();
// 讀寫鎖(默認非公平模式)
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 讀鎖(共享鎖)
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
// 寫鎖(排他鎖)
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
// 從緩存讀取數(shù)據(jù)(讀操作,用讀鎖)
public Object get(String key) {
// 獲取讀鎖
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 讀取緩存,key=" + key);
// 模擬讀操作耗時
Thread.sleep(100);
return cache.get(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
// 釋放讀鎖(必須在finally中釋放,避免鎖泄漏)
readLock.unlock();
System.out.println(Thread.currentThread().getName() + " 釋放讀鎖");
}
}
// 向緩存寫入數(shù)據(jù)(寫操作,用寫鎖)
public void put(String key, Object value) {
// 獲取寫鎖
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 寫入緩存,key=" + key + ", value=" + value);
// 模擬寫操作耗時
Thread.sleep(200);
cache.put(key, value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 釋放寫鎖
writeLock.unlock();
System.out.println(Thread.currentThread().getName() + " 釋放寫鎖");
}
}
// 清空緩存(寫操作,用寫鎖)
public void clear() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 清空緩存");
cache.clear();
} finally {
writeLock.unlock();
}
}
// 測試讀寫鎖
public static void main(String[] args) {
ReadWriteLockCache cache = new ReadWriteLockCache();
// 1. 啟動5個讀線程(并行執(zhí)行)
for (int i = 0; i < 5; i++) {
new Thread(() -> cache.get("user_1"), "讀線程" + i).start();
}
// 2. 啟動2個寫線程(串行執(zhí)行,且阻塞讀線程)
new Thread(() -> cache.put("user_1", "張三"), "寫線程1").start();
new Thread(() -> cache.put("user_2", "李四"), "寫線程2").start();
}
}運行代碼會發(fā)現(xiàn),5 個讀線程幾乎同時執(zhí)行(讀鎖共享),而 2 個寫線程串行執(zhí)行,且寫線程執(zhí)行時讀線程會等待。
讀線程0 讀取緩存,key=user_1
讀線程2 讀取緩存,key=user_1
讀線程1 讀取緩存,key=user_1
讀線程4 讀取緩存,key=user_1
讀線程3 讀取緩存,key=user_1
讀線程1 釋放讀鎖
讀線程3 釋放讀鎖
讀線程4 釋放讀鎖
讀線程2 釋放讀鎖
寫線程1 寫入緩存,key=user_1, value=張三
讀線程0 釋放讀鎖
寫線程2 寫入緩存,key=user_2, value=李四
寫線程1 釋放寫鎖
寫線程2 釋放寫鎖
四、讀寫鎖的關(guān)鍵特性詳解
1. 可重入性
- 讀線程獲取讀鎖后,可再次獲取讀鎖(重入次數(shù)累加)。
- 寫線程獲取寫鎖后,可再次獲取寫鎖,也可獲取讀鎖(鎖降級的基礎(chǔ))。
- 示例(鎖降級):
// 寫鎖降級為讀鎖
writeLock.lock();
try {
// 執(zhí)行寫操作
cache.put("key", "value");
// 降級:先獲取讀鎖,再釋放寫鎖
readLock.lock();
} finally {
writeLock.unlock();
}
try {
// 持有讀鎖,安全讀取數(shù)據(jù)
System.out.println(cache.get("key"));
} finally {
readLock.unlock();
}2. 公平 / 非公平模式
- 非公平模式(默認):性能更高,寫線程可能插隊(讀線程多的時候,寫線程可能饑餓)。
- 公平模式:通過構(gòu)造函數(shù)
new ReentrantReadWriteLock(true)指定,按線程等待順序獲取鎖,避免寫?zhàn)囸I,但性能略低。
3. 寫?zhàn)囸I問題
如果讀操作非常頻繁,寫線程可能長期無法獲取寫鎖(一直被讀鎖阻塞),解決方案:
- 使用公平模式。
- 控制讀鎖持有時間,避免長時間占用讀鎖。
到此這篇關(guān)于Java自旋鎖與讀寫鎖的實現(xiàn)原理的文章就介紹到這了,更多相關(guān)java自旋鎖與讀寫鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決idea中javaweb的mysql8.0.15配置問題
這篇文章主要介紹了idea中javaweb的mysql8.0.15配置問題 ,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-05-05
詳解Spring Cloud Gateway 數(shù)據(jù)庫存儲路由信息的擴展方案
這篇文章主要介紹了詳解Spring Cloud Gateway 數(shù)據(jù)庫存儲路由信息的擴展方案,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-11-11
Springboot-dubbo-fescar 阿里分布式事務(wù)的實現(xiàn)方法
這篇文章主要介紹了Springboot-dubbo-fescar 阿里分布式事務(wù)的實現(xiàn)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-03-03
Spring Boot通過Redis實現(xiàn)防止重復(fù)提交
表單提交是一個非常常見的功能,如果不加控制,容易因為用戶的誤操作或網(wǎng)絡(luò)延遲導(dǎo)致同一請求被發(fā)送多次,本文主要介紹了Spring Boot通過Redis實現(xiàn)防止重復(fù)提交,具有一定的參考價值,感興趣的可以了解一下2024-06-06
SpringBoot整合Shiro框架,實現(xiàn)用戶權(quán)限管理
Apache Shiro是一個強大且易用的Java安全框架,執(zhí)行身份驗證、授權(quán)、密碼和會話管理。作為一款安全框架Shiro的設(shè)計相當(dāng)巧妙。Shiro的應(yīng)用不依賴任何容器,它不僅可以在JavaEE下使用,還可以應(yīng)用在JavaSE環(huán)境中。2021-06-06

