警告⚠️:本文耗時很長,先做好心理准備,建議PC端瀏覽器瀏覽效果更佳。
Java的內置鎖一直都是備受爭議的,在JDK1.6之前,synchronized這個重量級鎖其性能一直都是較為低下,雖然在1.6后,進行大量的鎖優化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),但是它卻缺少了獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖,且它為獨占式在高並發場景下性能大打折扣。
如何自己來實現一個同步
自旋實現一個同步:
volatile int status=0;//標識---是否有線程在同步塊-----是否有線程上鎖成功 void lock(){ while(!compareAndSet(0,1)){ } //lock } void unlock(){ status=0; } boolean compareAndSet(int except,int newValue){ //cas操作,修改status成功則返回true }
缺點:耗費cpu資源。沒有競爭到鎖的線程會一直占用cpu資源進行cas操作,假如一個線程獲得鎖后要花費Ns處理業務邏輯,那另外一個線程就會白白的花費Ns的cpu資源
解決思路:讓得不到鎖的線程讓出CPU
yield+自旋實現同步:
volatile int status=0; void lock(){ while(!compareAndSet(0,1)){ yield();//自己實現 } //lock } void unlock(){ status=0; }
要解決自旋鎖的性能問題必須讓競爭鎖失敗的線程不空轉,而是在獲取不到鎖的時候能把cpu資源給讓出來,yield()方法就能讓出cpu資源,當線程競爭鎖失敗時,會調用yield方法讓出cpu。
自旋+yield的方式並沒有完全解決問題,當系統只有兩個線程競爭鎖時,yield是有效的。需要注意的是該方法只是當前讓出cpu,有可能操作系統下次還是選擇運行該線程,比如里面有2000個線程,想想會有什么問題?
sleep+自旋方式實現同步:
volatile int status=0; void lock(){ while(!compareAndSet(0,1)){ sleep(10); } //lock } void unlock(){ status=0; }
park+自旋方式實現同步:
volatile int status=0; Queue parkQueue;//集合 數組 list void lock(){ while(!compareAndSet(0,1)){ // park(); } //lock 10分鍾 。。。。。。 unlock() } void unlock(){ lock_notify(); } void park(){ //將當期線程加入到等待隊列 parkQueue.add(currentThread); //將當期線程釋放cpu 阻塞 releaseCpu(); } void lock_notify(){ //得到要喚醒的線程頭部線程 Thread t=parkQueue.header(); //喚醒等待線程 unpark(t); }
這種方法就比較完美,當然我寫的都偽代碼,我看看大師是如何利用這種機制來實現同步的;JDK的JUC包下面ReentrantLock類的原理就是利用了這種機制;
ReentrantLock源碼分析之上鎖過程
AQS(AbstractQueuedSynchronizer)類的設計主要代碼(具體參考源碼)
private transient volatile Node head; //隊首 private transient volatile Node tail;//尾 private volatile int state;//鎖狀態,加鎖成功則為1,重入+1 解鎖則為0
AQS當中的隊列示意圖:
Node類的設計:
public class Node{ volatile Node prev; volatile Node next; volatile Thread thread; }
上鎖過程重點:
鎖對象:其實就是ReentrantLock的實例對象,下文應用代碼第一行中的lock對象就是所謂的鎖 自由狀態:自由狀態表示鎖對象沒有被別的線程持有,計數器為0 計數器:再lock對象中有一個字段state用來記錄上鎖次數,比如lock對象是自由狀態則state為0,如果大於零則表示被線程持有了,當然也有重入那么state則>1 waitStatus:僅僅是一個狀態而已;ws是一個過渡狀態,在不同方法里面判斷ws的狀態做不同的處理,所以ws=0有其存在的必要性 tail:隊列的隊尾 head:隊列的對首 ts:第二個給lock加鎖的線程 tf:第一個給lock加鎖的線程 tc:當前給線程加鎖的線程 tl:最后一個加鎖的線程
tn:隨便某個線程 當然這些線程有可能重復,比如第一次加鎖的時候tf=tc=tl=tn 節點:就是上面的Node類的對象,里面封裝了線程,所以某種意義上node就等於一個線程
首先一個簡單的應用:
final ReentrantLock lock = new ReentrantLock(true); Thread t1= new Thread("t1"){ @Override public void run() { lock.lock(); logic(); lock.unlock(); } }; t1.start();
公平鎖lock方法的源碼分析:
final void lock() { acquire(1);//1------標識加鎖成功之后改變的值 }
非公平鎖的looc方法的源碼分析:
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
下面給出他們的代碼執行邏輯的區別圖:
公平鎖的上鎖是必須判斷自己是不是需要排隊;
而非公平鎖是直接進行CAS修改計數器看能不能加鎖成功;如果加鎖不成功則乖乖排隊(調用acquire);
所以不管公平還是不公平;只要進到了AQS隊列當中那么他就會排隊;一朝排隊;永遠排隊記住這點
acquire方法方法源碼分析:
public final void acquire(int arg) { //tryAcquire(arg)嘗試加鎖,如果加鎖失敗則會調用acquireQueued方法加入隊列去排隊,如果加鎖成功則不會調用 //acquireQueued方法下文會有解釋 //加入隊列之后線程會立馬park,等到解鎖之后會被unpark,醒來之后判斷自己是否被打斷了;被打斷下次分析 //為什么需要執行這個方法?下文解釋 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
acquire方法首先會調用tryAcquire方法,注意tryAcquire的結果做了取反;
tryAcquire方法源碼分析:
protected final boolean tryAcquire(int acquires) { //獲取當前線程 final Thread current = Thread.currentThread(); //獲取lock對象的上鎖狀態,如果鎖是自由狀態則=0,如果被上鎖則為1,大於1表示重入 int c = getState(); if (c == 0) {//沒人占用鎖--->我要去上鎖----1、鎖是自由狀態 //hasQueuedPredecessors,判斷自己是否需要排隊這個方法比較復雜, //下面我會單獨介紹,如果不需要排隊則進行cas嘗試加鎖,如果加鎖成功則把當前線程設置為擁有鎖的線程 //繼而返回true if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //設置當前線程為擁有鎖的線程,方面后面判斷是不是重入(只需把這個線程拿出來判斷是否當前線程即可判斷重入) setExclusiveOwnerThread(current); return true; } } //如果C不等於0,而且當前線程不等於擁有鎖的線程則不會進else if 直接返回false,加鎖失敗 //如果C不等於0,但是當前線程等於擁有鎖的線程則表示這是一次重入,那么直接把狀態+1表示重入次數+1 //那么這里也側面說明了reentrantlock是可以重入的,因為如果是重入也返回true,也能lock成功 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判斷是否需要排隊的源碼分析:
這里需要記住一點,整個方法如果最后返回false,則去加鎖,如果返回true則不加鎖,因為這個方法被取反了
public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; /** * 下面提到的所有不需要排隊,並不是字面意義,我實在想不出什么詞語來描述這個“不需要排隊”;不需要排隊有兩種情況 * 一:隊列沒有初始化,不需要排隊,不需要排隊,不需要排隊;直接去加鎖,但是可能會失敗;為什么會失敗呢? * 假設兩個線程同時來lock,都看到隊列沒有初始化,都認為不需要排隊,都去進行CAS修改計數器;有一個必然失敗 * 比如t1先拿到鎖,那么另外一個t2則會CAS失敗,這個時候t2就會去初始化隊列,並排隊 * * 二:隊列被初始化了,但是tc過來加鎖,發覺隊列當中第一個排隊的就是自己;比如重入; * 那么什么叫做第一個排隊的呢?下面解釋了,很重要往下看; * 這個時候他也不需要排隊,不需要排隊,不需要排隊;為什么不需要排對? * 因為隊列當中第一個排隊的線程他會去嘗試獲取一下鎖,因為有可能這個時候持有鎖鎖的那個線程可能釋放了鎖; * 如果釋放了就直接獲取鎖執行。但是如果沒有釋放他就會去排隊, * 所以這里的不需要排隊,不是真的不需要排隊 * * h != t 判斷首不等於尾這里要分三種情況 * 1、隊列沒有初始化,也就是第一個線程tf來加鎖的時候那么這個時候隊列沒有初始化, * h和t都是null,那么這個時候判斷不等於則不成立(false)那么由於是&&運算后面的就不會走了, * 直接返回false表示不需要排隊,而前面又是取反(if (!hasQueuedPredecessors()),所以會直接去cas加鎖。 * ----------第一種情況總結:隊列沒有初始化沒人排隊,那么我直接不排隊,直接上鎖;合情合理、有理有據令人信服; * 好比你去火車站買票,服務員都閑的蛋疼,整個隊列都沒有形成;沒人排隊,你直接過去交錢拿票 * * 2、隊列被初始化了,后面會分析隊列初始化的流程,如果隊列被初始化那么h!=t則成立;(不絕對,還有第3中情況) * h != t 返回true;但是由於是&&運算,故而代碼還需要進行后續的判斷 * (有人可能會疑問,比如隊列初始化了;里面只有一個數據,那么頭和尾都是同一個怎么會成立呢? * 其實這是第3種情況--對頭等於對尾;但是這里先不考慮,我們假設現在隊列里面有大於1個數據) * 大於1個數據則成立;繼續判斷把h.next賦值給s;s有是對頭的下一個Node, * 這個時候s則表示他是隊列當中參與排隊的線程而且是排在最前面的; * 為什么是s最前面不是h嘛?誠然h是隊列里面的第一個,但是不是排隊的第一個;下文有詳細解釋 * 因為h也就是對頭對應的Node對象或者線程他是持有鎖的,但是不參與排隊; * 這個很好理解,比如你去買車票,你如果是第一個這個時候售票員已經在給你服務了,你不算排隊,你后面的才算排隊; * 隊列里面的h是不參與排隊的這點一定要明白;參考下面關於隊列初始化的解釋; * 因為h要么是虛擬出來的節點,要么是持有鎖的節點;什么時候是虛擬的呢?什么時候是持有鎖的節點呢?下文分析 * 然后判斷s是否等於空,其實就是判斷隊列里面是否只有一個數據; * 假設隊列大於1個,那么肯定不成立(s==null---->false),因為大於一個Node的時候h.next肯定不為空; * 由於是||運算如果返回false,還要判斷s.thread != Thread.currentThread();這里又分為兩種情況 * 2.1 s.thread != Thread.currentThread() 返回true,就是當前線程不等於在排隊的第一個線程s; * 那么這個時候整體結果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true 最后true) * 結果: true && true 方法最終放回true,所以需要去排隊 * 其實這樣符合情理,試想一下買火車票,隊列不為空,有人在排隊; * 而且第一個排隊的人和現在來參與競爭的人不是同一個,那么你就乖乖去排隊 * 2.2 s.thread != Thread.currentThread() 返回false 表示當前來參與競爭鎖的線程和第一個排隊的線程是同一個線程 * 這個時候整體結果就是h!=t---->true; (s==null false || s.thread != Thread.currentThread() false-----> 最后false) * 結果:true && false 方法最終放回false,所以不需要去排隊 * 不需要排隊則調用 compareAndSetState(0, acquires) 去改變計數器嘗試上鎖; * 這里又分為兩種情況(日了狗了這一行代碼;有同學課后反應說子路老師老師老是說這個AQS難, * 你現在仔細看看這一行代碼的意義,真的不簡單的) * 2.2.1 第一種情況加鎖成功?有人會問為什么會成功啊,如這個時候h也就是持有鎖的那個線程執行完了 * 釋放鎖了,那么肯定成功啊;成功則執行 setExclusiveOwnerThread(current); 然后返回true 自己看代碼 * 2.2.2 第二種情況加鎖失敗?有人會問為什么會失敗啊。假如這個時候h也就是持有鎖的那個線程沒執行完 * 沒釋放鎖,那么肯定失敗啊;失敗則直接返回false,不會進else if(else if是相對於 if (c == 0)的) * 那么如果失敗怎么辦呢?后面分析; * *----------第二種情況總結,如果隊列被初始化了,而且至少有一個人在排隊那么自己也去排隊;但是有個插曲; * ----------他會去看看那個第一個排隊的人是不是自己,如果是自己那么他就去嘗試假設;嘗試看看鎖有沒有釋放 *----------也合情合理,好比你去買票,如果有人排隊,那么你乖乖排隊,但是你會去看第一個排隊的人是不是你女朋友; *----------如果是你女朋友就相當於是你自己(這里實在想不出現實世界關於重入的例子,只能用男女朋友來替代); * --------- 你就叫你女朋友看看售票員有沒有搞完,有沒有輪到你女朋友,因為你女朋友是第一個排隊的 * 疑問:比如如果在在排隊,那么他是park狀態,如果是park狀態,自己怎么還可能重入啊。 * 希望有同學可以想出來為什么和我討論一下,作為一個菜逼,希望有人教教我 * * * 3、隊列被初始化了,但是里面只有一個數據;什么情況下才會出現這種情況呢?ts加鎖的時候里面就只有一個數據? * 其實不是,因為隊列初始化的時候會虛擬一個h作為頭結點,tc=ts作為第一個排隊的節點;tf為持有鎖的節點 * 為什么這么做呢?因為AQS認為h永遠是不排隊的,假設你不虛擬節點出來那么ts就是h, * 而ts其實需要排隊的,因為這個時候tf可能沒有執行完,還持有着鎖,ts得不到鎖,故而他需要排隊; * 那么為什么要虛擬為什么ts不直接排在tf之后呢,上面已經時說明白了,tf來上鎖的時候隊列都沒有,他不進隊列, * 故而ts無法排在tf之后,只能虛擬一個thread=null的節點出來(Node對象當中的thread為null); * 那么問題來了;究竟什么時候會出現隊列當中只有一個數據呢?假設原隊列里面有5個人在排隊,當前面4個都執行完了 * 輪到第五個線程得到鎖的時候;他會把自己設置成為頭部,而尾部又沒有,故而隊列當中只有一個h就是第五個 * 至於為什么需要把自己設置成頭部;其實已經解釋了,因為這個時候五個線程已經不排隊了,他拿到鎖了; * 所以他不參與排隊,故而需要設置成為h;即頭部;所以這個時間內,隊列當中只有一個節點 * 關於加鎖成功后把自己設置成為頭部的源碼,后面會解析到;繼續第三種情況的代碼分析 * 記得這個時候隊列已經初始化了,但是只有一個數據,並且這個數據所代表的線程是持有鎖 * h != t false 由於后面是&&運算,故而返回false可以不參與運算,整個方法返回false;不需要排隊 * * *-------------第三種情況總結:如果隊列當中只有一個節點,而這種情況我們分析了, *-------------這個節點就是當前持有鎖的那個節點,故而我不需要排隊,進行cas;嘗試加鎖 *-------------這是AQS的設計原理,他會判斷你入隊之后是不是第一個排隊的,如果是則先嘗試加鎖 *-------------如果加鎖失敗了,在去park,下文有詳細解釋這樣設計源碼和原因 *-------------如果持有鎖的線程釋放了鎖,那么我能成功上鎖 * **/ return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
到此我們已經解釋完了!tryAcquire(arg)方法,為了方便我再次貼一下代碼:
public final void acquire(int arg) { //tryAcquire(arg)嘗試加鎖,如果加鎖失敗則會調用acquireQueued方法加入隊列去排隊,如果加鎖成功則不會調用 //acquireQueued方法下文會有解釋 //加入隊列之后線程會立馬park,等到解鎖之后會被unpark,醒來之后判斷自己是否被打斷了 //為什么需要執行這個方法?下次解釋 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
acquireQueued(addWaiter(Node.exclusive),arg))方法解析:
如果代碼能執行到這里說tc需要排隊 需要排隊有兩種情況—換言之代碼能夠執行到這里有兩種情況: 1、tf持有了鎖,並沒有釋放,所以tc來加鎖的時候需要排隊,但這個時候—隊列並沒有初始化 2、tn(無所謂哪個線程,反正就是一個線程)持有了鎖,那么由於加鎖tn!=tf(tf是屬於第一種情況,我們現在不考慮tf了),所以隊列是一定被初始化了的,tc來加鎖,那么隊列當中有人在排隊,故而他也去排隊
addWaiter(Node.EXCLUSIVE)源碼分析:
private Node addWaiter(Node mode) { //由於AQS隊列當中的元素類型為Node,故而需要把當前線程tc封裝成為一個Node對象,下文我們叫做nc Node node = new Node(Thread.currentThread(), mode); //tail為對尾,賦值給pred Node pred = tail; //判斷pred是否為空,其實就是判斷對尾是否有節點,其實只要隊列被初始化了對尾肯定不為空, //假設隊列里面只有一個元素,那么對尾和對首都是這個元素 //換言之就是判斷隊列有沒有初始化 //上面我們說過代碼執行到這里有兩種情況,1、隊列沒有初始化和2、隊列已經初始化了 //pred不等於空表示第二種情況,隊列被初始化了,如果是第二種情況那比較簡單 //直接把當前線程封裝的nc的上一個節點設置成為pred即原來的對尾 //繼而把pred的下一個節點設置為當nc,這個nc自己成為對尾了 if (pred != null) { //直接把當前線程封裝的nc的上一個節點設置成為pred即原來的對尾,對應 10行的注釋 node.prev = pred; //這里需要cas,因為防止多個線程加鎖,確保nc入隊的時候是原子操作 if (compareAndSetTail(pred, node)) { //繼而把pred的下一個節點設置為當nc,這個nc自己成為對尾了 對應第11行注釋 pred.next = node; //然后把nc返回出去,方法結束 return node; } } //如果上面的if不成了就會執行到這里,表示第一種情況隊列並沒有初始化---下面解析這個方法 enq(node); //返回nc return node; } private Node enq(final Node node) {//這里的node就是當前線程封裝的node也就是nc //死循環 for (;;) { //對尾復制給t,上面已經說過隊列沒有初始化, //故而第一次循環t==null(因為是死循環,因此強調第一次,后面可能還有第二次、第三次,每次t的情況肯定不同) Node t = tail; //第一次循環成了成立 if (t == null) { // Must initialize //new Node就是實例化一個Node對象下文我們稱為nn, //調用無參構造方法實例化出來的Node里面三個屬性都為null,可以關聯Node類的結構, //compareAndSetHead入隊操作;把這個nn設置成為隊列當中的頭部,cas防止多線程、確保原子操作; //記住這個時候隊列當中只有一個,即nn if (compareAndSetHead(new Node())) //這個時候AQS隊列當中只有一個元素,即頭部=nn,所以為了確保隊列的完整,設置頭部等於尾部,即nn即是頭也是尾 //然后第一次循環結束;接着執行第二次循環,第二次循環代碼我寫在了下面,接着往下看就行 tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } //為了方便 第二次循環我再貼一次代碼來對第二遍循環解釋 private Node enq(final Node node) {//這里的node就是當前線程封裝的node也就是nc //死循環 for (;;) { //對尾復制給t,由於第二次循環,故而tail==nn,即new出來的那個node Node t = tail; //第二次循環不成立 if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { //不成立故而進入else //首先把nc,當前線程所代表的的node的上一個節點改變為nn,因為這個時候nc需要入隊,入隊的時候需要把關系維護好 //所謂的維護關系就是形成鏈表,nc的上一個節點只能為nn,這個很好理解 node.prev = t; //入隊操作--把nc設置為對尾,對首是nn, if (compareAndSetTail(t, node)) { //上面我們說了為了維護關系把nc的上一個節點設置為nn //這里同樣為了維護關系,把nn的下一個節點設置為nc t.next = node; //然后返回t,即nn,死循環結束,enq(node);方法返回 //這個返回其實就是為了終止循環,返回出去的t,沒有意義 return t; } } } } //這個方法已經解釋完成了 enq(node); //返回nc,不管哪種情況都會返回nc;到此addWaiter方法解釋完成 return node; //再次貼出node的結構方便大家查看 public class Node{ volatile Node prev; volatile Node next; volatile Thread thread; } -------------------總結:addWaiter方法就是讓nc入隊-並且維護隊列的鏈表關系,但是由於情況復雜做了不同處理 -------------------主要針對隊列是否有初始化,沒有初始化則new一個新的Node nn作為對首,nn里面的線程為null -------------------接下來分析acquireQueued方法
acquireQueued(addWaiter(Node.exclusive),arg))經過上面的解析之后可以理解成為acquireQueued(nc,arg))
acquireQueued方法的源碼分析:
final boolean acquireQueued(final Node node, int arg) {//這里的node 就是當前線程封裝的那個node 下文叫做nc //記住標志很重要 boolean failed = true; try { //同樣是一個標志 boolean interrupted = false; //死循環 for (;;) { //獲取nc的上一個節點,有兩種情況;1、上一個節點為頭部;2上一個節點不為頭部 final Node p = node.predecessor(); //如果nc的上一個節點為頭部,則表示nc為隊列當中的第二個元素,為隊列當中的第一個排隊的Node; //這里的第一和第二不沖突;我上文有解釋; //如果nc為隊列當中的第二個元素,第一個排隊的則調用tryAcquire去嘗試加鎖---關於tryAcquire看上面的分析 //只有nc為第二個元素;第一個排隊的情況下才會嘗試加鎖,其他情況直接去park了, //因為第一個排隊的執行到這里的時候需要看看持有有鎖的線程有沒有釋放鎖,釋放了就輪到我了,就不park了 //有人會疑惑說開始調用tryAcquire加鎖失敗了(需要排隊),這里為什么還要進行tryAcquire不是重復了嗎? //其實不然,因為第一次tryAcquire判斷是否需要排隊,如果需要排隊,那么我就入隊; //當我入隊之后我發覺前面那個人就是第一個,持有鎖的那個,那么我不死心,再次問問前面那個人搞完沒有 //如果他搞完了,我就不park,接着他搞我自己的事;如果他沒有搞完,那么我則在隊列當中去park,等待別人叫我 //但是如果我去排隊,發覺前面那個人在睡覺,前面那個人都在睡覺,那么我也睡覺把---------------好好理解一下 if (p == head && tryAcquire(arg)) { //能夠執行到這里表示我來加鎖的時候,鎖被持有了,我去排隊,進到隊列當中的時候發覺我前面那個人沒有park, //前面那個人就是當前持有鎖的那個人,那么我問問他搞完沒有 //能夠進到這個里面就表示前面那個人搞完了;所以這里能執行到的幾率比較小;但是在高並發的世界中這種情況真的需要考慮 //如果我前面那個人搞完了,我nc得到鎖了,那么前面那個人直接出隊列,我自己則是對首;這行代碼就是設置自己為對首 setHead(node); //這里的P代表的就是剛剛搞完事的那個人,由於他的事情搞完了,要出隊;怎么出隊?把鏈表關系刪除 p.next = null; // help GC //設置表示---記住記加鎖成功的時候為false failed = false; //返回false;為什么返回false?下次博客解釋---比較復雜和加鎖無關 return interrupted; } //進到這里分為兩種情況 //1、nc的上一個節點不是頭部,說白了,就是我去排隊了,但是我上一個人不是隊列第一個 //2、第二種情況,我去排隊了,發覺上一個節點是第一個,但是他還在搞事沒有釋放鎖 //不管哪種情況這個時候我都需要park,park之前我需要把上一個節點的狀態改成park狀態 //這里比較難以理解為什么我需要去改變上一個節點的park狀態呢?每個node都有一個狀態,默認為0,表示無狀態 //-1表示在park;當時不能自己把自己改成-1狀態?為什么呢?因為你得確定你自己park了才是能改為-1; //不然你自己改成自己為-1;但是改完之后你沒有park那不就騙人? //你對外宣布自己是單身狀態,但是實際和劉宏斌私下約會;這有點坑人 //所以只能先park;在改狀態;但是問題你自己都park了;完全釋放CPU資源了,故而沒有辦法執行任何代碼了, //所以只能別人來改;故而可以看到每次都是自己的后一個節點把自己改成-1狀態 //關於shouldParkAfterFailedAcquire這個方法的源碼下次博客繼續講吧 if (shouldParkAfterFailedAcquire(p, node) && //改上一個節點的狀態成功之后;自己park;到此加鎖過程說完了 parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
加鎖過程總結:
如果是第一個線程tf,那么和隊列無關,線程直接持有鎖。並且也不會初始化隊列,如果接下來的線程都是交替執行,那么永遠和AQS隊列無關,都是直接線程持有鎖,
如果發生了競爭,比如tf持有鎖的過程中T2來lock,那么這個時候就會初始化AQS,
初始化AQS的時候會在隊列的頭部虛擬一個Thread為NULL的Node,因為隊列當中的head永遠是持有鎖的那個node(除了第一次會虛擬一個,其他時候都是持有鎖的那個線程鎖封裝的node),
現在第一次的時候持有鎖的是tf而tf不在隊列當中所以虛擬了一個node節點,隊列當中的除了head之外的所有的node都在park,
當tf釋放鎖之后unpark某個(基本是隊列當中的第二個,為什么是第二個呢?前面說過head永遠是持有鎖的那個node,當有時候也不會是第二個,比如第二個被cancel之后,至於為什么會被cancel,不在我們討論范圍之內,cancel的條件很苛刻,基本不會發生)node之后,node被喚醒,
假設node是t2,那么這個時候會首先把t2變成head(sethead),在sethead方法里面會把t2代表的node設置為head,並且把node的Thread設置為null,
為什么需要設置null?其實原因很簡單,現在t2已經拿到鎖了,node就不要排隊了,那么node對Thread的引用就沒有意義了。所以隊列的head里面的Thread永遠為null
文章轉載自:https://blog.csdn.net/java_lyvee/article/details/98966684