Java中的Lock使用實(shí)例詳解
1. 為什么需要 Lock?(從 synchronized 說起)
synchronized 的局限性:synchronized 用起來很簡單,但它有一些“笨拙”的地方:
- 鎖的獲取和釋放是固定的:線程進(jìn)入
synchronized塊時獲取鎖,退出時(無論是正常退出還是異常退出)自動釋放鎖。你無法手動控制。 - 不可中斷:如果一個線程在等待鎖,它會一直等下去,不能被中斷。
- 非公平鎖:默認(rèn)情況下,等待的線程們不是按先來后到的順序獲取鎖的,可能會“插隊”。
- 只有一個條件隊列:等待和通知用的是
wait()和notify(),不夠靈活。
為了解決這些“笨拙”的問題,Java 在 1.5 版本引入了 java.util.concurrent.locks.Lock 接口。
2. Lock 是什么?
Lock像一個需要你用鑰匙開閉的密碼鎖或指紋鎖:你可以自己決定什么時候上鎖,什么時候解鎖。功能強(qiáng)大,可以設(shè)置密碼(嘗試獲取鎖)、設(shè)置超時時間等,但需要你自己管理鑰匙,別弄丟了。
3. Lock 的核心方法
Lock 接口最主要的方法有以下幾個:
lock(): 獲取鎖。如果鎖被其他線程占用,則當(dāng)前線程會一直等待,直到拿到鎖為止。這是最基礎(chǔ)的用法。unlock(): 釋放鎖。非常重要! 你必須手動調(diào)用,通常放在finally塊中以確保一定會被執(zhí)行,防止死鎖。tryLock(): 嘗試獲取鎖。它不會像lock()那樣死等。如果能拿到鎖,就返回true;如果拿不到,就立刻返回false。這給了你“搶不到就去做別的事”的可能性。tryLock(long time, TimeUnit unit): 帶超時的嘗試獲取鎖。在指定的時間內(nèi)嘗試獲取鎖,獲取成功則返回true,超時后還獲取不到則返回false。這個方法非常實(shí)用,可以避免線程無限期等待。lockInterruptibly(): 可中斷地獲取鎖。在等待鎖的過程中,線程可以被中斷(調(diào)用interrupt()方法)。
4. 最常用的實(shí)現(xiàn)類:ReentrantLock
ReentrantLock 是 Lock 接口最主要、最常用的實(shí)現(xiàn)類。它的名字叫“可重入鎖”,意思是同一個線程可以多次獲取同一把鎖而不會把自己鎖死(和 synchronized 一樣)。
基礎(chǔ)使用模板
使用 Lock 有一個固定的“套路”,以確保鎖一定能被釋放:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
// 1. 創(chuàng)建一個 Lock 實(shí)例,通常是 ReentrantLock
private final Lock lock = new ReentrantLock();
public void safeMethod() {
// 2. 在操作共享資源前,獲取鎖
lock.lock();
try {
// 3. 在這里執(zhí)行需要線程安全的代碼(臨界區(qū)代碼)
// ... 比如修改一個共享變量
System.out.println(Thread.currentThread().getName() + " 拿到了鎖");
} finally {
// 4. 在 finally 塊中釋放鎖,確保無論發(fā)生什么異常,鎖都會被釋放
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 釋放了鎖");
}
}
}關(guān)鍵點(diǎn): lock.unlock() 一定要放在 finally 塊里!否則如果臨界區(qū)代碼發(fā)生異常,鎖可能永遠(yuǎn)無法釋放,導(dǎo)致其他線程全部“餓死”。
高級用法示例:tryLock
public void tryLockExample() {
if (lock.tryLock()) { // 嘗試獲取鎖
try {
// 成功拿到鎖,執(zhí)行任務(wù)
System.out.println(Thread.currentThread().getName() + " 成功獲取鎖,執(zhí)行任務(wù)...");
Thread.sleep(1000); // 模擬任務(wù)執(zhí)行時間
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
// 沒拿到鎖,去做別的事情
System.out.println(Thread.currentThread().getName() + " 獲取鎖失敗,去執(zhí)行其他任務(wù)了...");
}
}ReentrantLock基于AQS,AQS有tryAcquire(),所以ReentrantLock有tryLock()嘗試加鎖
5. Lock 的另一個強(qiáng)大功能:條件(Condition)
synchronized 的等待/通知
在 synchronized 中,我們是這樣讓線程等待和喚醒的:
synchronized (obj) {
while (條件不滿足) {
obj.wait(); // 線程等待
}
// 執(zhí)行任務(wù)...
}
// 另一個線程中
synchronized (obj) {
obj.notify(); // 喚醒一個等待的線程
// 或者 obj.notifyAll(); 喚醒所有等待的線程
}問題:所有線程都在同一個"等待隊列"里,我們無法精確控制喚醒哪種線程。
Condition 就像"專門的等待室"
想象一個餐廳:
- synchronized:只有一個大的"等候區(qū)",所有客人都擠在一起
- Condition:有多個專門的"等候室"
notFull:等待"桌子有空位"的生產(chǎn)者等候室notEmpty:等待"有菜可吃"的消費(fèi)者等候室
代碼對比理解
import java.util.concurrent.locks.*;
public class Restaurant {
private final Lock lock = new ReentrantLock();
// 創(chuàng)建兩個專門的"等候室"
private final Condition notFull = lock.newCondition(); // "桌子有空位"等候室
private final Condition notEmpty = lock.newCondition(); // "有菜可吃"等候室
private int foodCount = 0;
private final int MAX_FOOD = 5;
// 廚師(生產(chǎn)者):做菜
public void cook() throws InterruptedException {
lock.lock();
try {
while (foodCount == MAX_FOOD) {
System.out.println("廚房滿了,廚師在notFull等候室等待...");
notFull.await(); // 如果廚房滿了,就去"桌子有空位"等候室等待
}
// 做菜
foodCount++;
System.out.println("廚師做了一道菜,現(xiàn)在有 " + foodCount + " 道菜");
// 通知在"有菜可吃"等候室等待的客人
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 客人(消費(fèi)者):吃菜
public void eat() throws InterruptedException {
lock.lock();
try {
while (foodCount == 0) {
System.out.println("沒菜了,客人在notEmpty等候室等待...");
notEmpty.await(); // 如果沒菜了,就去"有菜可吃"等候室等待
}
// 吃菜
foodCount--;
System.out.println("客人吃了一道菜,還剩 " + foodCount + " 道菜");
// 通知在"桌子有空位"等候室等待的廚師
notFull.signal();
} finally {
lock.unlock();
}
}
}synchronized 配合 wait() 和 notify() 可以實(shí)現(xiàn)線程間的等待/通知機(jī)制。Lock 也有對應(yīng)的、但更強(qiáng)大的功能,那就是 Condition。
你可以通過 Lock 的 newCondition() 方法創(chuàng)建一個 Condition 對象。一個 Lock 可以創(chuàng)建多個 Condition,這意味著你可以有多個等待隊列,實(shí)現(xiàn)更精細(xì)的線程控制。
await(): 類似Object.wait(),讓當(dāng)前線程等待。signal(): 類似Object.notify(),喚醒一個在此 Condition 上等待的線程。signalAll(): 類似Object.notifyAll(),喚醒所有在此 Condition 上等待的線程。
經(jīng)典場景:生產(chǎn)者-消費(fèi)者模型
你可以創(chuàng)建兩個 Condition:notFull(隊列未滿)和 notEmpty(隊列未空)。
- 生產(chǎn)者往隊列放東西前,如果隊列滿了,就在
notFull上等待;放完之后,就喚醒在notEmpty上等待的消費(fèi)者。 - 消費(fèi)者從隊列拿東西前,如果隊列空了,就在
notEmpty上等待;拿完之后,就喚醒在notFull上等待的生產(chǎn)者。
這樣比單一的 wait()/notify() 更清晰,不容易出錯。
6. 公平鎖 vs 非公平鎖
在創(chuàng)建 ReentrantLock 時,可以傳入一個 boolean 參數(shù)來指定是否是公平鎖:
new ReentrantLock(true): 公平鎖。線程們按申請鎖的先后順序來獲取鎖,先到先得。new ReentrantLock(false): 非公平鎖(默認(rèn))。允許“插隊”,后申請的線程可能比先申請的線程先拿到鎖。
AQS先進(jìn)先出的隊列為實(shí)現(xiàn)公平鎖奠定了基礎(chǔ)。ReeentrantLock中有一個FairSync和NonfairSync的子類,他們都重寫了AQS的tryAcquire()方法,F(xiàn)airSync是依賴了AQS的隊列結(jié)構(gòu),實(shí)現(xiàn)了先來先獲取鎖的操作。NonfairSync就允許新線程插隊搶鎖。
優(yōu)缺點(diǎn):
- 公平鎖:優(yōu)點(diǎn)是不會產(chǎn)生線程饑餓(某個線程永遠(yuǎn)拿不到鎖)。缺點(diǎn)是吞吐量低,性能開銷大。
- 非公平鎖:優(yōu)點(diǎn)是吞吐率高(減少了線程切換的開銷)。缺點(diǎn)是可能導(dǎo)致線程饑餓。
在絕大多數(shù)情況下,使用默認(rèn)的非公平鎖即可,因為性能更好。
7. 總結(jié):Lock 和 synchronized 如何選擇?
| 特性 | synchronized | Lock |
|---|---|---|
| 本質(zhì) | Java 語言關(guān)鍵字,內(nèi)置的 | 是一個接口,需要手動實(shí)例化 |
| 用法 | 簡單,自動加鎖解鎖 | 靈活,手動控制,需配合 try-finally |
| 可中斷 | 否 | 是 (lockInterruptibly) |
| 超時獲取 | 否 | 是 (tryLock(long, TimeUnit)) |
| 公平性 | 非公平 | 兩者都可選(默認(rèn)非公平) |
| ** Condition ** | 單一 | 多個 |
選擇建議:
- 優(yōu)先使用 synchronized:如果你的需求很簡單,用
synchronized就足夠了。因為它寫起來更簡潔,不容易出錯(不會忘記釋放鎖),并且 JVM 對其有持續(xù)的優(yōu)化。 - 當(dāng)需要以下高級功能時,才使用
Lock:- 需要嘗試獲取鎖(
tryLock)。 - 需要可中斷的鎖。
- 需要超時獲取鎖。
- 需要實(shí)現(xiàn)公平鎖。
- 需要多個 Condition 來實(shí)現(xiàn)復(fù)雜的線程協(xié)作(比如復(fù)雜的生產(chǎn)者-消費(fèi)者模型)。
- 需要嘗試獲取鎖(
到此這篇關(guān)于Java中Lock使用詳解的文章就介紹到這了,更多相關(guān)Java中Lock使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA 去除 mybatis.xml 文件黃色警告的圖文教程
這篇文章主要介紹了IDEA 去除 mybatis.xml 文件黃色警告的方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07
了解java中的Clojure如何抽象并發(fā)性和共享狀態(tài)
Clojure是一種運(yùn)行在Java平臺上的 Lisp 方言,Lisp是一種以表達(dá)性和功能強(qiáng)大著稱的編程語言,但人們通常認(rèn)為它不太適合應(yīng)用于一般情況,而Clojure的出現(xiàn)徹底改變了這一現(xiàn)狀。,需要的朋友可以參考下2019-06-06
Vue3源碼解讀effectScope API及實(shí)現(xiàn)原理
這篇文章主要為大家介紹了Vue3源碼解讀effectScope API及實(shí)現(xiàn)原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
使用Spring的StopWatch實(shí)現(xiàn)代碼性能監(jiān)控的方法詳解
在開發(fā)過程中,偶爾還是需要分析代碼的執(zhí)行時間,Spring 框架提供了一個方便的工具類 StopWatch,本文將介紹 StopWatch 的基本用法,并通過示例演示如何在項目中使用 StopWatch 進(jìn)行代碼性能監(jiān)控2023-12-12
基于Nacos實(shí)現(xiàn)Spring Cloud Gateway實(shí)現(xiàn)動態(tài)路由的方法
這篇文章主要介紹了基于Nacos實(shí)現(xiàn)Spring Cloud Gateway實(shí)現(xiàn)動態(tài)路由的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
Java多線程輪流打印ABC的四種實(shí)現(xiàn)方式
在Java中實(shí)現(xiàn)多線程輪流打印ABC的操作是一種常見的并發(fā)編程練習(xí),它展示了如何利用多線程提高程序執(zhí)行的效率和并行性,本文將深入探討如何用Java實(shí)現(xiàn)這一任務(wù),需要的朋友可以參考下2025-08-08
struts2中通過json傳值解決亂碼問題的實(shí)現(xiàn)方法
這篇文章主要介紹了struts2中通過json傳值解決亂碼問題的實(shí)現(xiàn)方法,涉及js編碼及java解碼的相關(guān)操作技巧,需要的朋友可以參考下2016-06-06

