Java并發(fā)編程之ReentrantLock實(shí)現(xiàn)原理及源碼剖析
前面《Java并發(fā)編程之JUC并發(fā)核心AQS同步隊(duì)列原理剖析》介紹了AQS的同步等待隊(duì)列的實(shí)現(xiàn)原理及源碼分析,這節(jié)我們將介紹一下基于AQS實(shí)現(xiàn)的ReentranLock的應(yīng)用、特性、實(shí)現(xiàn)原理及源碼分析。
一、ReentrantLock簡介
ReentrantLock位于Java的juc包里面,從JDK1.5開始出現(xiàn),是基于AQS同步隊(duì)列的獨(dú)占模式實(shí)現(xiàn)的一種鎖。ReentrantLock使用起來比synchronized更加靈活,可以自己控制加鎖、解鎖的邏輯。ReentrantLock跟synchronized一樣也是可重入的鎖,提供了公平/非公平兩種模式:
- 公平鎖:多個(gè)線程競爭鎖的時(shí)候,會(huì)先判斷等待隊(duì)列中是否有等待的線程節(jié)點(diǎn),如果有則當(dāng)前線程會(huì)進(jìn)行排隊(duì),鎖的獲取順序符合請求的絕對時(shí)間順序,也就是 FIFO
- 非公平鎖:當(dāng)前線程競爭鎖的時(shí)候不管有沒有其他線程節(jié)點(diǎn)在排隊(duì),都會(huì)先通過CAS嘗試獲取鎖,獲取失敗了才會(huì)進(jìn)行排隊(duì)。
通過new ReentrantLock()的方式創(chuàng)建的是非公平鎖,要想創(chuàng)建公平鎖需要在構(gòu)造方法中指定new ReentrantLock(true)。ReentrantLock的常用方法如下:
- void lock() 獲取鎖,如果當(dāng)前線程獲取鎖成功將返回,獲取鎖失敗線程將被阻塞、掛起
- void lockInterruptibly() throws InterruptedException 可中斷的獲取鎖,和lock方法的不同之處在于該方法會(huì)響應(yīng)中斷,即在鎖的獲取過程中可以中斷當(dāng)前線程
- boolean tryLock() 嘗試非阻塞的獲取鎖,方法會(huì)立即返回,獲取鎖成功返回true,否則返回false
- boolean tryLock(long time, TimeUnit unit) throws InterruptedException 嘗試在指定超時(shí)時(shí)間內(nèi)獲取鎖,如果當(dāng)前線程獲取了鎖會(huì)立即返回true,如果被其他線程獲取了鎖則會(huì)被阻塞掛起,該方法會(huì)在下面三種情況下返回:1,在超時(shí)時(shí)間內(nèi)獲取了鎖,返回true;2,在超時(shí)時(shí)間內(nèi)線程被中斷;3,超時(shí)時(shí)間結(jié)束,返回false。
- void unlock() 釋放鎖
- Condition newCondition() 獲取等待通知組件,該組件與當(dāng)前的鎖綁定,當(dāng)前線程只有獲取了鎖,才能調(diào)用Condition的wait()方法,調(diào)用wait()方法后會(huì)釋放鎖
二、ReentrantLock使用
ReentrantLock的使用方式一般如下,一定要在finally里面進(jìn)行解鎖,防止程序出現(xiàn)異常無法解鎖
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
System.out.println("獲取了鎖");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
下面通過一個(gè)程序示例,演示一下ReentrantLock的使用:對同一個(gè)lock對象做多次加鎖,解鎖,演示一下ReentrantLock的鎖重入
public class ReentrantLockTest {
private Integer counter = 0;
private ReentrantLock lock = new ReentrantLock();
public void modifyResources(String threadName){
System.out.println("線程:--->"+threadName+"等待獲取鎖");
lock.lock();
System.out.println("線程:--->"+threadName+"第一次加鎖");
counter++;
System.out.println("線程:"+threadName+"做第"+counter+"件事");
//重入該鎖,我還有一件事情要做,沒做完之前不能把鎖資源讓出去
lock.lock();
System.out.println("線程:--->"+threadName+"第二次加鎖");
counter++;
System.out.println("線程:"+threadName+"做第"+counter+"件事");
lock.unlock();
System.out.println("線程:"+threadName+"釋放一個(gè)鎖");
lock.unlock();
System.out.println("線程:"+threadName+"釋放一個(gè)鎖");
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockTest tp = new ReentrantLockTest();
new Thread(()->{
String threadName = Thread.currentThread().getName();
tp.modifyResources(threadName);
},"Thread:張三").start();
new Thread(()->{
String threadName = Thread.currentThread().getName();
tp.modifyResources(threadName);
},"Thread:李四").start();
Thread.sleep(100);
}
}
程序運(yùn)行輸出如下所示:上面代碼中l(wèi)ock加鎖兩次然后解鎖兩次,在張三線程兩次解鎖完成之前,李四線程一直在等待。ReentrantLock加鎖了幾次,就要解鎖相同的次數(shù)才可以釋放鎖。
線程:--->Thread:張三等待獲取鎖
線程:--->Thread:張三第一次加鎖
線程:Thread:張三做第1件事
線程:--->Thread:張三第二次加鎖
線程:--->Thread:李四等待獲取鎖
線程:Thread:張三做第2件事
線程:Thread:張三釋放一個(gè)鎖
線程:Thread:張三釋放一個(gè)鎖
線程:--->Thread:李四第一次加鎖
線程:Thread:李四做第3件事
線程:--->Thread:李四第二次加鎖
線程:Thread:李四做第4件事
線程:Thread:李四釋放一個(gè)鎖
線程:Thread:李四釋放一個(gè)鎖
三、ReentrantLock源碼分析
ReentrantLock實(shí)現(xiàn)了Lock接口,它有一個(gè)內(nèi)部類Sync實(shí)現(xiàn)了前面介紹過的AbstractQueuedSynchronizer,而其公平鎖、非公平鎖分別通過Sync的子類FairSync、NonFairSync(也是ReentrantLock的內(nèi)部類)實(shí)現(xiàn)。下面看下其UML圖

