章節:
多線程併發 (一) 瞭解 Java 虛擬機 - JVM
多線程併發 (二) 瞭解 Thread
多線程併發 (三) 鎖 synchronized、volatile
多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量,CAS機制
多線程併發 (五) ReentrantLock 使用和源碼
對於多線程併發學過了併發產生的原因,併發產生的問題,併發產生問題的解決方式,對於之前介紹的併發問題的解決方式有synchronzied、volatile、原子類型無鎖控制。瞭解最後一個鎖ReentrantLock重入鎖。ReentrantLock的實現其實是利用了CAS + volatile+LockSupport 的方式控制線程安全的,也就是面試經常問道,不用鎖如何控制多線程安全。
1.ReentrantLock簡單使用
ReentrantLock和synchronzied都是獨佔式重入鎖,之前介紹過ReentrantLock是顯示鎖、synchronzied是內部鎖,對於synchronzied的使用十分簡單,能滿足我們工作中的大部分需求。相對於ReentrantLock的使用就比synchronzied略有複雜,但是ReentrantLock能解決業務比較複雜的場景。
1) 對比
- synchronzied鎖的是對象(鎖是保存在對象頭裏面的,根據對象頭數據來標識是否有線程獲得鎖/爭搶鎖),ReentrantLock鎖的是線程(根據進入的線程和int類型的state標識鎖的獲得/爭搶)
- synchronzied通過Object中的wait()/nofify()方法實現線程間通訊,ReentrantLock通過Condition的await()/signal()方法實現線程間通訊
- synchronzied是非公平鎖,ReentrantLock可選擇公平鎖/非公平鎖
- synchronzied涉及到鎖的升級無鎖->偏向鎖->自旋鎖->向OS申請重量級鎖,ReentrantLock實現不涉及鎖,利用CAS自旋機制和volatile同步隊列實現鎖的功能
- ReentrantLock具有tryLock嘗試獲取鎖以及tryLock timeout,可主動release釋放使用靈活
2) 簡單例子
public class Test {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
lock.lock();
new Thread(new SignalThread()).start();
System.out.println("等待通知");
try {
condition.await();
} finally {
lock.unlock();
}
System.out.println("恢復運行");
}
static class SignalThread implements Runnable {
@Override
public void run() {
lock.lock();
try {
condition.signal();
System.out.println("通知");
} finally {
lock.unlock();
}
}
}
}
使用了Condition + Reentrantlock實現線程間通信,和synchronzied的使用其實差別不大,使用的時候要保證lock()和unLock()方法的調用對應,調用次數保證相同。對於ReentrantLock的使用不做太多介紹,不熟悉的可以搜索用法。
2.ReentrantLock 源碼實現
ReentrantLock其實是對 AbstractQueuedSynchronizer 子類 Sync 的一個封裝,可以把ReentrantLock理解成一個包裝類,主要邏輯都在AbstractQueuedSynchronizer(AQS) 和 Sync 子類裏面,所以首先我們要學習的源碼要從AQS開始。
代碼結構圖:
可知 ReentrantLock 分爲公平鎖FairSync和非公平鎖NofairSync,這兩種鎖都是繼承自Sync,並且是AQS的子類。
學習源碼我們從兩方面入手:1.數據結構、2.算法代碼
- AQS的數據結構
AQS是一個同步隊列,是以Node類爲一個節點的雙向鏈表並且有首和尾指針。// 首指針 private transient volatile Node head; // 尾指針 private transient volatile Node tail; // 是否有線程佔用:0-無,1-有線程佔用,>1-當前線程重入的次數 private volatile int state;
AQS中主要有三個參數而且都是被volatile修飾的,其中他們的更新方式是通過CAS機制Unsafe更新的,這塊可以看多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量,CAS機制 瞭解CAS的參數含義。
Node內部類:static final class Node { volatile int waitStatus; //當前線程的等待狀態 volatile Node prev; volatile Node next; volatile Thread thread; //當前線程 }
1)prev:指向前一個結點的指針
2) next:指向後一個結點的指針
3) thread:當前結點表示的線程,因爲同步隊列中的結點內部封裝了之前競爭鎖失敗的線程,故而結點內部必然有一個對應線 程實例的引用
4) waitStatus:對於重入鎖而言,主要有3個值。
0:初始化狀態;
-1(SIGNAL):當前結點表示的線程在釋放鎖後需要喚醒後續節點的線程;
1(CANCELLED):在同步隊列中等待的線程等待超時或者被中斷,取消繼續等待1)隊列中每個Node節點就代表一個等待獲取鎖的線程,其中head指的那個node節點就是當前當用鎖的節點,當n1釋放鎖之後就會喚醒n2一次類推
2)當有新線程n3加入隊列時候,就會從tail尾部加入,改變tail的指向。
從上圖容易知道隊列的結構,具體如何被添加進入隊列又是如何釋放的,繼續看算法~ - 算法
1)線程被加鎖/加入隊列
從簡單使用引入public static void main(String[] args) throws InterruptedException { ReentrantLock lock = new ReentrantLock();//初始化鎖類型 lock.lock(); //進入加鎖流程 try { } finally { lock.unlock(); //釋放鎖 } }
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
final void lock() { if (compareAndSetState(0, 1)) //判斷是否有線程獲取了鎖 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
2)如果沒被其他線程佔用即state = 0,這時把當前線程設置到 AbstractOwnableSynchronizer 內存表示當前佔用的線程
3)如果state != 0 ,繼續 acquire(1) 把當前線程加入等待隊列public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 當前狀態 if (c == 0) { // 非公平的這裏會再次嘗試獲取鎖的機會和上面類似 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } }//如果還是當前的線程說明當前線程重入了這個鎖,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; // 不是當前線程並且鎖被其他線程佔用了 返回false }
1)第一部分if 中如果state=0了,那就直接佔用這個鎖,這裏也是非公平鎖的體現,並沒有從隊列中取,直接把鎖讓給了當前申請的線程
2)第二部分else if 中如果還是當前的線程那state +1 ,表示當前線程重入了這個鎖
3)三 是個新的線程進入並且鎖被其他線程佔用,返回false
所以回到上面 當tryAcquire(arg) 返回true 結束,返回false繼續走acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
首先看addWaiter()方法private Node addWaiter(Node mode) { Node node = new Node(mode); //創建一個新節點,mode = null for (;;) { //無線循環 Node oldTail = tail; //拿到當前的未隊列, if (oldTail != null) { //不爲空 U.putObject(node, Node.PREV, oldTail); if (compareAndSetTail(oldTail, node)) { //移動尾部指針對象 oldTail.next = node; //把當前node加入隊列 return node; } } else { initializeSyncQueue(); //爲空初始化 看下方 } } } private final void initializeSyncQueue() { Node h; if (U.compareAndSwapObject(this, HEAD, null, (h = new Node()))) //給head賦值 tail = h; //給tail賦值 }
1)U.putObject(node, Node.PREV, oldTail); 這個是Unsafe 中的方法,意思是把oldTail 賦值給node中的 prev。
2)compareAndSetTail(oldTail, node) if 判斷中的這塊代碼,意思是把tail這個指針從之前的oldTail指向node 看圖 例如之前 tail = n2(oldTail) ,現在加入了一個線程n3,這時候 tail = n3
3)oldTail.next = node; 看圖就是 n2.next = n3
繼續回到acquireQueued()方法acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
final boolean acquireQueued(final Node node, int arg) { try { boolean interrupted = false; for (;;) { // 死循環 final Node p = node.predecessor(); //獲取當前節點的上一個節點 if (p == head && tryAcquire(arg)) { //判斷是否是head節點 setHead(node); // 把當前節點設置成head p.next = null; // 把之前的head節點從鏈表中釋放,讓內存回收 return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 暫停當前線程 interrupted = true; } } catch (Throwable t) { cancelAcquire(node); throw t; } }
private final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 暫停當前線程 return Thread.interrupted(); }
2)線程釋放鎖/從隊列中移除
釋放鎖相對簡單通過主動調用unLock()方法,public final boolean release(int arg) { if (tryRelease(arg)) { //釋放 state = 0 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 解除線程的park等待 return true; } return false; }
先是釋放state的值,因爲他是鎖是否被佔用的標識。然後unpark線程。
private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) node.compareAndSetWaitStatus(ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; // 把取消的線程移除,輪尋直到線程沒有被取消 if (s == null || s.waitStatus > 0) { s = null; for (Node p = tail; p != node && p != null; p = p.prev) if (p.waitStatus <= 0) s = p; } if (s != null) LockSupport.unpark(s.thread); //釋放當前節點的線程 }