第三章 Java
記憶體模型
3.1 Java
記憶體模型的基礎
- 通訊 在共享記憶體的模型裡,通過寫-讀記憶體中的公共狀態進行隱式通訊;在訊息傳遞的併發模型裡,執行緒之間必須通過傳送訊息來進行顯示的通訊。
- 同步 在共享記憶體併發模型裡,同步是顯示進行的,程式設計師必須顯示指定某個方法或者某段程式碼需要線上程之間互斥執行;在訊息傳遞的併發模型裡,由於訊息的傳送必須在接收之前,因此同步是隱式進行的。
在Java
中,所有例項域、靜態域和陣列元素都儲存在堆記憶體中,堆記憶體線上程之間共享;區域性變數、方法定義引數和異常處理器引數不會線上程之間共享。
從抽象角度來看,JMM
定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體,本地記憶體涵蓋了快取、寫緩衝區、暫存器以及其它的硬體和編譯器優化。
JMM
通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為Java
程式設計師提供記憶體可見性保證。
重排序
指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段:
- 編譯器優化的重排序:編譯器在不改變單執行緒程式語義的前提下,重新安排語句的執行順序。
- 處理器的指令級並行的重排序:如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
- 記憶體系統的重排序:由於處理器使用快取和讀/寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。
JMM
的編譯器重新排序規則會禁止特定型別的編譯器重排序,對於處理器重排序,JMM
的處理器重排序規則會要求Java
編譯器在生成指令時,插入特定型別的記憶體屏障。
現代的處理器使用寫緩衝區臨時儲存向記憶體寫入的資料,但每個處理器上的寫緩衝區,僅僅對它所在的處理器可見。
由於寫緩衝區僅對自己的處理器可見,它會導致處理器執行記憶體操作的順序可能會與記憶體實際的操作執行順序不一致,由於現代的處理器都會使用寫緩衝區,因此現代的處理器都會允許對寫-讀操作進行重排序,但不允許對存在資料依賴的操作做重排序。
happens-before
簡介
用來闡述操作之間的記憶體可見性,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作必須要存在happens-before
關係,這兩個操作既可以在一個執行緒之內,也可以在不同執行緒之間,但並不等於前一個操作必須要在後一個操作之前執行。
資料依賴性
編譯器和處理器不會改變存在資料依賴關係的兩個操作的執行順序,但是僅針對單個處理器中執行的指令序列和單個執行緒中執行的操作。
as-if-serial
無論怎麼重排序,單執行緒程式的執行結果不能改變。
在單執行緒中,對存在控制依賴的操作重排序,不會改變執行結果;但在多執行緒程式中,對存在控制依賴的操作重排序,可能會改變程式的執行結果。
順序一致性
順序一致性是一個理論參考模型,在設計的時候,處理器的記憶體模型和程式語言的記憶體模型都會以順序一致性記憶體作為參照。 如果程式是正確同步的,程式的執行將具有順序一致性:即程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同。 如果程式是正確同步的,程式的執行將具有順序一致性:即程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同。 順序一致模型有兩大特性:
- 一個執行緒中的所有操作必須按照程式的順序來執行。
- 所有執行緒都只能看到一個單一的操作執行順序,在順序一致性記憶體模型中,每個操作都必須原子執行且立刻對所有執行緒可見。
對於未同步或未正確同步的多執行緒程式,JMM
只提供最小安全性:執行緒執行時讀取到的值,要麼是之前某個執行緒寫入的值,要麼是預設值。
JMM
不保證未同步程式的執行結果與該程式在順序一致性模型中的執行結果一致。
未同步程式在兩個模型中的執行特徵有如下差異:
- 順序一致性模型保證單執行緒內的操作會按程式的順序執行,而
JMM
不保證單執行緒內的操作會按程式的順序執行。 - 順序一致性模型保證所有執行緒只能看到一致的操作執行順序,而
JMM
不保證所有執行緒能看到一致的操作執行順序。 JMM
不保證對64位的long/double
型變數的寫操作具有原子性,而順序一致性模型保證對所有記憶體讀/寫操作都具有原子性。
第四章 Java
併發程式設計基礎
- 現代作業系統排程的最小單元是執行緒,也叫輕量級程式,在一個程式裡可以建立多個執行緒,這些執行緒都擁有各自的計數器、堆疊和區域性變數等特性,並且能夠訪問共享的記憶體變數。
- 設定執行緒優先順序時,針對頻繁阻塞(休眠或者
I/O
操作)的執行緒需要設定較高優先順序,而偏重計算(需要較多CPU
時間或者偏運算)的執行緒則設定較低的優先順序,確保處理器不會被獨佔。 - 執行緒在執行的生命週期中可能處於以下6種不同的狀態:
New
:初始狀態,執行緒被建立,但是沒有呼叫start()
方法。Runnable
:執行狀態,Java
執行緒將作業系統中的就緒和執行兩種狀態統稱為“執行中”。Blocked
:阻塞狀態,表示執行緒阻塞於鎖。Waiting
:等待狀態,表示執行緒進入等待狀態,進入該狀態表示當前執行緒需要等待其它執行緒做出一些指定動作(通知或中斷)。Time_Waiting
:超時等待狀態,可以在指定的時間自行返回。Terminated
:終止狀態,表示當前執行緒已經執行完畢。- 中斷可以理解為執行緒的一個標識位屬性,它標識一個執行中的執行緒是否被其它執行緒進行了中斷操作。中斷好比其他執行緒對該執行緒打了一個招呼,其他執行緒通過呼叫該執行緒的
interrupt()
方法對其進行中斷操作。 - 執行緒通過檢查自身是否被中斷來進行響應,執行緒通過方法
isInterrupt
來進行判斷是否被中斷,也可以呼叫靜態方法Thread.interrupt
對當前執行緒的中斷標識位進行復位,如果該執行緒已經處於終止狀態,即使該執行緒被中斷過,在呼叫該執行緒物件的isInterrupt
時依舊返回false
。 - 在丟擲
InterruptedException
異常之前,Java
虛擬機器會先將該執行緒的中斷標識位清除。 - 中斷狀態是執行緒的一個標識位,而中斷操作是一種簡便的執行緒間互動方式,而這種互動方式最適合用來取消或停止任務,除了中斷之外,還可以利用一個
boolean
變數來控制是否需要停止任務並終止該執行緒。 Java
支援多個執行緒同時訪問一個物件或者物件的成員變數,由於每個執行緒可以擁有這個變數的拷貝,所以在程式的執行過程中,一個執行緒看到的變數並不一定是最新的。volatile
可以用來修飾欄位,就是告知程式任何對該變數的訪問需要從共享記憶體中獲取,而對它的改變必須同步重新整理回共享記憶體,它能保證所有執行緒對變數訪問的可見性。synchronized
可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。- 任意執行緒對
Object
(Object
由synchronized
保護)的訪問,首先要獲得Object
的監視器,如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為Blocked
,當訪問Object
的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。 - 等待/通知的相關方法:
notify()
:通知一個在物件上等待的執行緒,使其從wait()
方法返回,而返回的前提是該執行緒獲取到了物件上的鎖。notifyAll()
:通知所有等待在該物件上的鎖。wait()
:呼叫該方法的執行緒進入Waiting
狀態,只有等待另外執行緒的通知或被中斷才會返回,呼叫wait()
方法後,會釋放物件的鎖。wait(long)
:超時等待一段時間,如果沒有通知就返回。wait(long, int)
:對於超時時間更精細粒度的控制,可以達到納秒。- 兩個執行緒通過物件來完成互動,而物件上的
wait
和notify/notifyAll()
的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。 - 等待/通知的經典正規化:
- 等待方
(1) 獲取物件的鎖。
(2) 如果條件不滿足,那麼呼叫物件的
wait()
方法,被通知後仍要檢查條件。 (3) 條件滿足則執行對應的邏輯。
synchronized(物件) {
while(條件不滿足) {
物件.wait();
}
對應的處理邏輯;
}
複製程式碼
- 通知方 (1) 獲得物件的鎖 (2) 改變條件 (3) 通知所有等待在該物件上的執行緒。
synchronized(物件) {
改變條件;
物件.notifyAll();
}
複製程式碼
- 管道輸入/輸出流用於執行緒之間的資料傳輸,而傳輸的媒介為記憶體,主要包括了以下4種實現:
PipedOutputStream、PipeInputStream、PipedReader、PipedWriter
,前兩種面向位元組,後兩種面向字元。 - 如果一個執行緒
A
執行了Thread.join()
,其含義是:當前執行緒A
等待Thread
執行緒終止後,才從Thread.join
返回,執行緒Thread
除了提供join()
方法外,還提供了join(long millis)
和join(long millis, int nanos)
兩個具備超時特性的方法,如果在給定的超時時間內沒有終止,那麼將會從超時方法中返回。 ThreadLocal
,即執行緒變數,是一個以ThreadLocal
物件為鍵、任意物件為值的儲存結構,這個結構被附帶線上程上,也就是說一個執行緒可以根據一個ThreadLocal
物件查詢到繫結在這個執行緒上的一個值,可以通過set(T)
方法來設定一個值,在當前執行緒下再通過get()
方法獲取到原先設定的值。
第五章 Java
中的鎖
5.1 Lock
介面
-
鎖是用來控制多個執行緒訪問共享資源的方式,雖然它缺少了隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷地獲取鎖以及超時獲取鎖等多種
synchronized
關鍵字不具備的同步特性。 -
在
finally
塊中釋放鎖,目的是保證在獲取到鎖之後,最終能夠被釋放。 -
Lock
介面提供的synchronized
關鍵字不具備的主要特性 -
嘗試非阻塞地獲取鎖:當前執行緒嘗試獲取鎖,如果這一時刻沒有被其它執行緒獲取到,則成功獲取並持有鎖。
-
能被中斷地獲取鎖:與
synchronized
不同,獲取到鎖的執行緒能夠響應中斷,當獲取到鎖的執行緒被中斷時,中斷異常將會被丟擲,同時鎖會被釋放。 -
在指定的截止時間之前獲取鎖:如果截止時間到了仍舊無法獲取鎖,則返回。
-
Lock
的API
-
void lock()
:獲取鎖,呼叫該方法當前執行緒將會獲取鎖,當鎖獲得後,從該方法返回。 -
void lockInterruptibly()
:可中斷地獲取鎖,該方法會響應中斷,即在鎖的獲取中可以中斷當前執行緒。 -
boolean tryLock()
:嘗試非阻塞地獲取鎖,呼叫該方法後立刻返回,如果能夠獲取則返回true
,否則返回false
。 -
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
:當前執行緒在超時時間內獲得了鎖;當前執行緒在超時時間內被中斷;超時時間結束,返回false
。 -
void unlock()
:釋放鎖。 -
Condition newCondition()
:獲取等待/通知元件,該元件和當前的鎖繫結,當前執行緒只有獲得了鎖,才能呼叫該元件的wait()
方法,而呼叫後,當前執行緒將釋放鎖。
5.2 佇列同步器
5.2.1 佇列同步器介面
- 佇列同步器
AbstractQueuedSynchronizer
,是用來構建鎖或者其它同步元件的基礎框架,它使用了一個int
成員變數表示同步狀態,通過內建的FIFO
佇列來完成資源獲取執行緒的排隊工作。 - 同步器是實現鎖的關鍵,在鎖的實現中聚合同步器,利用同步器實現鎖的語義。可以理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖互動的介面,隱藏了實現細節;同步器面向鎖的實現者,它簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注地領域。
- 同步器的設計是基於模板方法模式,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步元件的實現中,並呼叫同步器的模板方法,而這些模板方法將會呼叫使用者過載的方法。
- 重寫同步器指定的方法時,需要使用同步器提供的3個方法來訪問或者修改同步狀態:
getState()
:獲取當前同步狀態。setState(int newState)
:設定當前同步狀態。compareAndSetState(int except, int update)
:使用CAS
設定當前狀態,該方法能夠保證狀態設定的原始性。- 同步器提供的模板方法基本上分為以下3類:
- 獨佔式獲取與釋放同步狀態
- 共享式獲取與釋放同步狀態
- 查詢同步佇列中的等待執行緒情況。
5.2.2 佇列同步器的實現分析
5.2.2.1 同步佇列
- 同步器依賴內部的同步佇列來完成同步狀態的管理,當前執行緒獲取同步狀態失敗時,同步器會將當前執行緒以及等待狀態等資訊構造稱為一個節點,並將其加入同步佇列,同時會阻塞當前執行緒,當同步狀態釋放時,會把首節點中的執行緒喚醒,使其再次嘗試獲取同步狀態。
- 同步器中包含了兩個節點型別的引用,一個指向頭節點,而另一個指向尾節點。
- 當一個執行緒成功地獲取了同步狀態,其他執行緒將無法獲取到同步狀態,轉而被構造成為節點並加入到同步佇列當中,而這個加入到佇列地過程必須要保證執行緒安全,因此同步器提供了一個基於
CAS
的設定尾節點的方法。 - 同步佇列遵循
FIFO
,首節點是獲取同步狀態成功的節點,首節點的執行緒在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設定為首節點。
5.2.2.2 獨佔式同步狀態獲取與釋放
- 通過呼叫同步器的
acquire(int arg)
方法可以獲取同步狀態,該方法對中斷不敏感,即由於執行緒獲取同步狀態失敗而進入同步佇列後,後續對執行緒進行中斷操作時,執行緒不會從同步佇列中移除。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
它的主要邏輯是:
- (1)呼叫自定義同步器實現的
tryAcquire
方法,該方法保證執行緒安全的獲取同步狀態,這個方法需要佇列同步器的實現者來重寫。 - (2)如果同步狀態獲取失敗,則構造同步節點(獨佔式
Node.EXCLUSIVE
)並通過addWaiter(Node node)
方法將該節點加入到同步佇列的尾部。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
//1.確保節點能夠執行緒安全地被新增
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//2.通過死迴圈來確保節點的正確新增,在"死迴圈"中只有通過`CAS`將節點設定為尾節點之後,當前執行緒才能從該方法返回,否則當前執行緒不斷地進行嘗試。
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
複製程式碼
- (3)最後呼叫
acquireQueued(Node node, int arg)
方法,使得該節點以死迴圈的方式獲取同步狀態。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//1.得到當前節點的前驅節點
final Node p = node.predecessor();
//2.如果當前節點的前驅節點是頭節點,只有在這種情況下獲取同步狀態成功
if (p == head && tryAcquire(arg)) {
//3.將當前節點設為頭節點
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
-
可以看到,當前執行緒在“死迴圈”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態,這是由於:
- 頭節點是成功獲取到同步狀態的節點,而頭節點的執行緒釋放了同步狀態後,將會喚醒其後繼節點,後繼節點的執行緒被喚醒後需要檢查自己的前驅節點是否是頭節點。
- 維護同步佇列的
FIFO
原則,通過簡單地判斷自己的前驅是否為頭節點,這樣就使得節點的釋放規則符合FIFO
,並且也便於對過早通知的處理(過早通知是指前驅節點不是頭節點的執行緒由於中斷而被喚醒)
-
當同步狀態獲取成功之後,當前執行緒從
acquire(int arg)
方法返回,如果對於鎖這種併發元件而言,代表著當前執行緒獲取了鎖。 -
通過呼叫同步器的
release(int arg)
方法可以釋放同步狀態,該方法執行時,會喚醒頭節點的後繼節點執行緒,unparkSuccessor(Node node)
方法使用LockSupport
來喚醒處於等待狀態的執行緒。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
複製程式碼
- (4)如果獲取不到,則阻塞節點中的執行緒,而被阻塞執行緒的喚醒主要依靠前驅節點的出隊或阻塞執行緒被中斷來實現。
總結: 1.在獲取同步狀態時,同步器維護一個同步佇列,獲取狀態失敗的執行緒都會被加入到佇列中進行自旋; 2.移出佇列(或停止自旋)的條件是前驅節點為頭節點且成功獲取了同步狀態。 3.在釋放同步狀態時,同步器呼叫
tryRelease(int arg)
方法來釋放同步狀態,然後喚醒頭節點的後繼節點。
5.2.2.3 共享式同步狀態獲取與釋放
- 共享式獲取和獨佔式獲取最主要的區別在於同一時刻能夠有多個執行緒同時獲取到同步狀態。
- 通過呼叫同步器的
acquireShared(int arg)
方法可以共享式地獲取同步狀態:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
tryAcquireShared
返回int
型別,如果同步狀態獲取成功,那麼返回值大於等於0,否則進入自旋狀態;成功獲取到同步狀態並退出自旋狀態的條件是當前節點的前驅節點為頭節點,並且返回值大於等於0.
- 共享式獲取,通過呼叫
releaseShared(int arg)
方法釋放同步狀態,tryReleaseShared
必須要確保同步狀態執行緒安全釋放,一般是通過迴圈或CAS
來保證的,因為釋放同步狀態的操作會同時來自多個執行緒。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
複製程式碼
5.2.2.4 獨佔式超時獲取同步狀態
- 通過呼叫同步器的
doAcquireNanos(int arg, long nanosTimeout)
方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態。 - 在此之前,一個執行緒如果獲取不到鎖而被阻塞在
synchronized
之外,對該執行緒進行中斷操作,此時執行緒中斷的標誌位會被修改,但執行緒依舊會阻塞在synchronized
上;如果通過acquireInterruptibly(int arg)
方法獲取,如果在等待過程中被中斷,會立刻返回,並丟擲InterruptedException
異常。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//1.計算出截止時間.
final long deadline = System.nanoTime() + nanosTimeout;
//2.加入節點
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
//3.取出前驅節點
final Node p = node.predecessor();
//4.如果獲取成功則直接返回
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
//5.如果到了超時時間,則直接返回
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//6.如果在自旋過程中被中斷,那麼丟擲異常返回
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
通過上面的程式碼可以知道,它和獨佔式獲取的區別在於未獲取到同步狀態時的處理邏輯:獨佔式獲取在獲取不到是會一直自旋等待;而超時獲取則會使當前執行緒等待nanosTimeout
納秒,如果當前執行緒在這個時間內沒有獲取到同步狀態,將會從等待邏輯中自動返回。
5.2.2.5 自定義同步元件 - TwinsLock
TwinsLock
只允許至多兩個執行緒同時訪問,超過兩個執行緒的訪問將會被阻塞。
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
//初始值為2.
setState(count);
}
@Override
protected int tryAcquireShared(int arg) {
for(;;) {
//1.獲得當前的狀態.
int current = getState();
//2.newCount表示剩餘可獲取同步狀態的執行緒數
int newCount = current - arg;
//3.如果小於0,那麼返回獲取同步狀態失敗;否則通過CAS確保設定的正確性.
if (newCount < 0 || compareAndSetState(current, newCount)) {
//4.當返回值大於等於0表示獲取同步狀態成功.
return newCount;
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
for (;;) {
int current = getState();
//將可獲取同步狀態的執行緒數加1.
int newCount = current + current;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
}
@Override
public void lock() {
sync.acquireShared(1);
}
@Override
public void unlock() {
sync.releaseShared(1);
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@NonNull
@Override
public Condition newCondition() {
return null;
}
}
複製程式碼
測試用例:
public static void createTwinsLock() {
final Lock lock = new TwinsLock();
class TwinsLockThread extends Thread {
@Override
public void run() {
Log.d(TAG, "TwinsLockThread, run=" + Thread.currentThread().getName());
while (true) {
lock.lock();
try {
Thread.sleep(1000);
Log.d(TAG, "TwinsLockThread, name=" + Thread.currentThread().getName());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
Log.d(TAG, "TwinsLockThread, unlock=" + Thread.currentThread().getName());
lock.unlock();
}
}
}
}
for (int i = 0; i < 10; i++) {
Thread thread = new TwinsLockThread();
thread.start();
}
}
複製程式碼
5.3 重入鎖
- 重入鎖
ReentrantLock
表示該鎖能夠支援一個執行緒對資源的重複加鎖。 - 如果在絕對時間上,先對鎖獲取的請求一定先被滿足,那麼這個鎖是公平的,公平地獲取鎖,也就是等待時間最長的執行緒最優先地獲取鎖。
5.3.1 實現重進入
重進入需要解決兩個問題:
- 執行緒再次獲取鎖,鎖需要去識別獲取鎖地執行緒是否為當前佔據鎖的執行緒,如果是,則再次獲取成功。
- 鎖的最終釋放,執行緒重複
n
次獲取了鎖,隨後在第n
次釋放該鎖後,其它執行緒能夠獲取到該鎖。
5.3.2 公平與非公平鎖的區別
- 公平與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,即
FIFO
。 - 公平鎖的區別在於加入了同步佇列中當前節點是否有前驅節點的判斷,如果該方法返回
true
,表示有執行緒比當前執行緒更早地請求獲取鎖,因此需要等待前驅執行緒獲取並釋放鎖之後才能繼續獲取鎖;而對於非公平鎖,只要CAS
設定同步狀態成功即可。 - 因此,公平鎖每次都是從同步佇列中的第一個節點獲取到鎖,而非公平鎖出現了一個執行緒連續獲取鎖的情況。
- 非公平鎖可能使執行緒飢餓,但其極少的執行緒切換,保證了更大的吞吐量。
5.4 讀寫鎖
- 之前提到的鎖都是排它鎖,這些鎖在同一時刻只允許一個執行緒進行訪問,而讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得併發性有很大提升。
- 併發包提供的讀寫鎖的實現是
ReentrantReadWrireLock
,它支援公平性選擇、重進入、鎖降級(寫鎖能夠降級為讀鎖)。
ReadWriteLock
僅定義了獲取讀鎖和寫鎖的兩個方法,即readLock
和writeLock
,而其實現ReentrantReadWriteLock
:
getReadLockCount
:返回當前讀鎖被獲取的次數。getReadHoldCount
:返回當前執行緒獲取讀鎖的次數。isWriteLocked
:判斷寫鎖是否被獲取。getWriteHoldCount
:返回當前執行緒獲取寫鎖的次數。
下面是一個讀寫鎖的簡單用例:
public class ReadWriteCache {
static Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
public static Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
public static Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
public static void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
}
複製程式碼
5.4.2 讀寫鎖的實現分析
- 讀寫狀態的設計 讀寫鎖需要在同步狀態(一個整形變數,高16表示讀,低16表示寫)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵。
- 寫鎖的獲取與釋放 寫鎖是一個支援重進入的排它鎖,如果當前執行緒已經獲取了寫鎖,則增加寫狀態。如果當前執行緒在獲取寫鎖時,讀鎖已經被獲取,則當前執行緒進入等待狀態。 原因在於:讀寫鎖要確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已經被獲取的情況下對寫鎖的獲取,那麼正在執行的其它讀執行緒就無法感知到當前寫執行緒的操作。
- 讀鎖的獲取與釋放 讀鎖是一個支援重進入的共享鎖,它能被多個執行緒同時獲取,在沒有其它寫執行緒訪問(或者寫狀態為0)時,讀鎖總是被成功地獲取,而所做的也只是(執行緒安全)增加讀狀態。
- 鎖降級 鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨後釋放(先前擁有的)寫鎖的過程。
5.6 Condition
介面
Condition
定義了等待/通知兩種型別的方法,當前執行緒呼叫這些方法時,需要提前獲取到Condition
物件關聯的鎖,Condition
是依賴Lock
物件的。
當呼叫await()
方法後,當前執行緒會釋放鎖並在此等待,而其他執行緒呼叫Condition
物件的signal
方法,通知當前執行緒後,當前執行緒才從await
方法返回,並且在返回前已經獲取了鎖。
獲取一個Condition
必須通過Lock
的newCondition
方法,下面是一個有界佇列的示例:
public class BoundedQueue<T> {
private Object[] items;
private int addIndex, removeIndex, count;
private Lock lock = new ReentrantLock();
private Condition notEmpty = lock.newCondition();
private Condition notFull = lock.newCondition();
public BoundedQueue(int size) {
items = new Object[size];
}
public void add(T t) throws InterruptedException {
lock.lock();
try {
while (count == items.length) { //如果當前佇列內的個數等於最大長度,那麼釋放鎖.
notFull.await();
}
if (++addIndex == items.length) { //如果已經到了尾部,那麼從頭開始.
addIndex = 0;
}
++count;
notEmpty.signal(); //通知阻塞在"空"條件上的執行緒.
} finally {
lock.unlock();
}
}
public T remove() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); //如果當前佇列的個數等於0,那麼釋放鎖.
}
Object x = items[removeIndex];
if (++removeIndex == items.length) {
removeIndex = 0;
}
--count;
notFull.signal(); //通知阻塞在"滿"條件上的執行緒.
return (T) x;
} finally {
lock.unlock();
}
}
}
複製程式碼
Condition
的方法:
-
await()
:當前執行緒進入等待狀態直到被通知signal
或中斷,當前執行緒進入執行狀態且從await
返回的情況: -
其他執行緒呼叫該
Condition
的signal
或signalAll
方法。 -
其它執行緒中斷當前執行緒(
interrupt
)。 -
如果當前等待執行緒從
await
方法返回,那麼表明當前執行緒已經獲取了Condition
物件所對應的鎖。 -
awaitUninerruptibly
:對中斷不敏感 -
long await Nanos(long)
:加入了超時的判斷,返回值是(nanosTimeout
- 實際耗時),如果返回值是0或者負數,那麼可以認定為超時。 -
boolean awaitUntil(Data)
:直到某個固定時間。 -
signal
:喚醒一個等待在Condition
上的執行緒。 -
signalAll
:喚醒所有等待在Condition
上的執行緒。
5.6.2 Condition
的實現
ConditionObject
是AbstractQueuedSynchronizer
的內部類,每個Condition
物件都包含著一個佇列。
1.等待佇列
在佇列中的每個節點都包含了一個執行緒的引用,該執行緒就是在Condition
物件上等待的執行緒,同步佇列和等待佇列中節點的型別都是同步器的靜態內部類AbstractQueuedSynchronizer.Node
。
由於Condition
的實現是同步器的內部類,因此每個Condition
例項都能夠訪問同步器提供的方法,相當於每個Condition
都擁有所屬同步器的引用。
當呼叫await
方法時,將會以當前執行緒構造節點,並將節點從尾部加入到等待佇列,也就是將同步佇列移動到**Condition
**佇列當中。
2.等待
呼叫該方法的前提是當前執行緒必須獲取了鎖,也就是同步佇列中的首節點,它不是直接加入到等待佇列當中,而是通過addConditionWaiter()
方法把當前執行緒構造成一個新的節點並將其加入到等待佇列當中。
3.通知
呼叫該方法的前提是當前執行緒必須獲取了鎖,接著獲取等待佇列的首節點,將其移動到同步佇列並使用LockSupport
喚醒節點中的執行緒。
被喚醒的執行緒,將從await
方法中的while
中返回,進而呼叫同步器的acquireQueued
方法加入到獲取同步狀態的競爭中。
Condition
的signalAll
方法,相當於對等待佇列中的每個節點均執行一次signal
方法,效果就是將等待佇列中所有節點全部移動到同步佇列中,並喚醒每個節點。
六、Java
併發容器和框架
6.1 ConcurrentHashMap
ConcurrentHashMap
是執行緒安全並且高效的HashMap
,其它的類似容器有以下缺點:
HashMap
在併發執行put
操作時,會導致Entry
連結串列形成環形資料結構,就會產生死迴圈獲取Entry
。HashTable
使用synchronized
來保證執行緒安全,但線上程競爭激烈的情況下HashTable
的效率非常低下。ConcurrentHashMap
高效的原因在於它採用鎖分段技術,首先將資料分成一段一段地儲存,然後給每段資料配一把鎖,當一個執行緒佔用鎖並且訪問一段資料的時候,其他段的資料也能被其他執行緒訪問。
6.1.2 ConcurrentHashMap
的結構
ConcurrentHashMap
是由Segment
陣列結構和HashEntry
陣列結構組成:
Segment
是一種可重入鎖,在ConcurrentHashMap
裡面扮演鎖的角色;HashEntry
則用於儲存鍵值對資料。
一個ConcurrentHashMap
裡包含一個Segment
陣列,它的結構和HashMap
類似,是一種陣列和連結串列結構。
一個Segment
裡包含一個HashEntry
陣列,每個HashEntry
是一個連結串列結構的元素,每個Segment
守護著一個HashEntry
裡的元素,當對HashEntry
陣列的資料進行修改時,必須首先獲得與它對應的Segment
鎖。
6.1.5 ConcurrentHashMap
的操作
get
get
的高效在於整個get
過程中不需要加鎖,除非讀到的值是空才會加鎖重讀。原因是它的get
方法將要使用的共享變數都設為volatile
,能夠線上程間保持可見性,能夠被多執行緒同時讀,並且不會讀到過期的值,例如用於統計當前Segment
大小的count
欄位和用於儲存值的HashEntry
的value
。
put
put
方法裡需要對共享變數進行寫入操作,所以為了執行緒安全,在操作共享變數之前必須加鎖,put
首先定位到Segment
,然後在Segment
裡進行插入操作。
size
先嚐試2次通過不鎖住Segment
的方式來統計各個Segment
的大小,如果統計的過程中,容器的count
發生了變化,則再用加鎖的方式來統計所有Segment
的大小。
6.2 ConcurrentLinkedQueue
ConcurrentLinkedQueue
是一個基於連結節點的無界執行緒安全佇列,它採用先進先出的規則對節點進行排序,它採用CAS
演算法來實現。
6.2.1 入佇列
入隊主要做兩件事情:
- 將入隊節點設定成當前佇列尾節點的下一個節點。
- 更新
tail
節點,如果tail
節點的next
節點不為空,則將入隊節點設定成tail
節點;如果tail
節點的next
節點為空,則將入隊節點設定成tail
的next
節點。
在多執行緒情況下,如果有一個執行緒正在入隊,那麼它必須先獲取尾節點,然後設定尾節點的下一個節點為入隊節點,但這時可能有另外一個執行緒插隊了,那麼佇列的尾節點就會發生變化,這時第一個執行緒要暫停入隊操作,然後重新獲取尾節點。 整個入隊操作主要做兩件事:
- 定位出尾節點。
- 使用
CAS
演算法將入隊節點設定成尾節點的next
節點,如不成功則重試。
6.3 阻塞佇列
6.3.1 阻塞佇列
阻塞佇列是一個支援兩個附加操作的佇列,這兩個附加的操作支援阻塞的插入和移除方法:
- 當佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿。
- 當佇列空時,獲取元素的執行緒會等待佇列為空。
在阻塞佇列不可用時,附加操作提供了4種處理方式:丟擲異常、返回特殊值、一直阻塞、超時退出。每種方式通過呼叫不同的方法來實現。
Java
裡面提供了7種阻塞佇列。
6.4 Fork/Join
框架
用於並行執行任務的框架,是把一個大任務分割成若干個小任務,最終彙總每個小任務結果後得到大人物結果的框架。
Fork/Join
使用兩個類來完成事情:
ForkJoinTask
:它提供了fork()
和join()
操作的機制,通常情況下,我們繼承它的子類:有返回結果的RecursiveTask
和沒有返回結果的RecursiveAction
。ForkJoinPool
:ForkJoinTask
需要通過ForkJoinPool
來新增。ForkJoinTask
在執行的時候可能會丟擲異常,但是我們沒有辦法在主執行緒裡直接捕獲異常,所以ForkJoinTask
提供了isCompletedAbnormally()
方法來檢查任務是否已經丟擲異常或已經取消了。ForkJoinPool
由ForkJoinTask
陣列和ForkJoinWorkerThread
陣列組成,ForkJoinTask
陣列負責將存放程式提交給ForkJoinPool
的任務,而ForkJoinWorkerThread
陣列負責執行這些任務。
七、Java
中的13個原子操作類
Atomic
包裡提供了:原子更新基本型別、原子更新陣列、原子更新引用和原子更新屬性。
7.1 原子更新基本型別:
AtomicBoolean
AtomicInteger
AtomicLong
基本方法:
int addAndGet(int delta)
:以原子方式將輸入的值與當前的值相加,並返回結果。boolean compareAndSet(int expect, int update)
:如果當前的數值等於預期值,則以原子方式將該值設定為輸入的值。int getAndIncrement()
:以原子方式加1,並返回自增前的值。void lazySet(int newValue)
:最終會設定成newValue
,可能會導致其他執行緒在之後的一小段時間內還是讀到舊值。int getAndSet(int newValue)
:以原子方式設定為newValue
的值,並返回舊值。
7.2 原子更新引用型別
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
基本方法:
int addAndGet(int i, int delta)
:以原子方式將輸入值和索引i
的元素相加。boolean compareAndSet(int i, int expect, int update)
:如果當前值等於預期值,則以原子方式將陣列位置i
的元素設定成update
值。
7.3 原子更新引用型別
用於原子更新多個變數,提供了3種型別:
AtomicReference
:原子更新引用型別。AtomicReferenceFieldUpdater
:原子更新引用型別裡的欄位。AtomicMarkableReference
:原子更新帶有標記位的引用型別。
7.4 原子更新欄位類
AtomicIntegerFieldUpdater
:原子更新整形的欄位的更新器。AtomicLongFieldUpdater
:原子更新長整形欄位的更新器。AtomicStampedReference
:原子更新帶有版本號的引用型別。
原子地更新欄位需要兩步:
- 因為原子更新欄位類都是抽象類,每次使用的時候必須使用靜態方法
newUpdater
建立一個更新器,並且需要設定想要更新的類和屬性。 - 更新類的欄位必須使用
public volatile
來修飾。
八、Java
中的併發工具類
九、Java
中的執行緒池
執行緒池的優點:降低資源消耗,提高響應速度,提高執行緒的可管理性。
9.1 執行緒池的實現原理
執行緒池的處理流程如下:
- 判斷核心執行緒池是否已滿,如果不是,則建立一個新的工作執行緒來執行任務;如果已滿,則進入下個流程。
- 判斷工作佇列是否已滿,如果不是,則將提交的任務儲存在工作佇列裡;如果已滿,則進入下個流程。
- 判斷執行緒池的執行緒是否都處於工作狀態,如果沒有,則建立一個新的工作執行緒;如果已滿,則交給飽和策略來處理。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { //1.新增進入核心執行緒.
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { //2.新增進入佇列.
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false)) //3.新增進入非核心執行緒.
reject(command);
}
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
複製程式碼
在以上的三步中,除了加入佇列不用獲取全域性鎖以外,其它兩種情況都需要獲取,為了儘可能地避免獲取全域性鎖,在ThreadPoolExecutor
完成預熱之後(當前執行的執行緒數大於corePoolSize
),幾乎所有的execute
方法呼叫都是加入到佇列當中。
9.2 執行緒池的使用
9.2.1 執行緒池的建立
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
複製程式碼
corePoolSize
:當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其它空閒的基本執行緒能夠執行新任務也會建立。runnableTaskQueue
:用於儲存等待執行的任務的阻塞佇列,可以選擇:ArrayBlockingQueue
:基於陣列結構的有界阻塞佇列。LinkedBlockingQueue
:基於連結串列結構的阻塞佇列,吞吐量高於前者。SynchronousQueue
:不儲存元素的阻塞佇列,每個插入操作必須等待另一個執行緒呼叫了移除操作,靜態工廠方法Executors.newCachedThreadPool
使用了這個佇列。PriorityBlockingQueue
:一個具有優先順序的無限阻塞佇列。maxPoolSize
:允許建立的最大執行緒數。ThreadFactory
:用於設定建立執行緒的工廠。RejectExecutionHandler
:飽和策略。keepAliveTime
:執行緒池的工作執行緒空閒後,保持存活的時間。TimeUnit
:執行緒保持活動的單位。
9.2.2 向執行緒池提交任務
execute(Runnable runnable)
:提交不需要返回值的任務。Future<Object> future = executor.submit(haveReturnValuetask)
:用於提交需要返回值的任務,執行緒池會返回一個future
型別任務,可以用它來判斷任務是否執行成功,並且可以通過get
方法來獲取返回值,get
方法會阻塞當前執行緒直到任務完成。
9.2.3 關閉執行緒池
shutdownNow
:首先將執行緒池的狀態設為STOP
,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表。shutdown
:將執行緒池的狀態置為SHUTDOWN
,然後中斷所有沒有正在執行任務的執行緒。
十、Executor
框架
(1)在上層,Java
多執行緒程式通常把應用分解為若干個任務,然後使用使用者級的排程器(Executor
框架)將這些任務對映為固定數量的執行緒。
(2)在HotSpot VM
的執行緒模型中,Java
執行緒再被一對一對映為本地作業系統執行緒,Java
執行緒啟動時會建立一個本地作業系統執行緒,當該執行緒終止時,這個作業系統執行緒也會被回收。
(3)作業系統會排程所有執行緒並將它們分配給可用的CPU
。
Executor
框架
由三個部分組成:
- 任務,即
Runnable
介面或Callable
介面。 - 任務的執行,包括核心介面
Executor
,以及繼承自Executor
的ExecutorService
,還有它的兩個關鍵類ThreadPoolExecutor
(用來執行任務)和ScheduledThreadPoolExecutor
(可以在給定的延遲後執行命令,或者定期執行命令)。 - 非同步計算的結果,包括介面
Future
和實現類FutureTask
。
10.2 ThreadPoolExecutor
詳解
通過工具類Executors
,可以建立以下三種型別的ThreadPoolExecutor
,呼叫靜態建立方法之後,會返回ExecutorService
FixedThreadPool
可重用固定執行緒數的執行緒池;如果當前執行的執行緒數少於corePoolSize
,則建立新執行緒來執行任務;如果等於corePoolSize
,將任務加入到無界佇列LinkedBlockingQueue
當中;多餘的空閒執行緒將會被立即終止。SingleThreadPool
單個woker
執行緒的executor
;corePoolSize
和maximumPoolSize
為1;採用無界佇列作為工作佇列。CacheThreadPool
採用沒有容量的SynchronousQueue
作為執行緒池的工作佇列,其corePoolSize
為0,maximumPool
是無界的;其中的空閒執行緒最多等待60s。 如果主執行緒提交任務的速度高於maximumPool
中執行緒處理任務的速度時,CacheThreadPool
會不斷建立新執行緒,極端情況下,CacheThreadPool
會因為建立過多執行緒而耗盡CPU
資源。
10.3 ScheduledThreadPoolExecutor
詳解
用來在給定的延遲之後執行任務,或者定期執行任務,並且可以在指定的建構函式中指定多個對應的後臺執行緒數。
它採用DelayQueue
這個無界佇列作為工作佇列,其執行分為兩個部分:
- 當呼叫
ScheduledThreadPoolExecutor
的scheduleAtFixedRate()
或者scheduleWithFIxedDelay
,它會向DelayQueue
中新增ScheduledFutureTask
。 - 執行緒池中的執行緒從
DelayQueue
中獲取ScheduledFutureTask
。