中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java并發中AQS原理的示例分析

發布時間:2021-11-20 14:20:17 來源:億速云 閱讀:153 作者:小新 欄目:編程語言

這篇文章給大家分享的是有關Java并發中AQS原理的示例分析的內容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

1、線程阻塞原語

Java 的線程阻塞和喚醒是通過 Unsafe 類的 park 和 unpark 方法做到的。

這兩個方法都是 native 方法,它們本身是由 C 語言來實現的核心功能。park 的意思是停車,讓當前運行的線程 Thread.currentThread() 休眠,unpark 的意思是解除停車,喚醒指定線程。

這兩個方法在底層是使用操作系統提供的信號量機制來實現的。具體實現過程要深究 C 代碼,這里暫時不去具體分析。park 方法的兩個參數用來控制休眠多長時間,第一個參數 isAbsolute 表示第二個參數是絕對時間還是相對時間,單位是毫秒。

線程從啟動開始就會一直跑,除了操作系統的任務調度策略外,它只有在調用 park 的時候才會暫停運行。鎖可以暫停線程的奧秘所在正是因為鎖在底層調用了 park 方法。

2、parkBlocker

線程對象 Thread 里面有一個重要的屬性 parkBlocker,它保存當前線程因為什么而 park。就好比停車場上停了很多車,這些車主都是來參加一場拍賣會的,等拍下自己想要的物品后,就把車開走。那么這里的 parkBlocker 大約就是指這場「拍賣會」。它是一系列沖突線程的管理者協調者,哪個線程該休眠該喚醒都是由它來控制的。

當線程被 unpark 喚醒后,這個屬性會被置為 null。Unsafe.park 和 unpark 并不會幫我們設置 parkBlocker 屬性,負責管理這個屬性的工具類是 LockSupport,它對 Unsafe 這兩個方法進行了簡單的包裝。

Java 的鎖數據結構正是通過調用 LockSupport 來實現休眠與喚醒的。線程對象里面的 parkBlocker 字段的值就是下面我們要講的「排隊管理器」。

3、排隊管理器

當多個線程爭用同一把鎖時,必須有排隊機制將那些沒能拿到鎖的線程串在一起。當鎖釋放時,鎖管理器就會挑選一個合適的線程來占有這個剛剛釋放的鎖。

每一把鎖內部都會有這樣一個隊列管理器,管理器里面會維護一個等待的線程隊列。ReentrantLock 里面的隊列管理器是 AbstractQueuedSynchronizer,它內部的等待隊列是一個雙向列表結構。

加鎖不成功時,當前的線程就會把自己納入到等待鏈表的尾部,然后調用 LockSupport.park 將自己休眠。其它線程解鎖時,會從鏈表的表頭取一個節點,調用 LockSupport.unpark 喚醒它。

AbstractQueuedSynchronizer 類是一個抽象類,它是所有的鎖隊列管理器的父類,JDK 中的各種形式的鎖其內部的隊列管理器都繼承了這個類,它是 Java 并發世界的核心基石。

比如 ReentrantLock、ReadWriteLock、CountDownLatch、Semaphore、ThreadPoolExecutor 內部的隊列管理器都是它的子類。這個抽象類暴露了一些抽象方法,每一種鎖都需要對這個管理器進行定制。而 JDK 內置的所有并發數據結構都是在這些鎖的保護下完成的,它是JDK 多線程高樓大廈的地基。

鎖管理器維護的只是一個普通的雙向列表形式的隊列,這個數據結構很簡單,但是仔細維護起來卻相當復雜,因為它需要精細考慮多線程并發問題,每一行代碼都寫的無比小心。

JDK 鎖管理器的實現者是 Douglas S. Lea,Java 并發包幾乎全是他單槍匹馬寫出來的,在算法的世界里越是精巧的東西越是適合一個人來做。

后面我們將 AbstractQueuedSynchronizer 簡寫成 AQS。我必須提醒各位讀者,AQS 太復雜了,如果在理解它的路上遇到了挫折,這很正常。目前市場上并不存在一本可以輕松理解 AQS 的書籍,能夠吃透 AQS 的人太少太少,我自己也不算。

4、公平鎖與非公平鎖

公平鎖會確保請求鎖和獲得鎖的順序,如果在某個點鎖正處于自由狀態,這時有一個線程要嘗試加鎖,公平鎖還必須查看當前有沒有其它線程排在排隊,而非公平鎖可以直接插隊。聯想一下在肯德基買漢堡時的排隊場景。

也許你會問,如果某個鎖處于自由狀態,那它怎么會有排隊的線程呢?我們假設此刻持有鎖的線程剛剛釋放了鎖,它喚醒了等待隊列中第一個節點線程,這時候被喚醒的線程剛剛從 park 方法返回,接下來它就會嘗試去加鎖,那么從 park 返回到加鎖之間的狀態就是鎖的自由態,這很短暫,而這短暫的時間內還可能有其它線程也在嘗試加鎖。

其次還有一點需要注意,執行了 Lock.park 方法的線程自我休眠后,并不是非要等到其它線程 unpark 了自己才會醒來,它可能隨時會以某種未知的原因醒來。我們看源碼注釋,park 返回的原因有四種:

①其它線程 unpark 了當前線程;

②時間到了自然醒(park 有時間參數);

③其它線程 interrupt 了當前線程;

④其它未知原因導致的「假醒」;

文檔中沒有明確說明何種未知原因會導致假醒,它倒是說明了當 park 方法返回時并不意味著鎖自由了,醒過來的線程在重新嘗試獲取鎖失敗后將會再次 park 自己。所以加鎖的過程需要寫在一個循環里,在成功拿到鎖之前可能會進行多次嘗試。

計算機世界非公平鎖的服務效率要高于公平鎖,所以 Java 默認的鎖都使用了非公平鎖。不過現實世界似乎非公平鎖的效率會差一點,比如在肯德基如果可以不停插隊,你可以想象現場肯定一片混亂。為什么計算機世界和現實世界會有差異,大概是因為在計算機世界里某個線程插隊并不會導致其它線程抱怨。

5、共享鎖與排他鎖

ReentrantLock 的鎖是排他鎖,一個線程持有,其它線程都必須等待。而 ReadWriteLock 里面的讀鎖不是排他鎖,它允許多線程同時持有讀鎖,這是共享鎖。共享鎖和排他鎖是通過 Node 類里面的 nextWaiter 字段區分的。

那為什么這個字段沒有命名成 mode 或者 type 或者干脆直接叫 shared?這是因為 nextWaiter 在其它場景還有不一樣的用途,它就像 C 語言聯合類型的字段一樣隨機應變,只不過 Java 語言沒有聯合類型。

6、條件變量

關于條件變量,需要提出的第一個問題是為什么需要條件變量,只有鎖還不夠么?考慮下面的偽代碼,當某個條件滿足時,才去干某件事

當條件不滿足時,就循環重試(其它線程會通過加鎖來修改條件),但是需要間隔 sleep,不然 CPU 就會因為空轉而飆高。這里存在一個問題,那就是 sleep 多久不好控制。間隔太久,會拖慢整體效率,甚至會錯過時機(條件瞬間滿足了又立即被重置了),間隔太短,又回導致 CPU 空轉。有了條件變量,這個問題就可以解決了

await() 方法會一直阻塞在 cond 條件變量上直到被另外一個線程調用了 cond.signal() 或者 cond.signalAll() 方法后才會返回,await() 阻塞時會自動釋放當前線程持有的鎖,await() 被喚醒后會再次嘗試持有鎖(可能又需要排隊),拿到鎖成功之后 await() 方法才能成功返回。

阻塞在條件變量上的線程可以有多個,這些阻塞線程會被串聯成一個條件等待隊列。當 signalAll() 被調用時,會喚醒所有的阻塞線程,讓所有的阻塞線程重新開始爭搶鎖。如果調用的是 signal() 只會喚醒隊列頭部的線程,這樣可以避免「驚群問題」。

await() 方法必須立即釋放鎖,否則臨界區狀態就不能被其它線程修改,condition_is_true() 返回的結果也就不會改變。 這也是為什么條件變量必須由鎖對象來創建,條件變量需要持有鎖對象的引用這樣才可以釋放鎖以及被 signal 喚醒后重新加鎖。

創建條件變量的鎖必須是排他鎖,如果是共享鎖被 await() 方法釋放了并不能保證臨界區的狀態可以被其它線程來修改,可以修改臨界區狀態的只能是排他鎖。

有了條件變量,sleep 不好控制的問題就解決了。當條件滿足時,調用 signal() 或者 signalAll() 方法,阻塞的線程可以立即被喚醒,幾乎沒有任何延遲。

7、條件等待隊列

當多個線程 await() 在同一個條件變量上時,會形成一個條件等待隊列。同一個鎖可以創建多個條件變量,就會存在多個條件等待隊列。這個隊列和 AQS 的隊列結構很接近,只不過它不是雙向隊列,而是單向隊列。隊列中的節點和 AQS 等待隊列的節點是同一個類,但是節點指針不是 prev 和 next,而是 nextWaiter。

ConditionObject 是 AQS 的內部類,這個對象里會有一個隱藏的指針 this$0 指向外部的 AQS 對象,ConditionObject 可以直接訪問 AQS 對象的所有屬性和方法(加鎖解鎖)。位于條件等待隊列里的所有節點的 waitStatus 狀態都被標記為 CONDITION,表示節點是因為條件變量而等待。

8、隊列轉移

當條件變量的 signal() 方法被調用時,條件等待隊列的頭節點線程會被喚醒,該節點從條件等待隊列中被摘走,然后被轉移到 AQS 的等待隊列中,準備排隊嘗試重新獲取鎖。這時節點的狀態從 CONDITION 轉為 SIGNAL,表示當前節點是被條件變量喚醒轉移過來的。

被轉移的節點的 nextWaiter 字段的含義也發生了變更,在條件隊列里它是下一個節點的指針,在 AQS 等待隊列里它是共享鎖還是互斥鎖的標志。

9、讀寫鎖

讀寫鎖分為兩個鎖對象 ReadLock 和 WriteLock,這兩個鎖對象共享同一個 AQS。AQS 的鎖計數變量 state 將分為兩個部分,前 16bit 為共享鎖 ReadLock 計數,后 16bit 為互斥鎖 WriteLock 計數。互斥鎖記錄的是當前寫鎖重入的次數,共享鎖記錄的是所有當前持有共享讀鎖的線程重入總次數。

讀寫鎖同樣也需要考慮公平鎖和非公平鎖。共享鎖和互斥鎖的公平鎖策略和 ReentrantLock 一樣,就是看看當前還有沒有其它線程在排隊,自己會乖乖排到隊尾。非公平鎖策略不一樣,它會比較偏向于給寫鎖提供更多的機會。

如果當前 AQS 隊列里有任何讀寫請求的線程在排隊,那么寫鎖可以直接去爭搶,但是如果隊頭是寫鎖請求,那么讀鎖需要將機會讓給寫鎖,去隊尾排隊。 畢竟讀寫鎖適合讀多寫少的場合,對于偶爾出現一個寫鎖請求就應該得到更高的優先級去處理。

感謝各位的閱讀!關于“Java并發中AQS原理的示例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,讓大家可以學到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

乌拉特中旗| 武隆县| 彭泽县| 浠水县| 沾化县| 措美县| 石棉县| 灯塔市| 南雄市| 龙陵县| 河西区| 兰州市| 铁岭县| 南阳市| 时尚| 衡阳市| 绍兴市| 南召县| 延庆县| 陈巴尔虎旗| 永州市| 延吉市| 中方县| 旌德县| 金乡县| 凌云县| 淮滨县| 逊克县| 左云县| 潼南县| 莒南县| 古交市| 萨嘎县| 哈巴河县| 团风县| 古田县| 达尔| 永登县| 宣城市| 嘉兴市| 蒙自县|