Java併發程式設計——ReentrantLock重入鎖解析
重入鎖
所謂重入鎖,即支援重入性,表示能夠對共享資源重複加鎖,即當前執行緒獲取該鎖再次獲取不會被阻塞。
重入性
線上程獲取鎖的時候,如果已經獲取鎖的執行緒是當前執行緒的話則直接再次獲取成功; 由於鎖會被獲取n次,那麼只有鎖在被釋放同樣的n次之後,該鎖才算是完全釋放成功。
ReentrantLock分為公平鎖與非公平鎖,預設選擇的是非公平鎖(無參建構函式),另一種構造方式,可傳入一個boolean值。true時為公平鎖。
以非公平鎖為例,來看看它是如何獲取鎖。核心方法為其內部的抽象靜態類Sync中的nonfairTryAcquire
/** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //state為0表示該鎖沒有被任何執行緒佔有,可以被獲取 if (c == 0) { //如果執行緒當前狀態值accquires為0 if (compareAndSetState(0, acquires)) { //設定當前擁有獨佔訪問許可權的執行緒。 setExclusiveOwnerThread(current); return true; } } //如果執行緒被佔有,檢查佔有執行緒是否為當前執行緒 else if (current == getExclusiveOwnerThread()) { //再次獲取鎖,計數加1 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //如果被其他執行緒佔有,則獲取鎖失敗 return false; }
每次重新獲取鎖都會對同步狀態加1操作,那麼釋放鎖相對的,就會進行減一操作,釋放鎖核心方法tryRelease
protected final boolean tryRelease(int releases) { //同步狀態減1 int c = getState() - releases; //如果當前執行緒,不是鎖佔有執行緒,丟擲異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //只有當同步狀態c等於0,鎖才能被成功釋放。 if (c == 0) { free = true; setExclusiveOwnerThread(null); } //c不等於0,鎖釋放失敗 返回false setState(c); return free; }
每次鎖重新獲取都會對同步狀態進行加一的操作,同樣釋放鎖tryRelease執行減一操作,必須等到同步狀態為0時,鎖才算成功釋放。也就是鎖被獲取了n次,只有釋放n次才算成功釋放。
公平鎖與非公平鎖
ReentrantLock支援兩種鎖,公平鎖與非公平鎖。
ReentrantLock的建構函式可以傳遞一個boolean值,true時為公平鎖,false為非公平鎖。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
所謂公平鎖,就是鎖的獲取順序符合請求上的絕對時間順序,滿足FIFO(先入先出)。
ReentrantLock還有無參建構函式,為非公平鎖。
public ReentrantLock() {
sync = new NonfairSync();
}
上面講了非公平鎖的nonfairTryAcquire方法,這裡看看公平鎖的獲取邏輯,tryAcquire方法。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
上述程式碼可以看出,與非公平鎖的區別就在多了一個hasQueuedPredecessors()的判斷。該方法是用來判斷當前節點在佇列中是否有前驅節點。如果有,說明有執行緒比當前執行緒更早的請求資源,根據先入先出原則,所以當前執行緒請求鎖失敗。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
//當前佇列不為空,當前節點不是頭節點,當前節點的執行緒不是當前執行緒
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
公平鎖&非公平鎖
公平鎖每次獲取到鎖為同步佇列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的執行緒下次繼續獲取該鎖,則有可能導致其他執行緒永遠無法獲取到鎖,造成“飢餓”現象。
公平鎖為了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低效能開銷。因此,ReentrantLock預設選擇的是非公平鎖,則是為了減少一部分上下文切換,保證了系統更大的吞吐量。
一個小栗子
package com.lw.study.thread;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author:
* @Date: 16:46 2018/8/30
*/
public class LockDemo extends Thread{
private static ReentrantLock lock = new ReentrantLock();
private static int count = 0;
@Override
public void run() {
try {
lock.lock();
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName : " + Thread.currentThread().getName() +" : " +count);
count ++;
}
}finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
LockDemo lockDemo = new LockDemo();
LockDemo lockDemo2 = new LockDemo();
lockDemo.start();
lockDemo2.start();
lockDemo.join();
lockDemo2.join();
System.out.println(count);
}
}
輸出結果
hreadName : Thread-0 : 0
ThreadName : Thread-0 : 1
ThreadName : Thread-0 : 2
ThreadName : Thread-0 : 3
ThreadName : Thread-0 : 4
ThreadName : Thread-1 : 5
ThreadName : Thread-1 : 6
ThreadName : Thread-1 : 7
ThreadName : Thread-1 : 8
ThreadName : Thread-1 : 9
10
可以看到,在Thread-0執行完後,Thread-1才開始執行。如果不加鎖,結果將是兩個執行緒交替隨機執行。
ThreadName : Thread-0 : 0
ThreadName : Thread-0 : 1
ThreadName : Thread-1 : 0
ThreadName : Thread-0 : 2
ThreadName : Thread-1 : 3
ThreadName : Thread-0 : 4
ThreadName : Thread-1 : 5
ThreadName : Thread-0 : 6
ThreadName : Thread-1 : 7
ThreadName : Thread-1 : 9
10