Java並發編程實踐


最近閱讀了《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接口),並且該處理器至少會將異常信息記錄到日志中。

如果你希望在任務由於發生異常和失敗時獲得通知,並且執行一些特定於任務的居處操作,那么可以將任務封裝在能捕獲異常的RunnableCallable中,或者改寫ThreadPoolExecutor.afterExecute()方法。
只有通過execute()提交的任務,才能將它拋出的異常交給未捕獲異常處理器,而通過submit提交的任務的異常都被封裝在Future.get()ExecutionException中重新拋出。

JVM關閉

關閉鈎子是指通過Runtime.addShutdownHook注冊的但尚未開始的線程。JVM並不能保證關閉鈎子的調用順序。在關閉應用程序線程時,如果有(守護或非守護)線程仍然在運行,那么這些線程接下來將與關閉進程並發執行。當所有的關閉鈎子都執行結束時,如果runFinalizersOnExittrue,那么JVM將運行終結器,然后再停止。

關閉鈎子應該是線程安全的。它們在訪問共享數據時必須使用同步機制,並且小心地避免發生死鎖,這與其他並發代碼的要求相同。而且,關閉鈎子不應該對應用程序的狀態或者JVM的關閉原因做出任何假設,因此在編寫關閉鈎子的代碼時必須考慮周全。

關閉ExecutorService

ExecutorService提供了兩種關閉方法:

  • ExecutorService.shutdown():正常關閉
  • ExecutorService.shutdownNow():強行關閉
    這兩種關閉方式的差別在於各自的安全性響應性:強行關閉的速度更快,但風險也更大,因為任務很可能在執行到一半時被結束;而正常關閉雖然速度慢,但卻更安全,因為ExecutorService會一直等到隊列中的所有任務都執行完成后才關閉。在其他擁有線程的服務中也應該考慮提供類似的關閉方式以供選擇。
  1. 正常關閉
try{
    // 正常關閉
    executorService.shutdown(); 
    // 等待指定時間直到結束,超時會拋出InterruptedException異常
    executorService.awaitTermination(timeout, unit); 
}catch(InterruptedException ex){
    // do something
}
  1. 強行關閉
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中提供了四種工作隊列:

  1. ArrayBlockingQueue 基於數組的有界阻塞隊列,按FIFO排序。新任務進來后,會放到該隊列的隊尾,有界的數組可以防止資源耗盡問題。當線程池中線程數量達到corePoolSize后,再有新任務進來,則會將任務放入該隊列的隊尾,等待被調度。如果隊列已經是滿的,則創建一個新線程,如果線程數量已經達到maximumPoolSize,則會執行拒絕策略。

  2. LinkedBlockingQuene 基於鏈表的無界阻塞隊列(其實最大容量為Interger.MAX),按照FIFO排序。由於該隊列的近似無界性,當線程池中線程數量達到corePoolSize后,再有新任務進來,會一直存入該隊列,而不會去創建新線程直到maximumPoolSize,因此使用該工作隊列時,參數maximumPoolSize其實是不起作用的。

  3. SynchronousQuene 一個不緩存任務的阻塞隊列,生產者放入一個任務必須等到消費者取出這個任務。也就是說新任務進來時,不會緩存,而是直接被調度執行該任務,如果沒有可用線程,則創建新線程,如果線程數量達到maximumPoolSize,則執行拒絕策略。

  4. PriorityBlockingQueue 具有優先級的無界阻塞隊列,優先級通過參數Comparator實現。

  • threadFactory 線程工廠

創建一個新線程時使用的工廠,可以用來設定線程名是否為daemon線程Thread.UncaughtExceptionHandler等等。

  • handler 拒絕策略

當工作隊列中的任務已到達最大限制,並且線程池中的線程數量也達到最大限制,這時如果有新任務提交進來,該如何處理呢。這里的拒絕策略,就是解決這個問題的,JDK中提供了4中拒絕策略:

  1. CallerRunsPolicy 該策略下,在調用者線程中直接執行被拒絕任務的run方法,除非線程池已經shutdown,則直接拋棄任務。

  2. AbortPolicy 該策略下,直接丟棄任務,並拋出RejectedExecutionException異常。

  3. DiscardPolicy該策略下,直接丟棄任務,什么都不做。

  4. DiscardOldestPolicy 該策略下,拋棄進入隊列最早的那個任務,然后嘗試把這次拒絕的任務放入隊列

條件隊列

條件隊列使得一組線程(稱之為等待線程集合)能夠通過某種方式來等待特定的條件變成真。傳統隊列的元素是一個個數據,而與之不同的是,條件隊列中的元素是一個個正在等待相關條件的線程。

正如每個Java對象都可以作為一個鎖,每個對象同樣可以作為一個條件隊列,並且ObjectwaitnotifynotifyAll方法就構成了內部條件隊列的API。對象的內置鎖與其內部條件隊列是相互關聯的,要調用對象X中條件隊列的任何一個方法,必須持有對象X上的鎖。這就是因為“等待由狀態構成的條件”與“維護狀態一致性”這兩種機制必須被緊密地綁定在一起:只有能對狀態進行檢查時,才能在某個條件上等待,並且只有能修改狀態時,才能從條件等待中釋放一個線程。

當使用條件等待時(例如Object.waitCondition.await

  • 通常都有一個條件謂詞,包括一些對象狀態的測試,線程在執行前必須首先通過這些測試
  • 在調用wait之前測試條件謂詞,並且從wait中返回是再次進行測試
  • 在一個循環中調用wait
  • 確保使用與條件隊列相關的鎖來保護構成條件謂詞的各個狀態變量
  • 當調用waitnotifynotifyAll等方法時,一定要持有與條件隊列相關的鎖
  • 在檢查條件謂詞之后以及開始執行相應的操作之前,不要釋放鎖

降低鎖競爭程度的幾種方式

  • 減少鎖的持有時間
  • 降低鎖的請求頻率
  • 使用帶有協調機制的獨占鎖,這些機制允許更高的並發性

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,或者調用isInterruptedinterrupted)。
  • 終結器規則。對象的構造函數必須啟動在該對象的終結器之前執行完成。
  • 傳遞性。如果操作A在操作B之前執行,並且操作B在操作C之前執行,那么操作A必須在操作C之前執行。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM