@一 面試中關于 synchronized 關鍵字的 5 連擊
@二 面試中關于線程的2連擊
@三 面試中關于 線程池的 4 連擊
@四 面試中關于 Atomic 原子類的 4 連擊
@五 AQS
一 面試中關于 synchronized 關鍵字的 5 連擊
1.1 說一說自己對于 synchronized 關鍵字的了解
synchronized關鍵字解決的是多個線程之間通路資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻隻能有一個線程執行。
早期的 synchronized 效率低, Java 早期版本中,synchronized屬于重量級鎖,效率低下,因為螢幕鎖(monitor)是依賴于底層的操
作系統的 Mutex Lock 來實作的,Java 的線程是映射到作業系統的原生線程之上的。如果要挂起或者喚醒一個線程,都需要作業系統幫忙完成,而作業系統實作線程之間的切換時需要從使用者态轉換到核心态,這個狀态之間的轉換需要相對比較長的時間,時間成本相對較高。
JDK1.6對鎖的實作引入了大量的優化,如自旋鎖、适應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
1.2 說說自己是怎麼使用 synchronized 關鍵字,在項目中用到了嗎?
synchronized關鍵字最主要的三種使用方式:
修飾執行個體方法,作用于目前對象執行個體加鎖,進入同步代碼前要獲得目前對象執行個體的鎖
修飾靜态方法,作用于目前類對象加鎖,進入同步代碼前要獲得目前類對象的鎖 。也就是給目前類加鎖,會作用于類的所有對象執行個體,因為靜态成員不屬于任何一個執行個體對象,是類成員( static 表明這是該類的一個靜态資源,不管new了多少個對象,隻有一份,是以對該類的所有對象都加了鎖)。是以如果一個線程A調用一個執行個體對象的非靜态 synchronized 方法,而線程B需要調用這個執行個體對象所屬類的靜态 synchronized 方法,是允許的,不會發生互斥現象,因為通路靜态 synchronized 方法占用的鎖是目前類的鎖,而通路非靜态synchronized 方法占用的鎖是目前執行個體對象鎖。
修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。 和 synchronized 方法一樣,synchronized(this)代碼塊也是鎖定目前對象的。synchronized 關鍵字加到 static 靜态方法和synchronized(class)代碼塊上都是是給 Class 類上鎖。這裡再提一下:synchronized關鍵字加到非 static 靜态方法上是給對象執行個體上鎖。另外需要注意的是:盡量不要使用 synchronized(String a) 因為JVM中,字元串常量
池具有緩沖功能!
雙重校驗鎖實作對象單例(線程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判斷對象是否已經執行個體過,沒有執行個體化過才進入加鎖代碼
if (uniqueInstance == null) {
//類對象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
另外,需要注意 uniqueInstance 采用 volatile 關鍵字修飾也是很有必要。
uniqueInstance 采用 volatile 關鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實是分為三步執行:
- 為 uniqueInstance 配置設定記憶體空間
- 初始化 uniqueInstance
-
将 uniqueInstance 指向配置設定的記憶體位址
但是由于 JVM 具有指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出先問題,但是在多線程環境下會導緻一個線程獲得還沒有初始化的執行個體。例如,線程 T1 執行了 1 和 3,此時 T2 調用getUniqueInstance() 後發現uniqueInstance 不為空,是以傳回 uniqueInstance,但此時 uniqueInstance 還未被初始化。使用volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運作。
1.3 講一下 synchronized 關鍵字的底層原理
synchronized 關鍵字底層原理屬于 JVM 層面。
① synchronized 同步語句塊的情況
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代碼塊");
}
}
}
通過 JDK 自帶的 javap 指令檢視 SynchronizedDemo 類的相關位元組碼資訊:首先切換到類的對應目錄執行 javacSynchronizedDemo.java 指令生成編譯後的 .class 檔案,然後執行 javap -c -s -v -lSynchronizedDemo.class 。
synchronized 同步語句塊的實作使用的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。 當執行 monitorenter 指令時,線程試圖擷取鎖也就是擷取 monitor(monitor對象存在于每個Java對象的對象頭中,synchronized 鎖便是通過這種方式擷取鎖的,也是為什麼Java中任意對象可以作為鎖的原因) 的持有權.當計數器為0則可以成功擷取,擷取後将鎖計數器設為1也就是加1。相應的在執行 monitorexit 指令後,将鎖計數器設為0,表明鎖被釋放。如果擷取對象鎖失敗,那目前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
② synchronized 修飾方法的的情況
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的确實是ACC_SYNCHRONIZED 辨別,該辨別指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 通路标志來辨識一個方法是否聲明為同步方法,進而執行相應的同步調用。
1.4 說說 JDK1.6 之後的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎?
對鎖的實作引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、适應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。
鎖主要存在四種狀态,依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态、重量級鎖狀态,他們會随着競争的激烈而逐漸更新。注意鎖可以更新不可降級,這種政策是為了提高獲得鎖和釋放鎖的效率。
①偏向鎖
引入偏向鎖的目的和引入輕量級鎖的目的很像,他們都是為了沒有多線程競争的前提下,減少傳統的重量級鎖使用作業系統互斥量産生的性能消耗。但是不同是:輕量級鎖在無競争的情況下使用 CAS 操作去代替使用互斥量。而偏向鎖在無競争的情況下會把整個同步都消除掉。
偏向鎖的“偏”就是偏心的偏,它的意思是會偏向于第一個獲得它的線程,如果在接下來的執行中,該鎖沒有被其他線程擷取,那麼持有偏向鎖的線程就不需要進行同步!關于偏向鎖的原理可以檢視《深入了解Java虛拟機:JVM進階特性與最佳實踐》第二版的13章第三節鎖優化。
但是對于鎖競争比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,是以這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,并不會立即膨脹為重量級鎖,而是先更新為輕量級鎖。
② 輕量級鎖
倘若偏向鎖失敗,虛拟機并不會立即更新為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的)。輕量級鎖不是為了代替重量級鎖,它的本意是在沒有多線程競争的前提下,減少傳統的重量級鎖使用作業系統互斥量産生的性能消耗,因為使用輕量級鎖時,不需要申請互斥量。另外,輕量級鎖的加鎖和解鎖都用到了CAS操作。關于輕量級鎖的加鎖和解鎖的原理可以檢視《深入了解Java虛拟機:JVM進階特性與最佳實踐》第二版的13章第三節鎖優化。
輕量級鎖能夠提升程式同步性能的依據是“對于絕大部分鎖,在整個同步周期内都是不存在競争的”,這是一個經驗資料。如果沒有競争,輕量級鎖使用 CAS 操作避免了使用互斥操作的開銷。但如果存在鎖競争,除了互斥量開銷外,還會額外發生CAS操作,是以在有鎖競争的情況下,輕量級鎖比傳統的重量級鎖更慢!如果鎖競争激烈,那麼輕量級将很快膨脹為重量級鎖!
③ 自旋鎖和自适應自旋
輕量級鎖失敗後,虛拟機為了避免線程真實地在作業系統層面挂起,還會進行一項稱為自旋鎖的優化手段。
互斥同步對性能最大的影響就是阻塞的實作,因為挂起線程/恢複線程的操作都需要轉入核心态中完成(使用者态轉換到核心态會耗費時間)。
一般線程持有鎖的時間都不是太長,是以僅僅為了這一點時間去挂起線程/恢複線程是得不償失的。 是以,虛拟機的開發團隊就這樣去考慮:“我們能不能讓後面來的請求擷取鎖的線程等待一會而不被挂起呢?看看持有鎖的線程是否很快就會釋放鎖”。為了讓一個線程等待,我們隻需要讓線程執行一個忙循環(自旋),這項技術就叫做自旋。
百度百科對自旋鎖的解釋:
何謂自旋鎖?它是為實作保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在排程機制上略有不同。對于互斥鎖,如果資源已經被占用,資源申請者隻能進入睡眠狀态。但是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被别的執行單元保持,調用者就一直循環在那裡看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是是以而得名。
自旋鎖在 JDK1.6 之前其實就已經引入了,不過是預設關閉的,需要通過–XX:+UseSpinning參數來開啟。JDK1.6及1.6之後,就改為預設開啟的了。需要注意的是:自旋等待不能完全替代阻塞,因為它還是要占用處理器時間。如果鎖被占用的時間短,那麼效果當然就很好了!反之,相反!自旋等待的時間必須要有限度。如果自旋超過了限定次數任然沒有獲得鎖,就應該挂起線程。自旋次數的預設值是10次,使用者可以修改–XX:PreBlockSpin來更改。
另外,在 JDK1.6 中引入了自适應的自旋鎖。自适應的自旋鎖帶來的改進就是:自旋的時間不在固定了,而是和前一次同一個鎖上的自旋時間以及鎖的擁有者的狀态來決定,虛拟機變得越來越“聰明”了。
④ 鎖消除
鎖消除了解起來很簡單,它指的就是虛拟機即使編譯器在運作時,如果檢測到那些共享資料不可能存在競争,那麼就執行鎖消除。鎖消除可以節省毫無意義的請求鎖的時間。
⑤ 鎖粗化
原則上,我們再編寫代碼的時候,總是推薦将同步快的作用範圍限制得盡量小——隻在共享資料的實際作用域才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競争,那等待線程也能盡快拿到鎖。
大部分情況下,上面的原則都是沒有問題的,但是如果一系列的連續操作都對同一個對象反複加鎖和解鎖,那麼會帶來很多不必要的性能消耗。
1.5 談談 synchronized和ReenTrantLock 的差別
① 兩者都是可重入鎖
兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次擷取自己的内部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要擷取這個對象的鎖的時候還是可以擷取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次擷取鎖,鎖的計數器都自增1,是以要等到鎖的計數器下降為0時才能釋放鎖。
② synchronized 依賴于 JVM 而 ReenTrantLock 依賴于 API
synchronized 是依賴于 JVM 實作的,前面我們也講到了 虛拟機團隊在 JDK1.6 為 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛拟機層面實作的,并沒有直接暴露給我們。
ReenTrantLock 是 JDK 層面實作的(也就是 API 層面,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),是以我們可以通過檢視它的源代碼,來看它是如何實作的。
③ ReenTrantLock 比 synchronized 增加了一些進階功能
相比synchronized,ReenTrantLock增加了一些進階功能。主要來說主要有三點:
①等待可中斷;
②可實作公平鎖;
③可實作選擇性通知(鎖可以綁定多個條件)
ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實作這個機制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。
ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized隻能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReenTrantLock預設情況是非公平的,可以通過 ReenTrantLock類的ReentrantLock(boolean fair) 構造方法來制定是否是公平的。
synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實作等待/通知機制,ReentrantLock類當然也可以實作,但是需要借助于Condition接口與newCondition() 方法。Condition是JDK1.5之後才有的,它具有很好的靈活性,比如可以實作多路通知功能也就是在一個Lock對象中可以建立多個Condition執行個體(即對象螢幕),線程對象可以注冊在指定的Condition中,進而可以有選擇性的進行線程通知,在排程線程上更加靈活。 在使用notify/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結合Condition執行個體可以實作“選擇性通知” ,這個功能非常重要,而且是Condition接口預設提供的。而synchronized關鍵字就相當于整個Lock對象中隻有一個Condition執行個體,所有的線程都注冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處于等待狀态的線程這樣會造成很大的效率問題,而Condition執行個體的signalAll()方法 隻會喚醒注冊在該Condition執行個體中的所有等待線程。如果你想使用上述功能,那麼選擇ReenTrantLock是一個不錯的選擇。
④ 性能已不是選擇标準
二 面試中關于線程的2連擊
2.1 講一下Java記憶體模型
在 JDK1.2 之前,Java的記憶體模型實作總是從主存(即共享記憶體)讀取變量,是不需要進行特别的注意的。而在目前的 Java 記憶體模型下,線程可以把變量儲存本地記憶體(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數
據的不一緻。
要解決這個問題,就需要把變量聲明為 volatile,這就訓示 JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。
說白了, volatile 關鍵字的主要作用就是保證變量的可見性然後還有一個作用是防止指令重排序。
2.2 說說 synchronized 關鍵字和 volatile 關鍵字的差別
volatile關鍵字是線程同步的輕量級實作,是以volatile性能肯定比synchronized關鍵字要好。但是volatile關鍵字隻能用于變量而synchronized關鍵字可以修飾方法以及代碼塊。
synchronized關鍵字在JavaSE1.6之後進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優化之後執行效率有了顯著提升,實際開發中使用 synchronized 關鍵字的場景還是更多一些。
多線程通路volatile關鍵字不會發生阻塞,而synchronized關鍵字可能會發生阻塞。
volatile關鍵字能保證資料的可見性,但不能保證資料的原子性。synchronized關鍵字兩者都能保證。
volatile關鍵字主要用于解決變量在多個線程之間的可見性,而 synchronized關鍵字解決的是多個線程之間通路資源的同步性。
三 面試中關于 線程池的 4 連擊
3.1 為什麼要用線程池?
線程池提供了一種限制和管理資源(包括執行一個任務)。 每個線程池還維護一些基本統計資訊,例如已完成任務的數量。
這裡借用《Java并發程式設計的藝術》提到的來說一下使用線程池的好處:
1.降低資源消耗。 通過重複利用已建立的線程降低線程建立和銷毀造成的消耗。
2.提高響應速度。 當任務到達時,任務可以不需要的等到線程建立就能立即執行。
3.提高線程的可管理性。 線程是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的配置設定,調優和監控。
3.2 實作Runnable接口和Callable接口的差別
如果想讓線程池執行任務的話需要實作的Runnable接口或Callable接口。
Runnable接口或Callable接口實作類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。
兩者的差別在于 Runnable 接口不會傳回結果但是 Callable 接口可以傳回結果。
備注: 工具類 Executors 可以實作 Runnable 對象和 Callable 對象之間的互相轉換。( Executors.callable(Runnable task) 或 Executors.callable(Runnable task,Object resule) )。
3.3 執行execute()方法和submit()方法的差別是什麼呢?
-
execute() 方法用于送出不需要傳回值的任務,是以無法判斷任務是否被線程池執行成功與否;
2)submit()方法用于送出需要傳回值的任務。線程池會傳回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,并且可以通過future的get()方法來擷取傳回值,get()方法會阻塞目前線程直到任務完成,而使用get(long timeout,TimeUnit unit) 方法則會阻塞目前線程一段時間後立即傳回,這時候有可能任務沒有執行完。
3.4 如何建立線程池
《阿裡巴巴Java開發手冊》中強制線程池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明确線程池的運作規則,規避資源耗盡的風險
Executors 傳回線程池對象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允許請求的隊列長度為 Integer.MAX_VALUE,可能堆積大量的請求,進而導緻OOM。
CachedThreadPool 和 ScheduledThreadPool : 允許建立的線程數量為Integer.MAX_VALUE ,可能會建立大量線程,進而導緻OOM。
方式一:通過構造方法實作
方式二:通過Executor 架構的工具類Executors來實作 我們可以建立三種類型的ThreadPoolExecutor:
FixedThreadPool : 該方法傳回一個固定線程數量的線程池。該線程池中的線程數量始終不變。當有一個新的任務送出時,線程池中若有空閑線程,則立即執行。若沒有,則新的任務會被暫存在一個任務隊列中,待有線程空閑時,便處理在任務隊列中的任務。
SingleThreadExecutor: 方法傳回一個隻有一個線程的線程池。若多餘一個任務被送出到該線程池,任務會被儲存在一個任務隊列中,待線程空閑,按先入先出的順序執行隊列中的任務。
CachedThreadPool: 該方法傳回一個可根據實際情況調整線程數量的線程池。線程池的線程數量不确定,但若有空閑線程可以複用,則會優先使用可複用的線程。若所有線程均在工作,又有新的任務送出,則會建立新的線程處理任務。所有線程在目前任務執行完畢後,将傳回線程池進行複用。
四 面試中關于 Atomic 原子類的 4 連擊
4.1 介紹一下Atomic 原子類
Atomic 翻譯成中文是原子的意思。在化學上,我們知道原子是構成一般物質的最小機關,在化學反應中是不可分割的。在我們這裡 Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程幹擾。
是以,所謂原子類說簡單點就是具有原子/原子操作特征的類。
并發包 java.util.concurrent 的原子類都存放在 java.util.concurrent.atomic 下,如下圖所示。
4.2 JUC 包中的原子類是哪4類?
基本類型
使用原子的方式更新基本類型
AtomicInteger:整形原子類
AtomicLong:長整型原子類
AtomicBoolean :布爾型原子類
數組類型
使用原子的方式更新數組裡的某個元素
AtomicIntegerArray:整形數組原子類
AtomicLongArray:長整形數組原子類
AtomicReferenceArray :引用類型數組原子類
引用類型
AtomicReference:引用類型原子類
AtomicStampedRerence:原子更新引用類型裡的字段原子類
AtomicMarkableReference :原子更新帶有标記位的引用類型
對象的屬性修改類型
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新長整形字段的更新器
AtomicStampedReference :原子更新帶有版本号的引用類型。該類将整數值與引用關聯起來,可用于解決原子的更新資料和資料的版本号,可以解決使用 CAS 進行原子更新時可能出現的 ABA 問題。
4.3 講講 AtomicInteger 的使用
AtomicInteger 類常用方法
public final int get() //擷取目前的值
public final int getAndSet(int newValue)//擷取目前的值,并設定新的值
public final int getAndIncrement()//擷取目前的值,并自增
public final int getAndDecrement() //擷取目前的值,并自減
public final int getAndAdd(int delta) //擷取目前的值,并加上預期的值
boolean compareAndSet(int expect, int update) //如果輸入的數值等于預期值,則以原子方式将該值設定為輸入值(update)
public final void lazySet(int newValue)//最終設定為newValue,使用 lazySet 設定之後可能導緻其他線程在之後的一小段時間内還是可以讀到舊的值。
使用 AtomicInteger 之後,不用對 increment() 方法加鎖也可以保證線程安全。
4.4 能不能給我簡單介紹一下 AtomicInteger 類的原理
AtomicInteger 線程安全原理簡單分析
AtomicInteger 類的部分源碼:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較并替換”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,進而避免synchronized 的高開銷,執行效率大為提升。
CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的記憶體位址,傳回值是 valueOffset。另外 value 是一個volatile變量,在記憶體中可見,是以 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。
五 AQS
5.1 AQS 介紹
AQS的全稱為(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包下面。
AQS是一個用來建構鎖和同步器的架構,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。當然,我們自己也能利用AQS非常輕松容易地構造出符合我們自己需求的同步器。
5.2 AQS 原理分析
5.2.1 AQS 原理概覽
AQS核心思想是,如果被請求的共享資源空閑,則将目前請求資源的線程設定為有效的工作線程,并且将共享資源設定為鎖定狀态。如果被請求的共享資源被占用,那麼就需要一套線程阻塞等待以及被喚醒時鎖配置設定的機制,這個機制AQS是用CLH隊列鎖實作的,即将暫時擷取不到鎖的線程加入到隊列中。
AQS使用一個int成員變量來表示同步狀态,通過内置的FIFO隊列來完成擷取資源線程的排隊工作。AQS使用CAS對該同步狀态進行原子操作實作對其值的修改。
5.2.2 AQS 對資源的共享方式
AQS定義兩種資源共享方式
Exclusive(獨占):隻有一個線程能執行,如ReentrantLock。
又可分為公平鎖和非公平鎖:
公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
非公平鎖:當線程要擷取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
Share(共享):多個線程可同時執行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我們都會在後面講到。
ReentrantReadWriteLock 可以看成是組合式,因為ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某一資源進行讀。
不同的自定義同步器争用共享資源的方式也不同。自定義同步器在實作時隻需要實作共享資源 state 的擷取與釋放方式即可,至于具體線程等待隊列的維護(如擷取資源失敗入隊/喚醒出隊等),AQS已經在頂層實作好了。
5.2.3 AQS底層使用了模闆方法模式
同步器的設計是基于模闆方法模式的,如果需要自定義同步器一般的方式是這樣(模闆方法模式很經典的一個應用):
- 使用者繼承AbstractQueuedSynchronizer并重寫指定的方法。(這些重寫方法很簡單,無非是對于共享資源state的擷取和釋放)
-
将AQS組合在自定義同步元件的實作中,并調用其模闆方法,而這些模闆方法會調用使用者重寫的方法。
這和我們以往通過實作接口的方式有很大差別,這是模闆方法模式很經典的一個運用。
AQS使用了模闆方法模式,自定義同步器時需要重寫下面幾個AQS提供的模闆方法:
isHeldExclusively()//該線程是否正在獨占資源。隻有用到condition才需要去實作它。
tryAcquire(int)//獨占方式。嘗試擷取資源,成功則傳回true,失敗則傳回false。
tryRelease(int)//獨占方式。嘗試釋放資源,成功則傳回true,失敗則傳回false。
tryAcquireShared(int)//共享方式。嘗試擷取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則傳回true,失敗則傳回false。
預設情況下,每個方法都抛出 UnsupportedOperationException 。 這些方法的實作必須是内部線程安全的,并且通常應該簡短而不是阻塞。AQS類中的其他方法都是final ,是以無法被其他類使用,隻有這幾個方法可以被其他類使用。
以ReentrantLock為例,state初始化為0,表示未鎖定狀态。A線程lock()時,會調用tryAcquire()獨占該鎖并将state+1。此後,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機會擷取該鎖。當然,釋放鎖之前,A線程自己是可以重複擷取此鎖的(state會累加),這就是可重入的概念。但
要注意,擷取多少次就要釋放多麼次,這樣才能保證state是能回到零态的。
再以CountDownLatch以例,任務分為N個子線程去執行,state也初始化為N(注意N要與線程個數一緻)。這N個子線程是并行執行的,每個子線程執行完後countDown()一次,state會CAS(Compare and Swap)減1。等到所有子線程都執行完後(即state=0),會unpark()主調用線程,然後主調用線程就會從await()函數傳回,繼續後餘動作。
一般來說,自定義同步器要麼是獨占方法,要麼是共享方式,他們也隻需實作 tryAcquire-tryRelease 、tryAcquireShared-tryReleaseShared 中的一種即可。但AQS也支援自定義同步器同時實作獨占和共享兩種方式,如 ReentrantReadWriteLock 。
5.3 AQS 元件總結
Semaphore(信号量)-允許多個線程同時通路: synchronized 和 ReentrantLock 都是一次隻允許一個線程通路某個資源,Semaphore(信号量)可以指定多個線程同時通路某個資源。
CountDownLatch (倒計時器): CountDownLatch是一個同步工具類,用來協調多個線程之間的同步。這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計時結束,再開始執行。
CyclicBarrier(循環栅欄): CyclicBarrier 和 CountDownLatch 非常類似,它也可以實作線程間的技術等待,但是它的功能比 CountDownLatch 更加複雜和強大。主要應用場景和 CountDownLatch 類似。CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障才會開門,所有被屏障攔截的線程才會繼續幹活。CyclicBarrier預設的構造方法是 CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await方法告訴 CyclicBarrier 我已經到達了屏障,然後目前線程被阻塞。