最近閱讀了《Java并發編程實踐》這本書,總結了一下幾個相關的知識點,
執行緒安全
當多個執行緒訪問某個類時,不管運行時環境采用何種調度方式或者這些執行緒將如何交替執行,并且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那么就稱這個類是執行緒安全的,可以通過原子性、一致性、不可變物件、執行緒安全的物件和加鎖保護同時被多個執行緒訪問的可變狀態變數來解決執行緒安全的問題,
可見性
在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整,在缺乏足夠同步的多執行緒程式中,要想對記憶體操作的執行順序進行判斷,幾乎無法得出正確的結論,加鎖的含義不僅僅局限于互斥行為,還包括記憶體可見性,為了確保所有執行緒都能看到共享變數的最新值,所有執行讀寫操作的執行緒都必須持有同一把鎖,volatile變數不會被快取在暫存器或者對其他處理器不可見的地方,因此在讀取volatile型別的變數時總會回傳最新寫入的值,volatile變數是一種比synchronized關鍵字更輕量級的同步機制,加鎖機制即可以確保可見性又可以確保原子性,而volatile變數只能確保可見性,
發布逸出
當從物件的建構式中發布物件時,只是發布了一個尚未構造完成的物件,即使發布物件的陳述句位于建構式的最后一行也是如此,如果this參考在建構式中逸出,那么這種現象就被認為是不正確構造,常見的逸出有,在建構式中創建并啟動一個執行緒、內部私有可變狀態逸出等,
要安全地發布一個物件,物件的參考以及物件的狀態必須同時對其他執行緒可見,一個正確構造的物件可以通過一下方式來安全地發布:
- 在靜態初始化函式中初始化一個物件參考
- 將物件的參考保存到
volatile型別的域或者AtomicReference物件中 - 將物件的參考保存到某個正確構造物件的final型別域中
- 將物件的參考保存到一個由鎖保護的域中
物件的發布需求取決于它的可變性:
- 不可變物件可以通過任意機制來發布
- 事實不可變物件必須通過安全方式來發布
- 可變物件必須通過安全方式發布,并且必須是執行緒安全的或者由某個鎖保護起來
千萬不要在A執行緒中創建物件,在B執行緒中使用該物件,在物件初始化的時候,首先會去申請一個記憶體空間,然后給物件中的屬性賦默認值(如:int型別的變數默認值為0等),再通過建構式或者代碼塊對屬性進行賦值,最后地址空間指向的物件才算是創建完成了(當然還有很多其他的步驟,這里只是簡單說明一下),這樣很有可能出現B執行緒獲取到的物件是不完整的,因為Java執行緒模型的和物件的可見性的原因,
執行緒中斷
呼叫Thread.interrupt()并不意味著立即停止目標執行緒正在進行的作業,而只是傳遞了請求中斷的訊息,
對中斷操作的正確理解是:它并不是真正地中斷一個正在運行的執行緒,而只是發出中斷請求,然后由執行緒在下一個合適的時刻中斷自己,(這些時刻也被稱為取消點),有些方法,例如:Object.wait()、Thread.sleep()和Thread.join()等,將嚴格地處理這種請求,當它們收到中斷請求或者在開始執行時發現某個已被設定好的中斷狀態時,將拋出一個例外,
在使用靜態的interrupted時應該小心,因為它會清除當前執行緒的中斷狀態,如果在呼叫interrupted時回傳了true,那么除非你想屏蔽這個中斷,否則必須對它進行處理—可以拋出InterruptedException,或者通過再次呼叫interrupt()來恢復中斷狀態,Future.cancel()方法可以取消執行緒,
通常,中斷是實作取消的最合理方式,
未捕獲的例外
在運行時間較長的應用程式中,通常會為所有執行緒的未捕獲例外指定同一個例外處理器(實作Thread.UncaughtExceptionHandler介面),并且該處理器至少會將例外資訊記錄到日志中,
如果你希望在任務由于發生例外和失敗時獲得通知,并且執行一些特定于任務的居處操作,那么可以將任務封裝在能捕獲例外的Runnable或Callable中,或者改寫ThreadPoolExecutor.afterExecute()方法,
只有通過execute()提交的任務,才能將它拋出的例外交給未捕獲例外處理器,而通過submit提交的任務的例外都被封裝在Future.get()的ExecutionException中重新拋出,
JVM關閉
關閉鉤子是指通過Runtime.addShutdownHook注冊的但尚未開始的執行緒,JVM并不能保證關閉鉤子的呼叫順序,在關閉應用程式執行緒時,如果有(守護或非守護)執行緒仍然在運行,那么這些執行緒接下來將與關閉行程并發執行,當所有的關閉鉤子都執行結束時,如果runFinalizersOnExit為true,那么JVM將運行終結器,然后再停止,
關閉鉤子應該是執行緒安全的,它們在訪問共享資料時必須使用同步機制,并且小心地避免發生死鎖,這與其他并發代碼的要求相同,而且,關閉鉤子不應該對應用程式的狀態或者JVM的關閉原因做出任何假設,因此在撰寫關閉鉤子的代碼時必須考慮周全,
關閉ExecutorService
ExecutorService提供了兩種關閉方法:
ExecutorService.shutdown():正常關閉ExecutorService.shutdownNow():強行關閉
這兩種關閉方式的差別在于各自的安全性和回應性:強行關閉的速度更快,但風險也更大,因為任務很可能在執行到一半時被結束;而正常關閉雖然速度慢,但卻更安全,因為ExecutorService會一直等到佇列中的所有任務都執行完成后才關閉,在其他擁有執行緒的服務中也應該考慮提供類似的關閉方式以供選擇,
- 正常關閉
try{
// 正常關閉
executorService.shutdown();
// 等待指定時間直到結束,超時會拋出InterruptedException例外
executorService.awaitTermination(timeout, unit);
}catch(InterruptedException ex){
// do something
}
- 強行關閉
try{
// 強行關閉
List<Runnable> unfinishedTasks = executorService.shutdownNow();
// 處理未完成的任務
handle(unfinishedTasks);
// 等待指定時間直到結束,超時會拋出InterruptedException例外
executorService.awaitTermination(timeout, unit);
}catch(InterruptedException ex){
// do something
}
資源釋放
| 呼叫的方法 | 鎖 | CPU |
|---|---|---|
Thread.sleep() |
不釋放 | 釋放 |
Thread.join() |
不釋放 | 釋放 |
Thread.yield() |
不釋放 | 釋放 |
Object.wait() |
釋放 | 釋放 |
Condition.await() |
釋放 | 釋放 |
ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 執行緒池核心執行緒大小
在創建了執行緒池后,默認情況下,執行緒池中并沒有任何執行緒,而是等待有任務到來才創建執行緒去執行任務,(除非呼叫了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建執行緒的意思,即在沒有任務到來之前就創建corePoolSize個執行緒或者一個執行緒),
默認情況下,在創建了執行緒池后,執行緒池中的執行緒數為0,當有任務來之后,就會創建一個執行緒去執行任務,當執行緒池中的執行緒數目達到corePoolSize后,就會把到達的任務放到快取佇列當中,核心執行緒在allowCoreThreadTimeout被設定為true時會超時退出,默認情況下不會退出,
- maximumPoolSize 執行緒池最大執行緒數
當執行緒數大于或等于核心執行緒,且任務佇列已滿時,執行緒池會創建新的執行緒,直到執行緒數量達到maximumPoolSize,如果執行緒數已等于maximumPoolSize,且任務佇列已滿,則已超出執行緒池的處理能力,執行緒池會拒絕處理任務而拋出例外,
- keepAliveTime 空閑執行緒存活時間
當執行緒空閑時間達到keepAliveTime,該執行緒會退出,直到執行緒數量等于corePoolSize,如果allowCoreThreadTimeout設定為true,則所有執行緒均會退出直到執行緒數量為0,
- unit 空間執行緒存活時間單位
keepAliveTime的計量單位
- workQueue 作業佇列
新任務被提交后,會先進入到此作業佇列中,任務調度時再從佇列中取出任務,JDK中提供了四種作業佇列:
-
ArrayBlockingQueue基于陣列的有界阻塞佇列,按FIFO排序,新任務進來后,會放到該佇列的隊尾,有界的陣列可以防止資源耗盡問題,當執行緒池中執行緒數量達到corePoolSize后,再有新任務進來,則會將任務放入該佇列的隊尾,等待被調度,如果佇列已經是滿的,則創建一個新執行緒,如果執行緒數量已經達到maximumPoolSize,則會執行拒絕策略, -
LinkedBlockingQuene基于鏈表的無界阻塞佇列(其實最大容量為Interger.MAX),按照FIFO排序,由于該佇列的近似無界性,當執行緒池中執行緒數量達到corePoolSize后,再有新任務進來,會一直存入該佇列,而不會去創建新執行緒直到maximumPoolSize,因此使用該作業佇列時,引數maximumPoolSize其實是不起作用的, -
SynchronousQuene一個不快取任務的阻塞佇列,生產者放入一個任務必須等到消費者取出這個任務,也就是說新任務進來時,不會快取,而是直接被調度執行該任務,如果沒有可用執行緒,則創建新執行緒,如果執行緒數量達到maximumPoolSize,則執行拒絕策略, -
PriorityBlockingQueue具有優先級的無界阻塞佇列,優先級通過引數Comparator實作,
- threadFactory 執行緒工廠
創建一個新執行緒時使用的工廠,可以用來設定執行緒名、是否為daemon執行緒、Thread.UncaughtExceptionHandler等等,
- handler 拒絕策略
當作業佇列中的任務已到達最大限制,并且執行緒池中的執行緒數量也達到最大限制,這時如果有新任務提交進來,該如何處理呢,這里的拒絕策略,就是解決這個問題的,JDK中提供了4中拒絕策略:
-
CallerRunsPolicy該策略下,在呼叫者執行緒中直接執行被拒絕任務的run方法,除非執行緒池已經shutdown,則直接拋棄任務, -
AbortPolicy該策略下,直接丟棄任務,并拋出RejectedExecutionException例外, -
DiscardPolicy該策略下,直接丟棄任務,什么都不做, -
DiscardOldestPolicy該策略下,拋棄進入佇列最早的那個任務,然后嘗試把這次拒絕的任務放入佇列
條件佇列
條件佇列使得一組執行緒(稱之為等待執行緒集合)能夠通過某種方式來等待特定的條件變成真,傳統佇列的元素是一個個資料,而與之不同的是,條件佇列中的元素是一個個正在等待相關條件的執行緒,
正如每個Java物件都可以作為一個鎖,每個物件同樣可以作為一個條件佇列,并且Object中wait、notify和notifyAll方法就構成了內部條件佇列的API,物件的內置鎖與其內部條件佇列是相互關聯的,要呼叫物件X中條件佇列的任何一個方法,必須持有物件X上的鎖,這就是因為“等待由狀態構成的條件”與“維護狀態一致性”這兩種機制必須被緊密地系結在一起:只有能對狀態進行檢查時,才能在某個條件上等待,并且只有能修改狀態時,才能從條件等待中釋放一個執行緒,
當使用條件等待時(例如Object.wait和Condition.await)
- 通常都有一個條件謂詞,包括一些物件狀態的測驗,執行緒在執行前必須首先通過這些測驗
- 在呼叫
wait之前測驗條件謂詞,并且從wait中回傳是再次進行測驗 - 在一個回圈中呼叫
wait - 確保使用與條件佇列相關的鎖來保護構成條件謂詞的各個狀態變數
- 當呼叫
wait、notify或notifyAll等方法時,一定要持有與條件佇列相關的鎖 - 在檢查條件謂詞之后以及開始執行相應的操作之前,不要釋放鎖
降低鎖競爭程度的幾種方式
- 減少鎖的持有時間
- 降低鎖的請求頻率
- 使用帶有協調機制的獨占鎖,這些機制允許更高的并發性
CAS操作
CAS包含3個運算元:需要讀寫的記憶體位置V、進行比較的值A和擬寫入的新值B,當且僅當V的值等于A時,CAS才會通過原子方式用新值B來更新V的值,否則不會執行任何操作,無論位置V的值是否等于A,都將回傳V原有的值,
CAS的主要缺點是:它將使呼叫者處理競爭問題(通過重試、回退、放棄),而在鎖中能自動處理競爭問題,同時CAS還會出現ABA的問題,
Java記憶體模型(JMM)
在共享記憶體的多處理器體系架構中,每個處理器都擁有自己的快取,并且定期地與主記憶體進行協調,在不同的處理器架構中提供了不同級別的快取一致性(Cache Coherence),其中一部分只提供最小的保證,即允許不同的處理器在任意時刻從同一個存盤位置上看到不同的值,作業系統、編譯器以及運行時(有時甚至包括應用程式)需要彌合這種硬體能力與執行緒安全需求之間的差異,
Java記憶體模型是通過各種操作來定義的,包括對變數的讀寫操作,監視器的加鎖和釋放操作,以及執行緒啟動和合并操作,JMM為程式中所有的操作定義了一個偏序關系,稱之為Happens-Before,如果兩個操作之間缺乏Happens-Before關系,那么JVM可以對它們任意的重排序,
當一個變數被多個執行緒讀取并且至少被一個執行緒寫入時,如果在讀操作和寫操作之間沒有依照Happens-Before來排序,那么就會產生資料競爭的問題,在正確同步的程式中不存在資料競爭,并會表現出串行一致性,這意味著程式中的所有操作都會按照一種固定的和全域的順序執行,
Happens-Before的規則包括:
- 程式順序規則,如果程式中操作A在操作B之前,那么在執行緒中A操作將在B操作之前執行,
- 監視器鎖規則,在監視器鎖上的解鎖操作必須在同一個監視器鎖上的加鎖操作之前執行,
- volatile變數規則, 對volatile變數的寫入操作必須在對該變數的讀操作之前執行,
- 執行緒啟動規則,在執行緒上對
Thread.start()的呼叫必須在該執行緒中執行任何操作之前執行, - 執行緒結束規則,執行緒中的任何操作都必須在其他執行緒檢測到該執行緒已經結束之前執行,或者從
Thread.join()中成功回傳,或者在呼叫Thread.isAlive()時回傳false, - 中斷規則,當一個執行緒在另一個執行緒上呼叫
interrupt時,必須在被中斷執行緒檢測到interrupt呼叫之前執行(通過拋出InterruptedException,或者呼叫isInterrupted和interrupted), - 終結器規則,物件的建構式必須啟動在該物件的終結器之前執行完成,
- 傳遞性,如果操作A在操作B之前執行,并且操作B在操作C之前執行,那么操作A必須在操作C之前執行,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/145063.html
標籤:Java
上一篇:流量轉發映射
