Java並發編程之ReentrantLock實現原理及源碼剖析

前面《Java並發編程之JUC並發核心AQS同步隊列原理剖析》介紹瞭AQS的同步等待隊列的實現原理及源碼分析,這節我們將介紹一下基於AQS實現的ReentranLock的應用、特性、實現原理及源碼分析。

一、ReentrantLock簡介

ReentrantLock位於Java的juc包裡面,從JDK1.5開始出現,是基於AQS同步隊列的獨占模式實現的一種鎖。ReentrantLock使用起來比synchronized更加靈活,可以自己控制加鎖、解鎖的邏輯。ReentrantLock跟synchronized一樣也是可重入的鎖,提供瞭公平/非公平兩種模式:

  1. 公平鎖:多個線程競爭鎖的時候,會先判斷等待隊列中是否有等待的線程節點,如果有則當前線程會進行排隊,鎖的獲取順序符合請求的絕對時間順序,也就是 FIFO
  2. 非公平鎖:當前線程競爭鎖的時候不管有沒有其他線程節點在排隊,都會先通過CAS嘗試獲取鎖,獲取失敗瞭才會進行排隊。

通過new ReentrantLock()的方式創建的是非公平鎖,要想創建公平鎖需要在構造方法中指定new ReentrantLock(true)。ReentrantLock的常用方法如下:

  • void lock() 獲取鎖,如果當前線程獲取鎖成功將返回,獲取鎖失敗線程將被阻塞、掛起
  • void lockInterruptibly() throws InterruptedException 可中斷的獲取鎖,和lock方法的不同之處在於該方法會響應中斷,即在鎖的獲取過程中可以中斷當前線程
  • boolean tryLock() 嘗試非阻塞的獲取鎖,方法會立即返回,獲取鎖成功返回true,否則返回false
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException 嘗試在指定超時時間內獲取鎖,如果當前線程獲取瞭鎖會立即返回true,如果被其他線程獲取瞭鎖則會被阻塞掛起,該方法會在下面三種情況下返回:1,在超時時間內獲取瞭鎖,返回true;2,在超時時間內線程被中斷;3,超時時間結束,返回false。
  • void unlock() 釋放鎖
  • Condition newCondition() 獲取等待通知組件,該組件與當前的鎖綁定,當前線程隻有獲取瞭鎖,才能調用Condition的wait()方法,調用wait()方法後會釋放鎖

二、ReentrantLock使用

ReentrantLock的使用方式一般如下,一定要在finally裡面進行解鎖,防止程序出現異常無法解鎖

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
	System.out.println("獲取瞭鎖");
} catch (Exception e) {
	e.printStackTrace();
} finally {
	lock.unlock();
}

下面通過一個程序示例,演示一下ReentrantLock的使用:對同一個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+"釋放一個鎖");
        lock.unlock();
        System.out.println("線程:"+threadName+"釋放一個鎖");
    }
 
    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);
    }
}

程序運行輸出如下所示:上面代碼中lock加鎖兩次然後解鎖兩次,在張三線程兩次解鎖完成之前,李四線程一直在等待。ReentrantLock加鎖瞭幾次,就要解鎖相同的次數才可以釋放鎖。

線程:—>Thread:張三等待獲取鎖
線程:—>Thread:張三第一次加鎖
線程:Thread:張三做第1件事
線程:—>Thread:張三第二次加鎖
線程:—>Thread:李四等待獲取鎖
線程:Thread:張三做第2件事
線程:Thread:張三釋放一個鎖
線程:Thread:張三釋放一個鎖
線程:—>Thread:李四第一次加鎖
線程:Thread:李四做第3件事
線程:—>Thread:李四第二次加鎖
線程:Thread:李四做第4件事
線程:Thread:李四釋放一個鎖
線程:Thread:李四釋放一個鎖

三、ReentrantLock源碼分析

ReentrantLock實現瞭Lock接口,它有一個內部類Sync實現瞭前面介紹過的AbstractQueuedSynchronizer,而其公平鎖、非公平鎖分別通過Sync的子類FairSync、NonFairSync(也是ReentrantLock的內部類)實現。下面看下其UML圖

lock()方法調用時序圖如下:

前面《Java並發編程之JUC並發核心AQS同步隊列原理剖析》介紹AQS的時候說過,AbstractQueuedSynchronizer中有一個狀態變量state,在ReentrantLock中state等於0表示沒有線程獲取鎖,如果等於1說明有線程獲取瞭鎖,如果大於1說明獲取鎖的線程加鎖的次數,加瞭幾次鎖就必須解鎖幾次,每次unlock解鎖state都會減1,減到0時釋放鎖。

1、非公平鎖源碼分析

前面一篇博客《Java並發編程之JUC並發核心AQS同步隊列原理剖析》對AQS介紹的已經非常詳細瞭,所以下面源碼分析中牽涉AQS中的方法就不再進行介紹瞭,想瞭解的話可以看下那篇博客。

先看下非公平鎖的加鎖lock方法,lock方法中調用瞭sync的lock方法,而sync對象時根據構造ReentrantLock時是公平鎖(FairSync)還是非公平鎖(NonFairSync)。

public void lock() {
	sync.lock();
}

這裡調用的是非公平鎖,所以我們看下 NonFairSync的lock方法:進來時不管有沒有其他線程持有鎖或者等待鎖,會先調用AQS中的compareAndSetState方法嘗試獲取鎖,如果獲取失敗,會調用AQS中的acquire方法

final void lock() {
	/**
	 * 第一步:直接嘗試加鎖
	 * 與公平鎖實現的加鎖行為一個最大的區別在於,此處不會去判斷同步隊列(CLH隊列)中
	 * 是否有排隊等待加鎖的節點,上來直接加鎖(判斷state是否為0,CAS修改state為1)
	 * ,並將獨占鎖持有者 exclusiveOwnerThread 屬性指向當前線程
	 * 如果當前有人占用鎖,再嘗試去加一次鎖
	 */
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		//AQS定義的方法,加鎖
		acquire(1);
}

下面看下acquire方法,會先調用NonFairSync類中重寫的tryAcquire方法嘗試獲取鎖,如果獲取鎖失敗會調用AQS中的acquireQueued方法進行排隊、阻塞等處理。

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

下面看下NonFairSync類中重寫的tryAcquire方法,裡面又調用瞭nonfairTryAcquire方法

protected final boolean tryAcquire(int acquires) {
	return nonfairTryAcquire(acquires);
}

下面看下nonfairTryAcquire方法:

  • 判斷state如果為0,通過CAS的方式嘗試獲取鎖,如果獲取鎖成功,則將當前線程設置為獨占線程
  • 如果state不為0,則判斷當前線程是否跟獨占線程時同一個線程,如果是同一個線程則將鎖的state加1,也就是鎖的重入次數加1
  • 否則獲取鎖失敗,返回false
final boolean nonfairTryAcquire(int acquires) {
	//acquires = 1
	final Thread current = Thread.currentThread();
	int c = getState();
	/**
	 * 不需要判斷同步隊列(CLH)中是否有排隊等待線程
	 * 判斷state狀態是否為0,不為0可以加鎖
	 */
	if (c == 0) {
		//unsafe操作,cas修改state狀態
		if (compareAndSetState(0, acquires)) {
			//獨占狀態鎖持有者指向當前線程
			setExclusiveOwnerThread(current);
			return true;
		}
	}
	/**
	 * state狀態不為0,判斷鎖持有者是否是當前線程,
	 * 如果是當前線程持有 則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方法中調用瞭AQS中的release方法

public void unlock() {
	sync.release(1);
}

AQS中的release方法如下所示:會先調用AQS的子類Sync中重寫的tryRelease方法去釋放鎖,如果是否鎖成功,則喚醒同步隊列中head的後續節點,後續節點線程被喚醒會去競爭鎖。

public final boolean release(int arg) {
	if (tryRelease(arg)) {//釋放一次鎖
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);//喚醒後繼結點
		return true;
	}
	return false;
}

Sync中重寫的tryRelease方法:

獲取當前的state值,然後減1

判斷當前線程是否是鎖的持有線程,如果不是會拋出異常。

如果state的值被減到瞭0,表示鎖已經被釋放,會將獨占線程設置為空null,將state設置為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方法中調用瞭sync的lock方法,這裡調用的是FairSync的lock方法。

public void lock() {
	sync.lock();
}

FairSync的lock方法直接調用瞭AQS中的acquire方法,沒有像非公平鎖先通過CAS的方式先去嘗試獲取鎖

final void lock() {
	acquire(1);
}

下面看下acquire方法,會先調用FairSync類中重寫的tryAcquire方法嘗試獲取鎖,如果獲取鎖失敗會調用AQS中的acquireQueued方法進行排隊、阻塞等處理。

public final void acquire(int arg) {
	if (!tryAcquire(arg) &&
			acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
		selfInterrupt();
}

下面看下FairSync類中重寫的tryAcquire方法,這個方法跟NonFairSync的唯一區別就是state為0的時候,公平鎖會先通過hasQueuedPredecessors()方法判斷是否隊列中是否有等待的節點,如果沒有才會嘗試通過CAS的方式去獲取鎖,非公平鎖不會判斷直接回嘗試獲取鎖。

protected final boolean tryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState();
	if (c == 0) {
		/**
		 * 與非公平鎖中的區別,需要先判斷隊列當中是否有等待的節點
		 * 如果沒有則可以嘗試CAS獲取鎖
		 */
		if (!hasQueuedPredecessors() &&
				compareAndSetState(0, acquires)) {
			//獨占線程指向當前線程
			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方法與非公平鎖的代碼一樣,這裡就不再介紹瞭。

到此這篇關於Java並發編程之ReentrantLock實現原理及源碼剖析的文章就介紹到這瞭,更多相關Java ReentrantLock內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: