深刻理解Java併發框架AQS系列(一):線程
深刻理解Java併發框架AQS系列(二):AQS框架簡介及鎖概念html
AQS誕生於Jdk1.5,在當時低效且功能單一的
synchronized
的年代,某種意義上講,她拯救了Javajava
注:本系列文章全部測試用例均基於jdk1.8,操做系統爲macOS架構
咱們去學習一個知識點或開啓一個新課題時,最好是帶着問題去學習,這樣針對性比較強,且印象比較深入,主動思考帶給咱們帶來了無窮的好處併發
拋開AQS,設想如下問題:框架
咱們發現,一個簡單的等待資源的問題,牽扯出後續諸多龐雜且無頭緒的問題;加鎖不只依賴一套完善的框架體系,還要具體根據使用場景而定,才能接近最優解;那咱們即將要引出的AQS能完美解決上述這些問題嗎?工具
答案是確定的:不能性能
其實Doug Lea也意識到問題的複雜性,不可能出一個超級工具來解決全部問題,因此他把AQS設計爲一個abstract類,並提供一系列子類去解決不一樣場景的問題,例如ReentrantLock
、Semaphore
等;當咱們發現這些子類也不能知足咱們加鎖需求時,咱們能夠定義本身的子類,經過重寫兩三個方法,寥寥幾行代碼,實現強大的功能,這一切都得益於AQS做者敏銳的前瞻性學習
指的一提的是,雖然咱們能夠用某個子類去實現另外一個子類所提供的功能(例如使用Semaphore
替代CountDownLatch
),但其易用、簡潔、高效性等可否達到理想效果,都值得商榷;就比如在陸地上穿着雪橇走路,雖能前進,卻低效易摔跤測試
本小節僅帶你們對AQS架構有個初步瞭解,在後文的獨佔鎖、共享鎖等中會詳細闡述。下圖爲AQS框架的主體結構
優化
從上圖中咱們看到了AQS中很是關鍵的一個概念:「阻塞隊列」。即AQS的理念是當線程沒法獲取資源時,提供一個FIFO類型的有序隊列,用來維護全部處於「等待中」的線程。看似無解可擊的框架設計,同時也牽出另外的一個問題:阻塞隊列必定高效嗎?
當「同步塊邏輯」執行很快時,咱們列出兩種場景
ReentrantLock
,遇到資源爭搶,放阻塞隊列針對這2種場景,咱們寫測試用例比較一下
package org.xijiu.share.aqs.compare; import org.junit.Test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.ReentrantLock; /** * @author likangning * @since 2021/3/9 上午8:58 */ public class CompareTest { private class MyReentrantLock extends AbstractQueuedSynchronizer { protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); while (true) { int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } } } 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; } } /** * 使用AQS框架 */ @Test public void test1() throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 2; i++) { executorService.submit(() -> { for (int j = 0; j < 50000000; j++) { reentrantLock.lock(); doBusiness(); reentrantLock.unlock(); } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("ReentrantLock cost : " + (System.currentTimeMillis() - begin)); } /** * 無限重試 */ @Test public void test2() throws InterruptedException { MyReentrantLock myReentrantLock = new MyReentrantLock(); long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 2; i++) { executorService.submit(() -> { for (int j = 0; j < 50000000; j++) { myReentrantLock.tryAcquire(1); doBusiness(); myReentrantLock.tryRelease(1); } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("MyReentrantLock cost : " + (System.currentTimeMillis() - begin)); } private void doBusiness() { // 空實現,模擬程序快速運行 } }
上例,雖然MyReentrantLock
繼承了AbstractQueuedSynchronizer
,但沒有使用其阻塞隊列。咱們每種狀況跑5次,看下二者在耗時層面的表現
類 | 耗時1 | 耗時2 | 耗時3 | 耗時4 | 耗時5 | 平均耗時(ms) |
---|---|---|---|---|---|---|
ReentrantLock |
11425 | 12301 | 12289 | 10262 | 11461 | 11548 |
MyReentrantLock |
8717 | 8957 | 10283 | 8445 | 8928 | 9066 |
上例只是拿獨佔鎖舉例,共享鎖也同理。能夠簡單歸納爲:線程掛起、喚醒的時間佔整個加鎖週期比重較大,致使每次掛起、喚醒已經成爲一種負擔。固然此處並非說AQS設計有什麼缺陷,只是想表達並無一種萬能的框架能應對全部狀況,一切都要靠使用者靈活理解、應用
咱們經常使用的鎖併發類,基本上都是AQS的子類或經過組合方式實現,可見AQS在Java併發體系的重要性
至於如何使用,是須要區分子類是想實現獨佔鎖仍是共享鎖
獨佔鎖
tryAcquire()
tryRelease()
isHeldExclusively()
-- 可不實現共享鎖
tryAcquireShared()
tryReleaseShared()
AQS自己是一個abstract
類,將主要併發邏輯進行了封裝,咱們定義本身的併發控制類,僅須要實現其中的兩三個方法便可。而在對外(public
方法)表現形式上,可依據本身的業務特性來定義;例如Semaphore
定義爲acquire
、release
,而ReentrantLock
定義爲lock
、unlock
相信你們常常會被各類各樣鎖的定義搞亂,叫法兒也五花八門,爲了後續行文的方便,此章咱們把一些鎖概念闡述一下
獨佔鎖,顧名思義,即在同一時刻,僅容許一個線程執行同步塊代碼。比如一夥兒人想要過河,但只有一根獨木橋,且只能承受一人的重量
JDK支持的典型獨佔鎖:ReentrantLock
、ReentrantReadWriteLock
共享鎖實際上是相對獨佔鎖而言的,涉及到共享鎖就要聊到併發度,即同一時刻最多容許同時執行線程的數量。上圖所述的併發度爲3,即在同一時刻,最多可有3我的在同時過河。
但共享鎖的併發度也能夠設置爲1,此時它能夠看做是獨佔鎖
JDK支持的典型獨佔鎖:Semaphore
、CountDownLatch
雖然叫作公平鎖,但咱們知道任何事情都是相對的,此處也不例外,咱們也只能作到相對公平,後文會涉及,此處再也不贅述
線程在進入時,首先要檢查阻塞隊列中是否爲空,若是發現已有線程在排隊,那麼主動添加至隊尾並等待被逐一喚起;若是發現阻塞隊列爲空,纔會嘗試去獲取資源。公平鎖相對非公平鎖效率較低,一般來說,加鎖時間越短,表現越明顯
任何一個剛進入的線程,都會嘗試去獲取資源,釋放資源後,還會通知頭節點去嘗試獲取資源,這樣可能致使飢餓發生,即某一個阻塞隊列中的線程一直得不到調度。
那爲何咱們會說,非公平鎖的效率要高於公平鎖呢?假設一個獨佔鎖,阻塞隊列中已經有10個線程在排隊,線程A搶到資源並執行完畢後,去喚醒頭結點head,head線程喚醒須要時間,head喚醒後才嘗試去獲取資源,而在整個過程當中,沒有線程在執行加鎖代碼
由於線程喚起須要引起用戶態及內核態的切換,故是一個相對比較耗時的操做。
咱們再舉一個不恰當的例子:行政部在操場上爲同窗們辦理業務,由於天氣炎熱,故讓排隊的同窗在場邊一個涼亭等待,涼亭距離業務點約300米,且沒法直接看到業務點,須要等待上一個辦理完畢的同窗來通知。假定平均辦理一個業務耗時約30秒AQS框架是支持公平、非公平兩種模式的,使用者能夠根據自身的狀況作選擇,而Java中的內置鎖synchronized
是非公平鎖
即某個線程獲取到鎖後、在釋放鎖以前,再次嘗試獲取鎖,能成功獲取到,不會出現死鎖,即是可重入鎖;須要注意的是,加鎖次數須要跟釋放次數同樣
synchronized
、ReentrantLock
均爲可重入鎖
之因此將這三個鎖放在一塊兒論述,是由於它們都是synchronized
引入的概念,爲了描述流暢,咱們把它們放在一塊兒
synchronized
鎖只有一個線程,並且老是被這一個線程反覆加鎖、解鎖;故引入偏向鎖,且向對象頭的MarkWord
部分中, 標記上線程id,值得一提的是,在線程加鎖結束後,並無解鎖的動做,這樣帶來的好處首先是少了一次CAS操做,其次當這個線程再次嘗試加鎖時,僅僅比較MarkWord
部分中的線程id與當前線程的id是否一致,若是一致則加鎖成功。偏向鎖所以而得名,它偏向於佔有它的線程,對其很是友好。當上一個線程釋放鎖後,若是有另外一個線程嘗試加鎖,偏向鎖會從新偏向新的線程。而當一個線程正佔有鎖,又有一個新的線程試圖加鎖時,便進入了輕量級鎖這3層鎖是逐級膨脹的,且過程不可回逆,即某個鎖一旦進入重量級鎖,便不可回退至輕量級鎖或偏向鎖。雖然synchronized
不是本文的重點,但既然提起來了,咱們能夠把其特性簡單羅列一下
synchronized
獨佔鎖、非公平鎖、可重入;內部作了不少優化那synchronized
鎖的性能究竟如何呢?咱們跟AQS框架中的ReentrantLock
作個簡單對比
public class SynchronizedAndReentrant { private static int THREAD_NUM = 5; private static int EXECUTE_COUNT = 30000000; /** * 模擬ReentrantLock處理業務 */ @Test public void test() throws InterruptedException { ReentrantLock reentrantLock = new ReentrantLock(); long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < THREAD_NUM; i++) { executorService.submit(() -> { for (int j = 0; j < EXECUTE_COUNT; j++) { reentrantLock.lock(); doBusiness(); reentrantLock.unlock(); } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("ReentrantLock cost : " + (System.currentTimeMillis() - begin)); } private void doBusiness() { } /** * 模擬synchronized處理業務 */ @Test public void test2() throws InterruptedException { long begin = System.currentTimeMillis(); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < THREAD_NUM; i++) { executorService.submit(() -> { for (int j = 0; j < EXECUTE_COUNT; j++) { synchronized (SynchronizedAndReentrant.class) { doBusiness(); } } }); } executorService.shutdown(); executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); System.out.println("synchronized cost : " + (System.currentTimeMillis() - begin)); } }
鎖 | 耗時1 | 耗時2 | 耗時3 | 耗時4 | 耗時5 | 平均耗時(ms) |
---|---|---|---|---|---|---|
ReentrantLock |
5876 | 5879 | 5601 | 5939 | 5925 | 5844 |
synchronized |
5551 | 5611 | 5794 | 5397 | 5445 | 5559 |
在JDK1.8的ConcurrentHashMap
中,做者已經將分段鎖摒棄,進而採用synchronized
爲分桶加鎖。synchronized
已日趨成熟,咱們應該摒棄對它低性能的偏見,放心大膽地去使用它