高效並發JUC鎖-永恒磚石
JUC包的鎖(可重入鎖和讀寫鎖)
Lock是JAVA5增加的內容,在JUC(java.util.concurrent.locks)包下面,作者是並發大師Doug Lea。JUC包提供了很多封裝的鎖,包括常用的ReentrantLock和ReadWriteLock。這些所其實都是依賴java.util.concurrent.AbstractQueuedSynchronizer(AQS)這個類來實現的,這個類有個簡寫的名字叫AQS,對這就是著名的AQS。
關於Lock,先說說線程獲取Lock鎖的時候會引起哪些事件呢。首先AQS是依賴一個被volatile修飾的int變量來標識當前鎖的狀態的,為0的時候代表當前鎖不被任何線程擁有,當線程拿到這個鎖的時候會通過CAS操作修改state的狀態,那麽對於爭用失敗的線程AQS會怎麽辦呢,AQS內部維護了一個等待隊列,這個隊列是純JAVA實現的,其實現也是非常巧妙的,多線程在通過CAS來獲取自己在隊列中的位置,同時隊列中的線程狀態也是阻塞狀態,遇到阻塞就頭疼了,上面已經介紹過阻塞會帶來的性能問題。在源碼中我們可以看到的是AQS通過LockSupport(LockSupport底層依賴Unsafe)將線程阻塞,關於LockSupport其功能是用來代替wait和notity/notifyall的,更好的地方是LockSupport對park方法和unpark方法的調用沒有先後的限制,而notify/notifyall必須在wait調用之後調用。盡管如此,這一切並沒有阻止線程進入阻塞狀態。
鎖原理
可重入鎖就是當前持有該鎖的線程能夠多次獲取該鎖,無需等待。基於AQS實現,AQS是JDK1.5提供的一個基於FIFO等待隊列實現的一個用於實現同步器的基礎框架,這個基礎框架的重要性可以這麽說,JUC包裏面幾乎所有的有關鎖、多線程並發以及線程同步器等重要組件的實現都是基於AQS這個框架。AQS的核心思想是基於volatile int state這樣的一個屬性同時配合Unsafe工具對其原子性的操作來實現對當前鎖的狀態進行修改。當state的值為0的時候,標識改Lock不被任何線程所占有。
作為AQS的核心實現的一部分,舉個例子,我們假設目前有三個線程Thread1、Thread2、Thread3同時去競爭鎖,如果結果是Thread1獲取了鎖,Thread2和Thread3進入了等待隊列,AQS的等待隊列基於一個雙向鏈表實現的,HEAD節點不關聯線程,後面兩個節點分別關聯Thread2和Thread3,他們將會按照先後順序被串聯在這個隊列上。這個時候如果後面再有線程進來的話將會被當做隊列的TAIL。當三個線程同時進來,他們會首先會通過CAS去修改state的狀態,如果修改成功,那麽競爭成功,因此這個時候三個線程只有一個CAS成功,其他兩個線程失敗,也就是tryAcquire返回false。接下來,addWaiter會把將當前線程關聯的EXCLUSIVE類型的節點入隊列如果隊尾節點不為null,則說明隊列中已經有線程在等待了,那麽直接入隊尾。如果Thread2和Thread3同時進入了enq,同時t==null,則進行CAS操作對隊列進行初始化,這個時候只有一個線程能夠成功,然後他們繼續進入循環,第二次都進入了else代碼塊,這個時候又要進行CAS操作,將自己放在隊尾,因此這個時候又是只有一個線程成功,我們假設是Thread2成功,哈哈,Thread2開心的返回了,Thread3失落的再進行下一次的循環,最終入隊列成功,返回自己。當有多個線程,或者說很多很多的線程同時執行的時候,怎麽能保證最終他們都能夠乖乖的入隊列而不會出現並發問題的呢?這也是這部分代碼的經典之處,多線程競爭,熱點、單點在隊列尾部,多個線程都通過【CAS+死循環】這個free-lock黃金搭檔來對隊列進行修改,每次能夠保證只有一個成功,如果失敗下次重試,如果是N個線程,那麽每個線程最多loop N次,最終都能夠成功。上面只是addWaiter的實現部分,那麽節點入隊列之後會繼續發生什麽呢,如果Thread1死死的握住鎖不放,那麽Thread2和Thread3現在的狀態就是掛起狀態啦,而且HEAD,以及Thread的waitStatus都是SIGNAL,盡管他們在整個過程中曾經數次去嘗試獲取鎖,但是都失敗了,失敗了不能死循環呀,所以就被掛起了。
鎖釋放-等待線程喚起首先,Thread1會修改AQS的state狀態,加入之前是1,則變為0,註意這個時候對於非公平鎖來說是個很好的插入機會,舉個例子,如果鎖是公平鎖,這個時候來了Thread4,那麽這個鎖將會被Thread4搶去。
我們繼續走常規路線來分析,當Thread1修改完狀態了,判斷隊列是否為null,以及隊頭的waitStatus是否為0,如果waitStatus為0,說明隊列無等待線程,按照我們的例子來說,隊頭的waitStatus為SIGNAL=-1,因此這個時候要通知隊列的等待線程,可以來拿鎖啦,這也是unparkSuccessor做的事情,unparkSuccessor主要做三件事情:
將隊頭的waitStatus設置為0.
通過從隊列尾部向隊列頭部移動,找到最後一個waitStatus<=0的那個節點,也就是離隊頭最近的沒有被cancelled的那個節點,隊頭這個時候指向這個節點。
將這個節點喚醒,其實這個時候Thread1已經出隊列了。
還記得線程在哪裏掛起的麽,上面說過了,在acquireQueued裏面,我沒有貼代碼,自己去看哦。這裏我們也大概能理解AQS的這個隊列為什麽叫FIFO隊列了,因此每次喚醒僅僅喚醒隊頭等待線程,讓隊頭等待線程先出。當有多個線程去競爭同一個鎖的時候,假設鎖被某個線程占用,那麽如果有成千上萬個線程在等待鎖,有一種做法是同時喚醒這成千上萬個線程去去競爭鎖,這個時候就發生了羊群效應,海量的競爭必然造成資源的劇增和浪費,因此終究只能有一個線程競爭成功,其他線程還是要老老實實的回去等待。AQS的FIFO的等待隊列給解決在鎖競爭方面的羊群效應問題提供了一個思路:保持一個FIFO隊列,隊列每個節點只關心其前一個節點的狀態,線程喚醒也只喚醒隊頭等待線程。
Lock是一個接口,定義了鎖的基本方法:
public interface Lock { void lock();//加鎖 void lockInterruptibly() throws InterruptedException;//加可中斷鎖 boolean tryLock();//加鎖成功返回true,失敗返回false boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加鎖成功返回true,失敗等待一段時間後若仍無法加鎖則返回false,可響應中斷 void unlock();//解鎖 Condition newCondition(); //返回一個Condition,利用它可以如和 Synchronizd配合使用的wait()和notify()一樣對線程阻塞和喚醒,不同的是一個lock可以有多個condition }
最後的newCondition返回一個Condition對象,該對象是一個接口,我們來看看其中提供的方法。
public interface Condition { void await() throws InterruptedException;//類似於wait(),可以響應中斷 void awaitUninterruptibly();//不響應中斷的等待 long awaitNanos(long nanosTimeout) throws InterruptedException;//等待指定時間(單位是納秒),在接到信號、被中斷或到達指定等待時間之前一直處於等待狀態。方法返回被喚醒後的剩余等待時間,若返回值小於等於0則代表此次等待超時。 boolean await(long time, TimeUnit unit) throws InterruptedException;//指定時間到達前結束等待返回true,否則返回false boolean awaitUntil(Date deadline) throws InterruptedException;//指定日期到達前被喚醒返回true,否則返回false void signal();//喚醒一個等待中的線程,類似於notify() void signalAll();//喚醒所有等待中的線程,類似於notifyAll() }
可重入鎖是Lock接口的一個重要實現類。所謂可重入鎖即線程在執行某個方法時已經持有了這個鎖,那麽線程在執行另一個方法時也持有該鎖。
ReadWriteLock是一個接口,也是定義鎖的基本方法
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
ReentrantReadWriteLock和ReentrantLock不同,ReentrantReadWriteLock實現的是ReadWriteLock接口
讀寫鎖是接口ReadWriteLock的一個重要實現類。加讀鎖時其他線程可以進行讀操作但不可進行寫操作,加寫鎖時其他線程讀寫操作都不可進行。
待續。。
高效並發JUC鎖-永恒磚石