ReentrantLock原始碼
-
JUC 指java.util.concurrent包下,一系列關於併發的類,JUC就是包名的首字母
-
CAS 比較並交換,可以看另一篇文章
-
AQS 指主要利用CAS來實現的輕量級多執行緒同步機制,並且不會在CPU上出現上下文切換和排程的情況
自定義鎖
如何在自己實現一個鎖?
可以定義一個屬性來判斷當前是否有其執行緒在執行,如果正在執行那麼其他執行緒需要等待
如何實現? 例如有兩個執行緒T1和T2,都執行同一段程式碼
自定義兩個方法
public void lock();
public void unlock();
public void addI(){
i++;
}
將上面的addI方法更改為下面的
public void addI(){
lock();
i++;
unlock();
}
這裡忽略程式出錯導致死鎖的情況,正常解鎖需要放在finally程式碼塊中
當T1進入程式碼,將鎖的改為被持有的狀態
/**
* 0為未持有
* 1為被持有
*/
private volatile int i=0;
public void lock(){
//CAS修改成功返回true
while(CAS(i,1)){
return
}
}
public void unlock(){
i=0;
}
上面的虛擬碼當T1進入lock方法後,因為是第一個進入的,鎖的狀態還是0,通過cas可以改為1,修改成功返回true,進入迴圈return到addI方法,執行i++操作,然後進入unLock方法,將狀態改為0,方法結束
假設當T1進入方法將狀態改為1,那麼T2進入會一直迴圈CAS修改,執行緒一直在自旋不會走下面的程式碼,直到鎖的狀態改為0,才會繼續業務程式碼
那麼我們就實現了一個簡單的鎖,但是這個鎖有什麼缺點呢? 沒有獲取到鎖的執行緒會一直自旋,消耗系統資源,這個是我們不想看到的
在java中還有一個類LockSupport
,其中有一個park方法
public static void park() {
UNSAFE.park(false, 0L);
}
public native void park(boolean var1, long var2);
裡面繼續呼叫UNSAFE類,這個類裡的方法是使用C/C++實現,park方法的作用是將當前執行緒立即休眠,讓出CPU,直到被喚醒,還有一個喚醒的方法
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
public native void unpark(Object var1);
這個同樣也是其他語言實現,傳入需要被喚醒的執行緒,那麼我們上面的程式碼可以改造為
/**
* 0為未持有
* 1為被持有
*/
private volatile int i=0;
//存放等待獲取鎖的執行緒
private Thread t;
public void lock(){
//CAS修改成功返回true
if(CAS(i,1)){
return
}
//將沒有獲取到鎖的執行緒存放
t=Thread.currentThread()
//如果沒有獲取到鎖則進行休眠
LockSupport.park();
}
public void unlock(){
i=0;
if(t!=null){
LockSupport.unpark(t);
}
}
我們修改完後即使沒有獲取到鎖的執行緒也不會佔用CPU的資源,但是如果出現2個以上的執行緒同時進行操作,那麼會出現丟失執行緒的情況,可以再進行優化,將等待的執行緒存放到佇列中,就不再演示了,而ReentrantLock就是主要使用CAS,park,自旋來實現的,接下來看ReentrantLock的原始碼
ReentrantLock
當初始化一個ReentrantLock使用預設構造時建立的是一個非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
如果想建立一個公平鎖則使用有參構造
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
這篇文章先來看公平鎖的實現
public void service() {
//建立一個公平鎖
ReentrantLock reentrantLock = new ReentrantLock(true);
reentrantLock.lock();
try {
System.out.println("==這裡有一堆的業務===");
} catch (Exception e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
加鎖
沒有競爭情況
public void lock() {
sync.lock();
}
呼叫的sync是一個ReentrantLock的內部抽象類
abstract static class Sync extends AbstractQueuedSynchronizer{
......
}
它的公平鎖的實現方法,是FairSync類中的,也是一個內部類,在ReentrantLock中,繼承了Sync類,實現lock方法
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
點進tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//獲取當前執行的執行緒
final Thread current = Thread.currentThread();
//得到鎖的狀態
int c = getState();
//如果鎖狀態為0說明當前鎖沒有被其他執行緒持有
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方法,該方法定義在AbstractQueuedSynchronizer抽象類中的
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());
}
其中tail和head這兩個變數是在AbstractQueuedSynchronizer抽象類中定義的,用來存放等待執行緒頭和尾部
因為當前執行緒執行前鎖的狀態是未被持有的,所以還沒有初始化過佇列,那麼等待佇列的頭和尾部都為null,return的第一個判斷h!=t為false,後面的&&運算子,所以直接返回
那麼回到tryAcquire方法,hasQueuedPredecessors返回false,而前面有一個取反!符號,則繼續執行compareAndSetState(0, acquires)
方法,通過cas改變當前鎖的狀態為1,然後執行setExclusiveOwnerThread
方法,該方法就是簡單的賦值
protected final void setExclusiveOwnerThread(Thread thread) {
//當前持有鎖的執行緒
exclusiveOwnerThread = thread;
}
繼續返回到acquire
方法,為true,取反false,使用了&&阻斷符,則不會執行後面的acquireQueued
方法,直接結束lock()方法,執行自定義的業務程式碼
tryAcquire方法什麼時候走到 else if (current == getExclusiveOwnerThread()) 判斷呢
ReentrantLock的特性之一就是體現在這裡-重入鎖
啥叫重入鎖?簡單講就是在加鎖後又加鎖
public void addI(){
ReentrantLock rLock =new ReentrantLock(true);
rLock.lock();
//執行業務==
rLock.lock();
//執行業務==
//解鎖最後加鎖的
rLock.unlock();
//解鎖最先加鎖的
rLock.unlock();
}
當執行緒和該鎖已經持有的執行緒相同時則會進入這個判斷,將鎖的狀態加1,賦值給state,下面的判斷state小於0可能是判斷溢位的問題,即數值超出int型別最大容量則為負數,一般這種情況很少見吧
存在競爭情況
那麼上面是沒有其他執行緒競爭的情況,如果在T1加鎖後,T2,T3..來嘗試獲取鎖改怎麼辦呢?->進等待佇列
這個還是tryAcquire方法的程式碼,拿下來方便檢視
protected final boolean tryAcquire(int acquires) {
//獲取當前執行的執行緒
final Thread current = Thread.currentThread();
//得到鎖的狀態
int c = getState();
//如果鎖狀態為0說明當前鎖沒有被其他執行緒持有
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;
}
如果在T1進行完加鎖後T2來嘗試獲取鎖,因為state狀態不為0,而當前執行緒和鎖持有的執行緒又不同,則直接返回false
那麼返回acquire方法中
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
Node.EXCLUSIVE 返回一個Node節點
取反為true,則執行acquireQueued方法,而acquireQueued方法中有執行了addWaiter方法,先來看addWaiter方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
使用連結串列的形式來儲存阻塞排隊的執行緒,來看node的內部結構
主要的三個屬性
//存放上一個節點
volatile Node prev;
//存放下一個節點
volatile Node next;
//存放當前等待的執行緒
volatile Thread thread;
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
當進入這個方法後,首先將AbstractQueuedSynchronizer類中的尾部節點賦值給一個臨時變數,判斷尾部是否為空,假設現線上程為T2,佇列還沒有被初始化,尾部為空,則進入enq
方法,繼續點進
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
//CAS設定頭節點
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
//CAS設定尾巴節點
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
還是將AbstractQueuedSynchronizer類中尾部節點賦值給臨時變數t
然後判斷t是否為空,因為佇列還沒有初始化,所以尾巴節點為空,則使用cas來設定 AbstractQueuedSynchronizer類中的頭節點,之後將設定的頭節點賦值給尾部
當執行完節點的關係如下
這時候有個疑問,怎麼沒有設定傳入的Node節點呢?而是設定新new出來的Node,和引數傳入的Node節點沒有一點關係?
注意看上面的程式碼for(;;)
死迴圈,當下次迴圈的時候t已經不為空了,因為上次迴圈給加了一個空節點,然後將傳入的Node節點的上一個賦值為t,然後通過CAS獲取AbstractQueuedSynchronizer類中的尾部節點,如果尾部節點還是為t,則更改為傳入的node物件,如果CAS失敗,即在CAS設定前被其他執行緒對AbstractQueuedSynchronizer類中的尾部節點進行了修改,則進行下一次for迴圈,直至設定成功,當操作完成後,節點結構如下圖
之後程式碼返回到acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
還是一個for死迴圈,首先獲取上一個節點和AbstractQueuedSynchronizer類中的頭節點進行判斷,如果相同則呼叫tryAcquire()方法嘗試獲取鎖,因為在初始化佇列過程中可能獲取鎖執行的執行緒已經執行完了,並且釋放了鎖,所以這裡嘗試一下獲取鎖,假設沒有獲取到鎖,則不會進入if (p == head && tryAcquire(arg)) {}程式碼塊,繼續下面的判斷,進入shouldParkAfterFailedAcquire()方法,從名稱可以看到[在獲取鎖失敗後應該睡眠]
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
判斷上一個node節點的狀態,將上一個節點的Node.SIGNAL狀態的值為-1,而上面的程式碼中並沒有對waitStatus的值進行更改,預設初始化為0,則進入最後的else程式碼塊,通過CAS將waitStatus的值改為-1,方法返回false結束,回到acquireQueued方法中,繼續進行for迴圈,假設還是沒有獲取到鎖,則再次進入shouldParkAfterFailedAcquire方法中,因為上次for迴圈將waitStatus的值改為了-1,則這次進入了if (ws == Node.SIGNAL)
的程式碼塊,返回true,返回到 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())判斷中,因為shouldParkAfterFailedAcquire方法返回了true,則繼續執行parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
當執行完parkAndCheckInterrupt方法後,T2執行緒就在這裡進行休眠
為什麼不開始就把waitStatus設定為-1呢?還要多自旋一次,有一個原因是儘量不使用park,能嘗試獲取到鎖最好
那麼假設現在又來一個執行緒T3
public final void acquire(int arg) {
//嘗試獲取鎖肯定不會成功,則進入acquireQueued,addWaiter方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//這時tail已經是t2節點了
Node pred = tail;
//不為空進入
if (pred != null) {
//將當前節點上一個節點設定為t2
node.prev = pred;
//通過CAS來設定AQS類中的尾節點
if (compareAndSetTail(pred, node)) {
//然後設定T2的下一個節點
pred.next = node;
return node;
}
}
enq(node);
return node;
}
完成操作後節點關係如下
之後繼續執行acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//獲取上一個節點:T2
final Node p = node.predecessor();
//T2不是頭節點,則不進入下面的程式碼塊
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//之後呼叫shouldParkAfterFailedAcquire方法
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//同樣的程式碼,第一次獲取T3的前一個節點T2,判斷T2的ws值為0,
//CAS修改後返回,外層迴圈再次進入這時T2的ws值為-1,返回true,方法結束
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
解鎖
假設現在T1執行unlock方法,T2,T3在佇列中
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
進入tryRelease方法
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;
}
首先將狀態數值-1,判斷如果當前執行緒和持有鎖的執行緒不是同一個則丟擲異常,即解鎖的執行緒和加鎖的不是同一個執行緒
判斷如果c==0,也就是沒有重入鎖的情況,將free改為true,然後進入setExclusiveOwnerThread方法
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final void setState(int newState) {
state = newState;
}
方法返回,沒有重入鎖的情況,則free為true,獲取AQS類中的頭節點,假設不為空,ws=-1,則進入unparkSuccessor(h)
方法
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
首先獲取頭結點的狀態,小於0進入程式碼塊,將頭結點的鎖狀態改為0,獲取下一個節點,那麼s就是t2,而t2的ws也是-1,所以直接進入最下面的程式碼塊,if(s!=null),unpark(t2)執行緒
那麼回到t2執行緒休眠的地方
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//在這裡醒來
return Thread.interrupted();
}
下面的是判斷執行緒是否被中斷過,native方法,無法看到實現了,那麼假設沒有被中斷過則返回false,那麼返回上一個方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//返回程式碼後繼續執行這裡
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
因為parkAndCheckInterrupt方法返回false,所以進不去程式碼塊,那麼繼續執行for,當執行if (p == head && tryAcquire(arg))
時p==head成立,而呼叫tryAcquire方法嘗試獲取鎖成功,因為t1已經釋放了,那麼進入下面的程式碼塊
if (p == head && tryAcquire(arg)) {
setHead(node);
//設定t2上一個節點,也就是空節點的下一個節點設定為null
p.next = null; // help GC p節點沒有任何引用指向了,幫助垃圾回收
failed = false;
return interrupted;
}
private void setHead(Node node) {
//將t2節點設定為頭部
head = node;
//然後將t2節點的thread設定為null
node.thread = null;
//節點的上一個節點設定為null
node.prev = null;
}
經過上面的操作後節點關係如下
如果這個節點在頭說明它正在執行程式碼,而不是排隊,即使初始化時T1沒有進佇列,但是給它新增了一個空node,來代替它正在執行
例如有T2,T3在排隊,T1執行緒unpark後T2執行緒執行,上面的程式碼也能說明T2會先把當前節點的執行緒,上下節點都設定為null,而T2執行緒去執行程式碼去了,已經在執行過程中了
看別的部落格有一段解釋:比如你去買車票,你如果是第一個這個時候售票員已經在給你服務了,你不算排隊,你後面的才算排隊
注意一點:佇列頭始終為空Node
如何保證公平
情況1
T1執行完unpark後,釋放完鎖,還沒來的及喚醒佇列中的T2,這時T3執行緒來嘗試獲取到鎖
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
這種情況佇列中肯定有節點排隊,如果沒有節點直接獲取到鎖也是公平的,那麼有節點排隊h就不等於t,true,&&運算子繼續判斷,h的next節點也不為null,返回false
s.thread != Thread.currentThread()
如果當前來嘗試獲取鎖的物件不是在排隊的第一個(也就是頭結點的下一個節點,頭結點正在執行,不算在排隊的佇列中)也就是其他執行緒插隊的情況,則返回true,結果就是(true&&(false||true)) 整體返回true,外層程式碼取反為false,不會嘗試CAS獲取鎖,則T3去排隊
情況2
T2嘗試獲取鎖時發現T1持有鎖,於是去初始化佇列,在初始化過程中T1執行完釋放鎖,T2執行初始化佇列程式碼時間片用完,這時T3來嘗試獲取鎖
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))<------假設T2初始化佇列執行到這裡CPU時間片用完
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
此時節點關係如下
那麼回到hasQueuedPredecessors
方法,看最後的return
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
h頭節點為一個空node,而t為節點為null,不等於true繼續判斷,h頭結點下一個為null,整體返回true,外層程式碼取反為false,則去排隊
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
遺留問題
1 初始化佇列以及後面的入隊為什麼要設定空的頭節點
2 在parkAndCheckInterrupt()方法中最後呼叫的Thread.interrupted();一系列方法最後不改變任何東西,不明白它這個的作用,也有說是為了複用lockInterruptibly()方法,但是感覺有點牽強
太笨了看不明白,希望不吝賜教,也可以加qq群一起探討:737698533