lock()方法調(diào)用時(shí)序圖如下:

前面《Java并發(fā)編程之JUC并發(fā)核心AQS同步隊(duì)列原理剖析》介紹AQS的時(shí)候說過,AbstractQueuedSynchronizer中有一個(gè)狀態(tài)變量state,在ReentrantLock中state等于0表示沒有線程獲取鎖,如果等于1說明有線程獲取了鎖,如果大于1說明獲取鎖的線程加鎖的次數(shù),加了幾次鎖就必須解鎖幾次,每次unlock解鎖state都會(huì)減1,減到0時(shí)釋放鎖。
1、非公平鎖源碼分析
前面一篇博客《Java并發(fā)編程之JUC并發(fā)核心AQS同步隊(duì)列原理剖析》對AQS介紹的已經(jīng)非常詳細(xì)了,所以下面源碼分析中牽涉AQS中的方法就不再進(jìn)行介紹了,想了解的話可以看下那篇博客。
先看下非公平鎖的加鎖lock方法,lock方法中調(diào)用了sync的lock方法,而sync對象時(shí)根據(jù)構(gòu)造ReentrantLock時(shí)是公平鎖(FairSync)還是非公平鎖(NonFairSync)。
public void lock() {
sync.lock();
}
這里調(diào)用的是非公平鎖,所以我們看下 NonFairSync的lock方法:進(jìn)來時(shí)不管有沒有其他線程持有鎖或者等待鎖,會(huì)先調(diào)用AQS中的compareAndSetState方法嘗試獲取鎖,如果獲取失敗,會(huì)調(diào)用AQS中的acquire方法
final void lock() {
/**
* 第一步:直接嘗試加鎖
* 與公平鎖實(shí)現(xiàn)的加鎖行為一個(gè)最大的區(qū)別在于,此處不會(huì)去判斷同步隊(duì)列(CLH隊(duì)列)中
* 是否有排隊(duì)等待加鎖的節(jié)點(diǎn),上來直接加鎖(判斷state是否為0,CAS修改state為1)
* ,并將獨(dú)占鎖持有者 exclusiveOwnerThread 屬性指向當(dāng)前線程
* 如果當(dāng)前有人占用鎖,再嘗試去加一次鎖
*/
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//AQS定義的方法,加鎖
acquire(1);
}
下面看下acquire方法,會(huì)先調(diào)用NonFairSync類中重寫的tryAcquire方法嘗試獲取鎖,如果獲取鎖失敗會(huì)調(diào)用AQS中的acquireQueued方法進(jìn)行排隊(duì)、阻塞等處理。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
下面看下NonFairSync類中重寫的tryAcquire方法,里面又調(diào)用了nonfairTryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
下面看下nonfairTryAcquire方法:
- 判斷state如果為0,通過CAS的方式嘗試獲取鎖,如果獲取鎖成功,則將當(dāng)前線程設(shè)置為獨(dú)占線程
- 如果state不為0,則判斷當(dāng)前線程是否跟獨(dú)占線程時(shí)同一個(gè)線程,如果是同一個(gè)線程則將鎖的state加1,也就是鎖的重入次數(shù)加1
- 否則獲取鎖失敗,返回false
final boolean nonfairTryAcquire(int acquires) {
//acquires = 1
final Thread current = Thread.currentThread();
int c = getState();
/**
* 不需要判斷同步隊(duì)列(CLH)中是否有排隊(duì)等待線程
* 判斷state狀態(tài)是否為0,不為0可以加鎖
*/
if (c == 0) {
//unsafe操作,cas修改state狀態(tài)
if (compareAndSetState(0, acquires)) {
//獨(dú)占狀態(tài)鎖持有者指向當(dāng)前線程
setExclusiveOwnerThread(current);
return true;
}
}
/**
* state狀態(tài)不為0,判斷鎖持有者是否是當(dāng)前線程,
* 如果是當(dāng)前線程持有 則state+1
*/
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//加鎖失敗
return false;
}
下面看下非公平鎖的解鎖過程:unlock方法中調(diào)用了AQS中的release方法
public void unlock() {
sync.release(1);
}
AQS中的release方法如下所示:會(huì)先調(diào)用AQS的子類Sync中重寫的tryRelease方法去釋放鎖,如果是否鎖成功,則喚醒同步隊(duì)列中head的后續(xù)節(jié)點(diǎn),后續(xù)節(jié)點(diǎn)線程被喚醒會(huì)去競爭鎖。
public final boolean release(int arg) {
if (tryRelease(arg)) {//釋放一次鎖
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//喚醒后繼結(jié)點(diǎn)
return true;
}
return false;
}
Sync中重寫的tryRelease方法:
獲取當(dāng)前的state值,然后減1
判斷當(dāng)前線程是否是鎖的持有線程,如果不是會(huì)拋出異常。
如果state的值被減到了0,表示鎖已經(jīng)被釋放,會(huì)將獨(dú)占線程設(shè)置為空null,將state設(shè)置為0,返回true,否則返回false。
/**
* 釋放鎖
*/
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
2、公平鎖源碼分析
先看下公平鎖的加鎖lock方法,lock方法中調(diào)用了sync的lock方法,這里調(diào)用的是FairSync的lock方法。
public void lock() {
sync.lock();
}
FairSync的lock方法直接調(diào)用了AQS中的acquire方法,沒有像非公平鎖先通過CAS的方式先去嘗試獲取鎖
final void lock() {
acquire(1);
}
下面看下acquire方法,會(huì)先調(diào)用FairSync類中重寫的tryAcquire方法嘗試獲取鎖,如果獲取鎖失敗會(huì)調(diào)用AQS中的acquireQueued方法進(jìn)行排隊(duì)、阻塞等處理。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
下面看下FairSync類中重寫的tryAcquire方法,這個(gè)方法跟NonFairSync的唯一區(qū)別就是state為0的時(shí)候,公平鎖會(huì)先通過hasQueuedPredecessors()方法判斷是否隊(duì)列中是否有等待的節(jié)點(diǎn),如果沒有才會(huì)嘗試通過CAS的方式去獲取鎖,非公平鎖不會(huì)判斷直接回嘗試獲取鎖。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
/**
* 與非公平鎖中的區(qū)別,需要先判斷隊(duì)列當(dāng)中是否有等待的節(jié)點(diǎn)
* 如果沒有則可以嘗試CAS獲取鎖
*/
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//獨(dú)占線程指向當(dāng)前線程
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平鎖的unlock方法與非公平鎖的代碼一樣,這里就不再介紹了。
到此這篇關(guān)于Java并發(fā)編程之ReentrantLock實(shí)現(xiàn)原理及源碼剖析的文章就介紹到這了,更多相關(guān)Java ReentrantLock內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java并發(fā)編程之阻塞隊(duì)列(BlockingQueue)詳解
- Java中雙重檢查鎖(double checked locking)的正確實(shí)現(xiàn)
- java中synchronized Lock(本地同步)鎖的8種情況
- 詳解Java多線程tryLock()方法使用
- java并發(fā)編程工具類PriorityBlockingQueue優(yōu)先級隊(duì)列
- Java中提供synchronized后為什么還要提供Lock
- Java并發(fā)編程之StampedLock鎖介紹
- Java中l(wèi)ock和tryLock及l(fā)ockInterruptibly的區(qū)別
相關(guān)文章
踩坑Debug啟動(dòng)失敗,無報(bào)錯(cuò)信息問題
在進(jìn)行項(xiàng)目debug時(shí)遇到了無法啟動(dòng)的問題,項(xiàng)目一直處于正在啟動(dòng)狀態(tài),但未出現(xiàn)任何報(bào)錯(cuò)信息,分析原因可能是存在不合法的斷點(diǎn)位置,即斷點(diǎn)未打在方法內(nèi)部,解決方法是檢查所有斷點(diǎn)信息,并移除非法斷點(diǎn),之后項(xiàng)目能夠正常啟動(dòng)2023-02-02
Java兩個(gè)變量的互換(不借助第3個(gè)變量)具體實(shí)現(xiàn)方法
這篇文章主要介紹了Java兩個(gè)變量的互換(不借助第3個(gè)變量)具體實(shí)現(xiàn)方法,需要的朋友可以參考下2014-02-02
SpringMVC框架post提交數(shù)據(jù)庫出現(xiàn)亂碼解決方案
這篇文章主要介紹了SpringMVC框架post提交數(shù)據(jù)庫出現(xiàn)亂碼解決方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
java實(shí)現(xiàn)將文件上傳到ftp服務(wù)器的方法
這篇文章主要介紹了java實(shí)現(xiàn)將文件上傳到ftp服務(wù)器的方法,結(jié)合實(shí)例形式分析了基于java實(shí)現(xiàn)的ftp文件傳輸類定義與使用方法,需要的朋友可以參考下2016-08-08
Java實(shí)現(xiàn)中文算數(shù)驗(yàn)證碼的實(shí)現(xiàn)示例(算數(shù)運(yùn)算+-*/)
這篇文章主要介紹了Java實(shí)現(xiàn)中文算數(shù)驗(yàn)證碼的實(shí)現(xiàn)示例(算數(shù)運(yùn)算+-*/),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Java 文件傳輸助手的實(shí)現(xiàn)(單機(jī)版)
這篇文章主要介紹了Java 文件傳輸助手的實(shí)現(xiàn)(單機(jī)版),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

