本篇文章將介紹兩種自己動手實現可重入鎖的方法。
我們都知道JDK中提供了一個類ReentrantLock,利用這個類我們可以實現一個可重入鎖,這種鎖相對於synchronized來說是一種輕量級鎖。
重入鎖的概念
重入鎖實際上指的就是一個線程在沒有釋放鎖的情況下,可以多次進入加鎖的代碼塊。
public void a() {
lock2.lock();
System.out.println("a");
b();
lock2.unlock();
}
public void b() {
lock2.lock();
System.out.println("b");
lock2.unlock();
}
new Thread(() -> {
m.a();
}).start();
這種情況下,如果我們加的鎖不是支持可重入的鎖,那么b方法中的代碼塊不會執行,如果我們的鎖是一個重入鎖,那么b方法中的打印代碼塊也會被執行。
土方法實現重入鎖
首先我們先實現一個沒有實現可重入的鎖,這個鎖實現接口Lock,代碼如下:
public class MyLock implements Lock {
//鎖標記
private boolean isLocked = false;
@Override
public synchronized void lock() {
//如果已經有一個線程獲得了鎖,那么線程就一直等待
while (isLocked)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
isLocked = true;
}
@Override
public synchronized void unlock() {
//可以進入的一定使已經獲得鎖的線程,那么直接改變標志,喚醒其他等待的線程
isLocked = false;
notify();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
代碼中使用sychronized關鍵字,來控制只有一個線程可以獲得鎖。
由於sychronized關鍵字加在類的方法上的時候,內置鎖對象實際使當前類的對象,因此,我們需要使用同一個對象來調用加鎖、解鎖方法。
這樣我們就可以保證只有一個線程可以進入加解鎖方法內部,然后通過isLocked來標記是否已經有線程獲得了鎖。
當我們使用上面介紹重入鎖的測試方式來測驗代碼時,只會打印出a,之后將一直等待,無法打印b。
下面,我們將修改方法,讓其實現可重入。
實際上,所謂可重入的方法就是將獲得鎖的線程記錄下來,如果進入方法的線程可獲得鎖的線程是同一個線程,那么我們就可以直接獲得鎖,不需要等待。
實現方法如下:
private boolean isLocked = false;
//記錄獲得鎖的線程
private Thread lockBy = null;
//記錄獲得鎖的線程的重入次數
private int count = 0;
@Override
public synchronized void lock() {
//獲取當前線程
Thread currentThread = Thread.currentThread();
//已經有線程獲得鎖,並且獲得鎖的線程不是當前線程,那么不滿足獲得鎖,線程需要等待
while (isLocked && currentThread != lockBy)
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//沒有線程獲得鎖,或者獲得鎖的線程就是當前線程
isLocked = true;
lockBy = currentThread;
//記錄當前線程的重入次數
count++;
}
@Override
public synchronized void unlock() {
//釋放鎖時,只有當獲得鎖的線程和當前線程是同一線程時才是正確的
if (lockBy == Thread.currentThread()) {
//線程重入次數減一
count--;
//只有當count變為0也就是所有獲取鎖的地方都已經釋放了,才能夠真正釋放鎖,修改標志位,喚醒其他線程
if (count == 0) {
notify();
isLocked = false;
}
}
}
當我們使用上面介紹重入鎖的測試方式來測驗代碼時,將會打印出a、b,因為鎖是可以重入的,不會出現一直等待的情況。
使用AQS類實現重入鎖
AQS類簡單介紹
AbstractQueuedSynchronizer類時JDK在1.5版本開始提供的一個可以用來實現依賴於先進先出 (FIFO) 等待隊列的阻塞鎖和相關同步器(信號量、事件,等等)框架,在這里我們不關注它的其他功能,重點介紹一下如何利用AQS實現阻塞鎖。
當我們要使用AQS實現一個鎖時,我們需要在我們的鎖類的內部聲明一個非公共的內部幫助類,讓這個類集成AbstractQueuedSynchronizer類,並實現其某些方法。
利用AQS類,我們可以實現兩種模式的鎖,一種是獨占鎖,一種是共享鎖。這兩種鎖的幫助類需要實現的方法是不同,如果是獨占鎖,那么需要實現tryAcquire(int)和tryRelease(int)方法;如果是共享鎖,那么需要實現tryAcquireShared(int)和tryReleaseShared(int)方法。
AQS類內部維護了一個FIFO的雙向鏈表,用來保存所有爭奪鎖的線程,AQS源碼中的Node類就是雙向鏈表中節點的數據結構。
當使用AQS類加鎖時,會調用方法acquire(int)方法中會調用,而acquire(int)方法中會調用tryAcquire(int)來嘗試獲得鎖,如果獲得鎖成功,方法結束,如果獲得鎖失敗,那么需要將線程維護到FIFO鏈表中,並且讓新增加的線程進入等待狀態,並且維護鏈表中線程的狀態。(這部分代碼比較復雜,可以參考網上對AQS的講解,之后會再寫一篇專門介紹AQS類的源碼解析)
注:這個方法是忽略中斷的,不忽略中斷的方法,這里不做介紹
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
當使用AQS釋放鎖時,會調用方法release(int)方法中會調用,而release(int)方法中會調用tryRelease(int)來嘗試釋放鎖,如果釋放鎖成功后,如果FIFO鏈表中有線程,那么,會喚醒所有等待狀態的線程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
利用AQS實現可重入鎖
實際上利用AQS實現一個可重入鎖是非常容易的,首先給出代碼。
實際上利用AQS實現鎖和用土方法實現鎖的思路大體上是相同的,只是,我們不需要關注線程的喚醒和等待,這些會有AQS幫助我們實現,我們只需要實現方法tryAcquire(int)和tryRelease(int)就可以了。
這里我們實際上是應用AQS中的int值保存當前線程的重入次數。
加鎖思路:
如果第一個線程進入可以拿到鎖,可以返回true,
如果第二個線程進入,拿不到鎖,返回false,
有一種特例(實現可重入),如果當前進入線程和當前保存線程為同一個,允許拿到鎖,但是有代價,更新狀態值,也就是記錄線程的重入次數
public class MyLock2 implements Lock {
private Helper helper = new Helper();
//實現一個私有的幫助類,繼承AQS類
private class Helper extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
//AQS中的int值,當沒有線程獲得鎖時為0
int state = getState();
Thread t = Thread.currentThread();
//第一個線程進入
if (state == 0) {
//由於可能有多個線程同時進入這里,所以需要使用CAS操作保證原子性,這里不會出現線程安全性問題
if (compareAndSetState(0, 1)) {
//設置獲得獨占鎖的線程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
} else if (getExclusiveOwnerThread() == t) {
//已經獲得鎖的線程和當前線程是同一個,那么state加一,由於不會有多個線程同時進入這段代碼塊,所以沒有線程安全性問題,可以直接使用setState方法
setState(state + 1);
return true;
}
//其他情況均無法獲得鎖
return false;
}
@Override
protected boolean tryRelease(int arg) {
//鎖的獲取和釋放使一一對應的,那么調用此方法的一定是當前線程,如果不是,拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new RuntimeException();
}
int state = getState() - arg;
boolean flag = false;
//如果state減一后的值為0了,那么表示線程重入次數已經降低為0,可以釋放鎖了。
if (state == 0) {
setExclusiveOwnerThread(null);
flag = true;
}
//無論是否釋放鎖,都需要更改state的值
setState(state);
//只有state的值為0了,才真正釋放了鎖,返回true
return flag;
}
Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
helper.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
helper.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return helper.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return helper.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
helper.release(1);
}
@Override
public Condition newCondition() {
return helper.newCondition();
}
}
至此,我們已經使用兩種方式實現了一個重入鎖。
