一、ReentrantLock簡介
1.1 特點(diǎn)
ReentrantLock具有如下的特點(diǎn):
- 可重入
- 可中斷
- 可設(shè)置超時時間
- 可設(shè)置為公平鎖
- 支持條件變量
后面會重點(diǎn)講解其特點(diǎn)的實(shí)現(xiàn)原理时迫。
1.2 代碼結(jié)構(gòu)
其代碼結(jié)構(gòu)如下圖:
有三個內(nèi)部類颅停,分別是:Sync、FairSync掠拳、NonfairSync癞揉。
Sync繼承自AbstractQueuedSynchronizer。
AbstractQueuedSynchronizer當(dāng)中有Node和ConditionObject兩個內(nèi)部類溺欧。
通過上圖類的字面意思應(yīng)該能大概知曉前面提到的特點(diǎn)都是在哪里實(shí)現(xiàn)的了喊熟。
二、原理解析
2.1 可重入
可重入是指同一個線程如果首次獲得了這把鎖姐刁,因?yàn)樗沁@把鎖的擁有者芥牌,因此有權(quán)利再次獲取這把鎖。如果是不可重入鎖聂使,那么第二次嘗試獲得鎖時壁拉,自己也會被鎖擋住。
前面學(xué)習(xí)的synchronized也是可重入鎖柏靶。
可重入使用實(shí)例如下:
public class Test {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
System.out.println("method1");
method2();
} finally {
lock.unlock();
System.out.println("method1 unlock");
}
}
public static void method2() {
lock.lock();
try {
System.out.println("method2");
method3();
} finally {
lock.unlock();
System.out.println("method2 unlock");
}
}
public static void method3() {
lock.lock();
try {
System.out.println("method3");
} finally {
lock.unlock();
System.out.println("method3 unlock");
}
}
}
結(jié)果:
method1
method2
method3
method3 unlock
method2 unlock
method1 unlock
需要注意的是扇商,lock.unlock()一定要在finally塊的第一行。
- 源碼分析
還是使用之前的代碼宿礁,一步步跟蹤:
獲取鎖
public void lock() {
sync.lock();
}
Sync的lock方法有兩個實(shí)現(xiàn)類案铺,公平鎖和非公平鎖:
此處使用的是非公平鎖,因?yàn)槌跏蓟疪eetrantLock時梆靖,默認(rèn)使用非公平鎖NonfairLock:
public ReentrantLock() {
sync = new NonfairSync();
}
繼續(xù)源碼跟蹤控汉,非公平鎖當(dāng)中的lock()方法:
final void lock() {
// 此處使用自旋鎖,判斷當(dāng)前線程是否持有該鎖返吻,如果是0的話姑子,則將值替換成1
if (compareAndSetState(0, 1))
// 上述比較成立,設(shè)置此線程獨(dú)占該鎖
setExclusiveOwnerThread(Thread.currentThread());
else
// 比較不成立测僵,嘗試獲取鎖
acquire(1);
}
當(dāng)首次執(zhí)行上鎖操作街佑,一定走的的是上面的setExclusiveOwnerThread流程,當(dāng)線程重入或其他線程嘗試獲取該鎖捍靠,走下面的acquire(1):
public final void acquire(int arg) {
// 嘗試獲取鎖沐旨,
if (!tryAcquire(arg) &&
// 使用短路邏輯運(yùn)算符,當(dāng)獲取失敗榨婆,就繼續(xù)向下走磁携,會將線程添加到等待隊(duì)列當(dāng)中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 中斷當(dāng)前線程,其實(shí)是設(shè)置中斷標(biāo)記
selfInterrupt();
}
我們主要關(guān)注tryAcquire良风,看起如何實(shí)現(xiàn)鎖重入的谊迄,忽略中間過程闷供,直接查看如下代碼:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 獲取當(dāng)前線程同步狀態(tài),state使用volatile修飾
int c = getState();
// 表示沒有線程持有鎖
if (c == 0) {
// 自旋加鎖统诺,與前面的過程相同
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 當(dāng)前線程就是持有鎖的線程
else if (current == getExclusiveOwnerThread()) {
// 對當(dāng)前狀態(tài) 加 1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 設(shè)置狀態(tài)值
setState(nextc);
return true;
}
return false;
}
關(guān)于unlock釋放鎖過程歪脏,直接放關(guān)鍵代碼:
protected final boolean tryRelease(int releases) {
// 獲取當(dāng)前狀態(tài) 減1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 鎖重入可以多次,只有當(dāng)狀態(tài)減為0粮呢,才能釋放鎖唾糯。
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
關(guān)于鎖重入的原理比較簡單,就介紹到這里鬼贱。
2.2 可中斷
ReetrantLock處理提供一個常規(guī)的lock()方法之外移怯,還提供了一個可中斷的方法lockInterruptibly(),當(dāng)時用此方法獲取鎖時这难,如果持有鎖的線程發(fā)生中斷舟误,則該方法將拋出異常:
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
System.out.println("啟動...");
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("等鎖的過程中被打斷");
return;
}
try {
System.out.println("獲得了鎖");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
System.out.println("獲得了鎖");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
t1.interrupt();
System.out.println("執(zhí)行打斷");
} finally {
lock.unlock();
}
}
}
結(jié)果:
獲得了鎖
啟動...
執(zhí)行打斷
等鎖的過程中被打斷
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.cloud.bssp.juc.reetrantlock.InterruptTest.lambda$main$0(InterruptTest.java:19)
at java.lang.Thread.run(Thread.java:748)
如果使用的是lock()方法,即使線程發(fā)生中斷姻乓,仍然可以獲取到鎖嵌溢,且不會拋出任何異常。
2.3 可設(shè)置超時時間
ReetrantLock提供了兩個獲取鎖并快速返回的方法蹋岩,不會一直等待赖草,無論成功失敗都將立即返回:
tryLock()
當(dāng)鎖沒有被持有時,即使該鎖是公平鎖剪个,那么tryLock()方法仍會會立即獲得該鎖秧骑,違背公平的原則,但是很有用扣囊。tryLock(long timeout, TimeUnit unit)
可以設(shè)置超時時間乎折,與tryLock不同的是,此方法在設(shè)置時間結(jié)束時侵歇,會嘗試獲取鎖骂澄,如果成功,則持有鎖并立即返回惕虑,當(dāng)有任何公平原則存在坟冲,且有線程正在等待獲取鎖時,都不能獲取到鎖溃蔫,這與tryLock形成鮮明對比健提。
tryLock()測試:
public class TryLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
if (!lock.tryLock()) {
System.out.println("獲取鎖失敗");
return;
} else {
try {
System.out.println("獲取鎖成功");
} finally {
lock.unlock();
}
}
});
lock.lock();
try {
t1.start();
TimeUnit.SECONDS.sleep(1);
} finally {
lock.unlock();
}
}
}
結(jié)果:
獲取鎖失敗
定時tryLock()如下所示:
public class TryLockTimeTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("等待一秒獲取鎖失敗");
return;
} else {
try {
System.out.println("等待一秒獲取鎖成功");
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
lock.lock();
try {
t1.start();
System.out.println("等待兩秒");
TimeUnit.SECONDS.sleep(2);
} finally {
lock.unlock();
}
}
}
結(jié)果:
等待兩秒
等待一秒獲取鎖失敗
2.4 設(shè)置為公平鎖
前面就提到過ReentrantLock 默認(rèn)是不公平的。
之所以使用非公平是因?yàn)楣芥i一般是沒有必要的酒唉,而且會降低并發(fā)度西潘。
使用如下的方式創(chuàng)建公平鎖:
ReentrantLock lock = new ReentrantLock(true);
跟蹤器構(gòu)造器:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
關(guān)注其公平鎖實(shí)現(xiàn):
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 繼承自AQS的方法铐姚,內(nèi)部先調(diào)用tryAcquire獲取鎖财喳,獲取失敗則添加下城到等待隊(duì)列當(dāng)中
acquire(1);
}
/**
* 公平鎖版本的tryAcquire
*/
protected final boolean tryAcquire(int acquires) {
// 獲取當(dāng)前線程
final Thread current = Thread.currentThread();
// 獲取鎖的狀態(tài)
int c = getState();
// 0表示鎖沒有被持有
if (c == 0) {
// 判斷當(dāng)前等待隊(duì)列是否有節(jié)點(diǎn)在等待遭贸,沒有才去競爭
if (!hasQueuedPredecessors() &&
// 比較并替換狀態(tài)
compareAndSetState(0, acquires)) {
// 設(shè)置當(dāng)前線程為獨(dú)占線程
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;
}
}
2.5 條件變量
ReentrantLock支持多個條件變量。
如何理解上面這句話网沾?我們在前面學(xué)習(xí)synchronized時癞蚕,介紹了其wait方法,當(dāng)線程調(diào)用wait方法時辉哥,會從線程的持有者owner變成等待狀態(tài)桦山,會加入到Monitor的WaitSet當(dāng)中,當(dāng)有其他線程再次調(diào)用wait醋旦,仍然會添加進(jìn)來恒水。就好比一個公共的休息室一樣。
而ReentrantLock的多個條件變量就好比成多個休息室饲齐。
ReentrantLock實(shí)現(xiàn)多個條件變量要使用到await()/signal()方法钉凌,以及conditionObject隊(duì)列,后面我們慢慢講解捂人,首先看下其用法:
public class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
static Condition Tom = lock.newCondition();
static Condition Jerry = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
Tom.await();
System.out.println("吃到了魚");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
Jerry.await();
System.out.println("吃到了奶酪");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
TimeUnit.SECONDS.sleep(1);
try {
lock.lock();
System.out.println("魚來了");
Tom.signal();
} finally {
lock.unlock();
}
TimeUnit.SECONDS.sleep(1);
try {
lock.lock();
System.out.println("奶酪來了");
Jerry.signal();
} finally {
lock.unlock();
}
}
}
結(jié)果:
魚來了
吃到了魚
奶酪來了
吃到了奶酪
如上所示御雕,有幾個重點(diǎn):
- await前需要獲得鎖
- await后鎖是被釋放的
- 調(diào)用signal喚醒線程,但是同樣需要獲取鎖滥搭,否則會報錯酸纲。喚醒后的線程重新競鎖,并且從await后繼續(xù)執(zhí)行瑟匆。
- lock后一定記得unlock闽坡。
下面我們重點(diǎn)關(guān)注下是如何實(shí)現(xiàn)的?只講解重點(diǎn)方法
await方法:
public final void await() throws InterruptedException {
// 如果線程狀態(tài)是中斷愁溜,則拋出異常
if (Thread.interrupted())
throw new InterruptedException();
// 將當(dāng)前線程加入條件等待隊(duì)列
Node node = addConditionWaiter();
// 釋放鎖的占用
int savedState = fullyRelease(node);
int interruptMode = 0;
// 當(dāng)節(jié)點(diǎn)不在同步等待隊(duì)列時
while (!isOnSyncQueue(node)) {
// 阻塞當(dāng)前線程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 獲取等待隊(duì)列的鎖 并且不拋出中斷異常
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
// 重新設(shè)置中斷標(biāo)記
interruptMode = REINTERRUPT;
// 清除取消的節(jié)點(diǎn)
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 如果中斷模式不是0无午,則根據(jù)狀態(tài)決定拋出異常,中斷線程還是什么都不執(zhí)行
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
addConditionWaiter方法:
private Node addConditionWaiter() {
// 當(dāng)前conditionObject當(dāng)中的最后一個等待者
Node t = lastWaiter;
// 如果最后一個等待者被取消祝谚,請清空(不是null宪迟,且狀態(tài)不是等待)
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 將當(dāng)前線程設(shè)置為等待狀態(tài)的節(jié)點(diǎn)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
signal方法:
public final void signal() {
// 判斷線程是否持有了鎖,沒有則拋出異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
//如果條件隊(duì)列的第一等待者不是null交惯,執(zhí)行信號喚醒
if (first != null)
doSignal(first);
}
doSignal方法
private void doSignal(Node first) {
do {
如果第一個節(jié)點(diǎn)的下一個等待者是null
if ( (firstWaiter = first.nextWaiter) == null)
//則條件隊(duì)列的最后一個等待者設(shè)置為null
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
transferForSignal方法
final boolean transferForSignal(Node node) {
// 比較節(jié)點(diǎn)狀態(tài)是否是condition次泽,是則更新成0,否則返回false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 添加節(jié)點(diǎn)到同步等待隊(duì)列
*/
Node p = enq(node);
int ws = p.waitStatus;
// 此處等待狀態(tài)是0席爽,比較并替換狀態(tài)為SIGNAL
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 解除線程阻塞狀態(tài)
LockSupport.unpark(node.thread);
return true;
}
關(guān)于ReentrantLock就簡單介紹這些了意荤,其實(shí)應(yīng)該先學(xué)習(xí)AQS的,不然可能不太理解源碼只锻。