Java多线程知识小抄集(三)
歡迎支持筆者新作:《深入理解Kafka:核心設(shè)計與實踐原理》和《RabbitMQ實戰(zhàn)指南》,同時歡迎關(guān)注筆者的微信公眾號:朱小廝的博客。
歡迎跳轉(zhuǎn)到本文的原文鏈接:https://honeypps.com/java/java-multiple-thread-summary-3/
本文主要整理博主遇到的Java多線程的相關(guān)知識點,適合速記,故命名為“小抄集”。本文沒有特別重點,每一項針對一個多線程知識做一個概要性總結(jié),也有一些會帶一點例子,習(xí)題方便理解和記憶。
###51. SimpleDateFormat非線程安全
當(dāng)多個線程共享一個SimpleDateFormat實例的時候,就會出現(xiàn)難以預(yù)料的異常。
主要原因是parse()方法使用calendar來生成返回的Date實例,而每次parse之前,都會把calendar里的相關(guān)屬性清除掉。問題是這個calendar是個全局變量,也就是線程共享的。因此就會出現(xiàn)一個線程剛把calendar設(shè)置好,另一個線程就把它給清空了,這時第一個線程再parse的話就會有問題了。
解決方案:1. 每次使用時創(chuàng)建一個新的SimpleDateFormat實例;2. 創(chuàng)建一個共享的SimpleDateFormat實例變量,并對這個變量進(jìn)行同步;3. 使用ThreadLocal為每個線程都創(chuàng)建一個獨享的SimpleDateFormat實例變量。
###52. CopyOnWriteArrayList
在每次修改時,都會創(chuàng)建并重新發(fā)布一個新的容器副本,從而實現(xiàn)可變現(xiàn)。CopyOnWriteArrayList的迭代器保留一個指向底層基礎(chǔ)數(shù)組的引用,這個數(shù)組當(dāng)前位于迭代器的起始位置,由于它不會被修改,因此在對其進(jìn)行同步時只需確保數(shù)組內(nèi)容的可見性。因此,多個線程可以同時對這個容器進(jìn)行迭代,而不會彼此干擾或者與修改容器的線程相互干擾。“寫時復(fù)制”容器返回的迭代器不會拋出ConcurrentModificationException并且返回的元素與迭代器創(chuàng)建時的元素完全一致,而不必考慮之后修改操作所帶來的影響。顯然,每當(dāng)修改容器時都會復(fù)制底層數(shù)組,這需要一定的開銷,特別是當(dāng)容器的規(guī)模較大時,僅當(dāng)?shù)僮鬟h(yuǎn)遠(yuǎn)多于修改操作時,才應(yīng)該使用“寫入時賦值”容器。
###53. 工作竊取算法(work-stealing)
工作竊取算法是指某個線程從其他隊列里竊取任務(wù)來執(zhí)行。在生產(chǎn)-消費者設(shè)計中,所有消費者有一個共享的工作隊列,而在work-stealing設(shè)計中,每個消費者都有各自的雙端隊列,如果一個消費者完成了自己雙端隊列中的全部任務(wù),那么它可以從其他消費者雙端隊列末尾秘密地獲取工作。
優(yōu)點:充分利用線程進(jìn)行并行計算,減少了線程間的競爭。
缺點:在某些情況下還是存在競爭,比如雙端隊列(Deque)里只有一個任務(wù)時。并且該算法會消耗了更多的系統(tǒng)資源,比如創(chuàng)建多個線程和多個雙端隊列。
###54. Future & FutureTask
FutureTask表示的計算是通過Callable來實現(xiàn)的,相當(dāng)于一種可生產(chǎn)結(jié)果的Runnable,并且可以處于一下3種狀態(tài):等待運行,正在運行和運行完成。運行表示計算的所有可能結(jié)束方式,包括正常結(jié)束、由于取消而結(jié)束和由于異常而結(jié)束等。當(dāng)FutureTask進(jìn)入完成狀態(tài)后,它會永遠(yuǎn)停止在這個狀態(tài)上。Future.get的行為取決于任務(wù)的狀態(tài),如果任務(wù)已經(jīng)完成,那么get會立刻返回結(jié)果,否則get將阻塞知道任務(wù)進(jìn)入完成狀態(tài),然后返回結(jié)果或者異常。FutureTask的使用方式如下:
運行結(jié)果:yes no
Callable表示的任務(wù)可以拋出受檢查或未受檢查的異常,并且任何代碼都可能拋出一個Error.無論任務(wù)代碼拋出什么異常,都會被封裝到一個ExecutionException中,并在Future.get中被重新拋出。
###55. Executors
newFixedThreadPool:創(chuàng)建一個固定長度的線程池,每當(dāng)提交一個任務(wù)時就創(chuàng)建一個線程,直到達(dá)到線程池的最大數(shù)量,這時線程池的規(guī)模將不再變化(如果某個線程由于發(fā)生了未預(yù)期的Exception而結(jié)束,那么線程池會補(bǔ)充一個新的線程)。(LinkedBlockingQueue)
newCachedThreadPool:創(chuàng)建一個可換成的線程池,如果線程池的當(dāng)前規(guī)模超過了處理需求時,那么將回收空閑的線程,而當(dāng)需求增加時,則可以添加新的線程,線程池的規(guī)模不存在任何限制。(SynchronousQueue)
newSingleThreadExecutor:是一個單線程的Executor,它創(chuàng)建單個工作者線程來執(zhí)行任務(wù),如果這個線程異常結(jié)束,會創(chuàng)建另一個線程來替代。能確保一組任務(wù)在隊列中的順序來串行執(zhí)行。(LinkedBlockingQueue)
newScheduledThreadPool:創(chuàng)建了一個固定長度的線程池,而且以延遲或者定時的方式來執(zhí)行任務(wù),類似于Timer。
###56. ScheduledThreadPoolExecutor替代Timer
由第17項可知Timer有兩個缺陷,在JDK5開始就很少使用Timer了,取而代之的可以使用ScheduledThreadPoolExecutor。使用實例如下:
運行結(jié)果:1 Callable
###57. Callable & Runnable
Executor框架使用Runnable作為基本的任務(wù)表示形式。Runnable是一種有很大局限的抽象,雖然run能寫入到日志文件或者將結(jié)果放入某個共享的數(shù)據(jù)結(jié)構(gòu),但它不能返回一個值或拋出一個受檢查的異常。
許多任務(wù)實際上都是存在延遲的計算——執(zhí)行數(shù)據(jù)庫查詢,從網(wǎng)絡(luò)上獲取資源,或者計算某個復(fù)雜的功能。對于這些任務(wù),Callable是一種更好的抽象:它認(rèn)為主入口點(call())將返回一個值,并可能拋出一個異常。
Runnable和Callable描述的都是抽象的計算任務(wù)。這些任務(wù)通常是有范圍的,即都有一個明確的起始點,并且最終會結(jié)束。
###58. CompletionService
如果想Executor提交了一組計算任務(wù),并且希望在計算完成后獲得結(jié)果,那么可以保留與每個任務(wù)關(guān)聯(lián)的Future,然后反復(fù)使用get方法,同事將參數(shù)timeout指定為0,從而通過輪詢來判斷任務(wù)是否完成。這種方法雖然可行,但卻有些繁瑣。幸運的是,還有一種更好的方法:CompletionService。CompletionService將Executor和BlockingQueue的功能融合在一起。你可以將Callable任務(wù)提交給它來執(zhí)行,然后使用類似于隊列操作的take和poll等方法來獲得已完成的結(jié)果,而這些結(jié)果會在完成時被封裝為Future。ExecutorCompletionService實現(xiàn)了CompletionService,并將計算部分委托到一個Executor。代碼示例如下:
運行結(jié)果:
pool-1-thread-1 pool-1-thread-2 pool-1-thread-3 pool-1-thread-4可以通過ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)構(gòu)造函數(shù)指定特定的BlockingQueue(如下代碼剪輯),默認(rèn)為LinkedBlockingQueue。
BlockingQueue<Future<Object>> bq = new LinkedBlockingQueue<Future<Object>>();CompletionService<Object> completionService = new ExecutorCompletionService<Object>(executor,bq);ExecutorCompletionService的JDK源碼只有100行左右,有興趣的朋友可以看看。
###59. 通過Future來實現(xiàn)取消
ExecutorService.submit將返回一個Future來描述任務(wù)。Future擁有一個cancel方法,該方法帶有一個boolean類型的參數(shù)mayInterruptIfRunning,表示取消操作是否成功。如果mayInterruptIfRunning為true并且任務(wù)當(dāng)前正在某個線程運行,那么這個線程能被中斷。如果這個參數(shù)為false,那么意味著“若任務(wù)還沒啟動,就不要運行它”,這種方式應(yīng)該用于那些不處理中斷的任務(wù)中。當(dāng)Future.get拋出InterruptedException或TimeoutException時,如果你知道不再需要結(jié)果,那么就可以調(diào)用Futuure.cancel來取消任務(wù)。
###60. 處理不可中斷的阻塞
對于一下幾種情況,中斷請求只能設(shè)置線程的中斷狀態(tài),除此之外沒有其他任何作用。
- Java.io包中的同步Socket I/O:雖然InputStream和OutputStream中的read和write等方法都不會響應(yīng)中斷,但通過關(guān)閉底層的套接字,可以使得由于執(zhí)行read或write等方法而被阻塞的線程拋出一個SocketException。
- Java.io包中的同步I/O:當(dāng)中斷一個在InterruptibleChannel上等待的線程時會拋出ClosedByInterrptException并關(guān)閉鏈路。當(dāng)關(guān)閉一個InterruptibleChannel時,將導(dǎo)致所有在鏈路操作上阻塞的線程都拋出AsynchronousCloseException。
- Selector的異步I/O:如果一個線程在調(diào)用Selector.select方法時阻塞了,那么調(diào)用close或wakeup方法會使線程拋出ClosedSelectorException并提前返回。
- 獲得某個鎖:如果一個線程由于等待某個內(nèi)置鎖而阻塞,那么將無法響應(yīng)中斷,因為線程認(rèn)為它肯定會獲得鎖,所以將不會理會中斷請求,但是在Lock類中提供了lockInterruptibly方法,該方法允許在等待一個鎖的同時仍能響應(yīng)中斷。
###61. 關(guān)閉鉤子
JVM既可以正常關(guān)閉也可以強(qiáng)制關(guān)閉,或者說非正常關(guān)閉。關(guān)閉鉤子可以在JVM關(guān)閉時執(zhí)行一些特定的操作,譬如可以用于實現(xiàn)服務(wù)或應(yīng)用程序的清理工作。關(guān)閉鉤子可以在一下幾種場景中應(yīng)用:1. 程序正常退出(這里指一個JVM實例);2.使用System.exit();3.終端使用Ctrl+C觸發(fā)的中斷;4. 系統(tǒng)關(guān)閉;5. OutOfMemory宕機(jī);6.使用Kill pid命令干掉進(jìn)程(注:在使用kill -9 pid時,是不會被調(diào)用的)。使用方法(Runtime.getRuntime().addShutdownHook(Thread hook))。更多內(nèi)容可以參考JAVA虛擬機(jī)關(guān)閉鉤子(Shutdown Hook)
###62. 終結(jié)器finalize
終結(jié)器finalize:在回收器釋放它們后,調(diào)用它們的finalize方法,從而保證一些持久化的資源被釋放。在大多數(shù)情況下,通過使用finally代碼塊和顯示的close方法,能夠比使用終結(jié)器更好地管理資源。唯一例外情況在于:當(dāng)需要管理對象,并且該對象持有的資源是通過本地方法獲得的。但是基于一些原因(譬如對象復(fù)活),我們要盡量避免編寫或者使用包含終結(jié)器的類。
###63. 線程工廠ThreadFactory
每當(dāng)線程池(ThreadPoolExecutor)需要創(chuàng)建一個線程時,都是通過線程功夫方法來完成的。默認(rèn)的線程工廠方法將創(chuàng)建一個新的、非守護(hù)的線程,并且不包含特殊的配置信息。通過指定一個線程工廠方法,可以定制線程池的配置信息。在ThreadFactory中只定義了一個方法newThread,每當(dāng)線程池需要創(chuàng)建一個新線程時都會調(diào)用這個方法。默認(rèn)的線程工廠(DefaultThreadFactory 是Executors的內(nèi)部類)如下:
通過implements ThreadFactory可以定制線程工廠。譬如,你希望為線程池中的線程指定一個UncaughtExceptionHandler,或者實例化一個定制的Thread類用于執(zhí)行調(diào)試信息的記錄。
###64. synchronized與ReentrantLock之間進(jìn)行選擇
由第21條可知ReentrantLock與synchronized想必提供了許多功能:定時的鎖等待,可中斷的鎖等待、公平鎖、非阻塞的獲取鎖等,而且從性能上來說ReentrantLock比synchronized略有勝出(JDK6起),在JDK5中是遠(yuǎn)遠(yuǎn)勝出,為嘛不放棄synchronized呢?ReentrantLock的危險性要比同步機(jī)制高,如果忘記在finnally塊中調(diào)用unlock,那么雖然代碼表面上能正常運行,但實際上已經(jīng)埋下了一顆定時炸彈,并很可能傷及其他代碼。僅當(dāng)內(nèi)置鎖不能滿足需求時,才可以考慮使用ReentrantLock.
###65. Happens-Before規(guī)則
程序順序規(guī)則:如果程序中操作A在操作B之前,那么在線程中A操作將在B操作之前。
監(jiān)視器鎖規(guī)則:一個unlock操作現(xiàn)行發(fā)生于后面對同一個鎖的lock操作。
volatile變量規(guī)則:對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的“后面”同樣是指時間上的先后順序。
線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
線程終止規(guī)則:線程的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等于段檢測到線程已經(jīng)終止執(zhí)行。
線程中斷規(guī)則:線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生。
終結(jié)器規(guī)則:對象的構(gòu)造函數(shù)必須在啟動該對象的終結(jié)器之前執(zhí)行完成。
傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
注意:如果兩個操作之間存在happens-before關(guān)系,并不意味著java平臺的具體實現(xiàn)必須要按照Happens-Before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法。
###66. as-if-serial
不管怎么重排序,程序執(zhí)行結(jié)果不能被改變。
###67. ABA問題
ABA問題發(fā)生在類似這樣的場景:線程1轉(zhuǎn)變使用CAS將變量A的值替換為C,在此時,線程2將變量的值由A替換為C,又由C替換為A,然后線程1執(zhí)行CAS時發(fā)現(xiàn)變量的值仍為A,所以CAS成功。但實際上這時的現(xiàn)場已經(jīng)和最初的不同了。大多數(shù)情況下ABA問題不會產(chǎn)生什么影響。如果有特殊情況下由于ABA問題導(dǎo)致,可用采用AtomicStampedReference來解決,原理:樂觀鎖+version。可以參考下面的案例來了解其中的不同。
輸出結(jié)果:true false
持續(xù)更新中~
博主嘔心瀝血整理發(fā)布,跪求一贊。
wanna more?
Java多線程知識小抄集(一)
Java多線程知識小抄集(二)
參考資料
歡迎跳轉(zhuǎn)到本文的原文鏈接:https://honeypps.com/java/java-multiple-thread-summary-3/
歡迎支持筆者新作:《深入理解Kafka:核心設(shè)計與實踐原理》和《RabbitMQ實戰(zhàn)指南》,同時歡迎關(guān)注筆者的微信公眾號:朱小廝的博客。
超強(qiáng)干貨來襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生
總結(jié)
以上是生活随笔為你收集整理的Java多线程知识小抄集(三)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java多线程知识小抄集(二)
- 下一篇: Java虚拟机结构分析