Java 7 并发编程指南
原文是發表在并發編程網上翻譯后的 《Java 7 并發編程指南》,這里對其中的目錄做個更加詳細的描述,并且寫出了重點說明,方便日后快速查閱。建議仔細查看每節的代碼實現,非常具有參考價值??梢灾苯狱c擊標題,查看原文。
原文目錄地址:http://ifeve.com/java-7-concurrency-cookbook/
代碼實現:https://github.com/Wang-Jun-Chao/java-concurrency
目錄
前言
第一章: 線程管理
介紹
本節主要介紹線程基本的操作。
線程的創建和運行
在Java中,我們有2個方式創建線程:
- 通過直接繼承thread類,然后覆蓋run()方法。
- 構建一個實現Runnable接口的類, 然后創建一個thread類對象并傳遞Runnable對象作為構造參數
之后調用這個線程對象的start()對象即可運行。
獲取和設置線程信息
Thread類的對象中保存了一些屬性信息能夠幫助我們來辨別每一個線程,知道它的狀態,調整控制其優先級。 這些屬性是:
- ID: 每個線程的獨特標識。
- Name: 線程的名稱。
- Priority: 線程對象的優先級。優先級別在1-10之間,1是最低級,10是最高級。不建議改變它們的優先級,但是你想的話也是可以的。
- Status: 線程的狀態。在Java中,線程只能有這6種中的一種狀態: new, runnable, blocked, waiting, time waiting, 或 terminated.
線程的中斷
Java提供中斷機制來通知線程表明我們想要結束它。中斷機制的特性是線程需要檢查是否被中斷,而且還可以決定是否響應結束的請求。所以,線程可以忽略中斷請求并且繼續運行。如果需要中斷線程,只需要調用線程對象的interrupt()方法即可,線程通過isInterrupted()方法來檢測自己是否被中斷。比如,你可以像例子中一樣,輪詢檢測自己是否被中斷,如果被中斷了就直接return。
操作線程的中斷機制
我們可以選擇使用拋出InterruptedException異常的方式來停止線程。通常它可以由并發API來拋出比如sleep()方法
線程的睡眠和恢復
當你調用sleep()方法, Thread 離開CPU并在一段時間內停止運行。在這段時間內,它是不消耗CPU時間的,使得CPU可以執行其他任務。
當 Thread 是睡眠并且處于中斷狀態(比如調用了線程的interrupt()方法)的時候,sleep方法會立刻拋出InterruptedException異常并不會一直等到睡眠時間過去。
Java 并發 API 有另一種方法能讓線程對象離開 CPU。它是 yield() 方法, 它向JVM表示線程對象可以讓CPU執行其他任務。JVM 不保證它會遵守請求。通常,它只是用來試調的。
等待線程的終結
當線程調用某個其他線程的join()方法時,它會暫停當前線程,直到被調用join()方法的其他線程執行完成。
守護線程的創建和運行
調用線程對象的setDaemon(true);來使其成為一個守護線程。
處理線程的不受控制異常
Java里有2種異常:
- 檢查異常(Checked exceptions): 這些異常必須強制捕獲它們或在一個方法里的throws子句中。 例如, IOException 或者ClassNotFoundException。
- 未檢查異常(Unchecked exceptions): 這些異常不用強制捕獲它們。例如, NumberFormatException。當一個非檢查異常被拋出,默認的行為是在控制臺寫下stack trace并退出程序。
我們可以使用線程對象的setUncaughtExceptionHandler方法來設置未檢測異常的回調函數。這個回調函數以Thread 對象和 Exception 作為參數。
此外調用Thread.setDefaultUncaughtExceptionHandler()可以為應用里的所有線程對象建立異常 handler 。
當一個未捕捉的異常在線程里被拋出,JVM會尋找此異常的3種可能潛在的處理者(handler)。首先, 它尋找這個未捕捉的線程對象的異常handle,如我們在在這個指南中學習的。如果這個handle 不存在,那么JVM會在線程對象的ThreadGroup里尋找非捕捉異常的handler,如在處理線程組內的不受控制異常里介紹的那樣(下面的第12節)。如果此方法也不存在,那么 JVM 會尋找默認非捕捉異常handle,就是調用Thread.setDefaultUncaughtExceptionHandler()設置的那個handle。如果沒有一個handler存在, 那么 JVM會把異常的 stack trace 寫入操控臺并結束任務。
使用本地線程變量
使用ThreadLocal來存儲線程局部變量。它的使用類似于這種:
線程組
Java 提供 ThreadGroup 類來組織線程。 ThreadGroup 對象可以由 Thread 對象組成和由另外的 ThreadGroup 對象組成,生成線程樹結構。
ThreadGroup 類儲存線程對象和其他有關聯的 ThreadGroup 對象,所以它可以訪問他們的所有信息 (例如,狀態) 和全部成員的操作表現 (例如,中斷)。
處理線程組內的不受控制異常
重寫ThreadGroup的uncaughtException方法來為線程組設置未捕獲異常的回調方法
用線程工廠創建線程
實現ThreadFactory接口來使用線程工廠創建線程,你需要知道使用線程工廠創建線程有什么優勢,一般來說它具有下面這些優勢:
- 更簡單的改變了類的對象創建或者說創建這些對象的方式。
- 更簡單的為了限制的資源限制了對象的創建。 例如, 我們只new一個此類型的對象。
- 更簡單的生成創建對象的統計數據。
第二章 : 基本線程同步
介紹
在并發應用程序中,多個線程讀或寫相同的數據或訪問同一文件或數據庫連接這是正常的。這些共享資源會引發錯誤或數據不一致的情況,我們必須通過一些機制來避免這些錯誤。
同步方法
synchronized關鍵字基本使用
在同步的類里安排獨立屬性
當你使用synchronized關鍵字來保護代碼塊時,你必須通過一個對象的引用作為參數。通常,你將會使用this關鍵字來引用執行該方法的對象,但是你也可以使用其他對象引用。通常情況下,這些對象被創建只有這個目的。比如,你在一個類中有被多個線程共享的兩個獨立屬性。你必須同步訪問每個變量,如果有一個線程訪問一個屬性和另一個線程在同一時刻訪問另一個屬性,這是沒有問題的。
在同步代碼中使用條件
涉及wait(),notify(),和notifyAll() 方法。
一個線程可以在synchronized代碼塊中調用wait()方法。如果在synchronized代碼塊外部調用wait()方法,JVM會拋出IllegalMonitorStateException異常。當線程調用wait()方法,JVM讓這個線程睡眠,并且釋放控制 synchronized代碼塊的對象,這樣,雖然它正在執行但允許其他線程執行由該對象保護的其他synchronized代碼塊。為了喚醒線程,你必 須在由相同對象保護的synchronized代碼塊中調用notify()或notifyAll()方法。
通常我們會將wait()寫在一個while()循環中,以在喚醒之后再次檢測是否滿足條件,滿足則繼續執行,否則再次進入wait(),因為wait()有的時候會被錯誤的喚醒。。
使用Lock來同步代碼塊
Java提供另外的機制用來同步代碼塊。它比synchronized關鍵字更加強大、靈活。它是基于Lock接口和實現它的類(如ReentrantLock)。這種機制有如下優勢:
- 它允許以一種更靈活的方式來構建synchronized塊。使用synchronized關鍵字,你必須以結構化方式得到釋放synchronized代碼塊的控制權。Lock接口允許你獲得更復雜的結構來實現你的臨界區。
- Lock 接口比synchronized關鍵字提供更多額外的功能。新功能之一是實現的tryLock()方法。這種方法試圖獲取鎖的控制權并且如果它不能獲取該鎖是因為其他線程在使用這個鎖,它將返回這個鎖。使用synchronized關鍵字的時候,當線程A試圖執行synchronized代碼塊,如果線程B正在執行它,那么線程A將阻塞直到線程B執行完synchronized代碼塊。而使用鎖,你可以執行tryLock()方法,這個方法返回一個 Boolean值表示是否有其他線程正在運行這個鎖所保護的代碼。
- 當有多個讀者和一個寫者時,Lock接口允許讀寫操作分離。
- Lock接口比synchronized關鍵字提供更好的性能。
使用讀/寫鎖來同步數據訪問
鎖所提供的最重要的改進之一就是ReadWriteLock接口和唯一 一個實現它的ReentrantReadWriteLock類。這個類提供兩把鎖,一把用于讀操作和一把用于寫操作。同時可以有多個線程執行讀操作。當一個線程正在執行一個寫操作,不可能有任何線程執行讀操作。當一個線程正在執行一個讀操作,也不可能有任何線程執行寫操作??偨Y來說就是:讀讀不互斥,讀寫,寫寫都是互斥的。
修改Lock的公平性
ReentrantLock類和 ReentrantReadWriteLock類的構造器中,允許一個名為fair的boolean類型參數,它允許你來控制這些類的行為。默認值為 false,這將啟用非公平模式。在這個模式中,當有多個線程正在等待一把鎖(ReentrantLock或者 ReentrantReadWriteLock),這個鎖必須選擇它們中間的一個來獲得進入臨界區,選擇任意一個是沒有任何標準的。true值將開啟公平 模式。在這個模式中,當有多個線程正在等待一把鎖(ReentrantLock或者ReentrantReadWriteLock),這個鎖必須選擇它們 中間的一個來獲得進入臨界區,它將選擇等待時間最長的線程。
在Lock中使用多條件
涉及await()、signal()和signallAll();
這有點像前面的第4節在同步代碼中使用條件。但是await()、signal()和signallAll()是配合Lock來進行使用的。而wait(),notify(),和notifyAll() 是配合synchronized來使用的。并且前者可以實現鎖上的多個條件的等待??梢圆榭丛撜鹿澥纠a中的Buffer類來查看如何使用。
第三章: 線程同步工具
介紹
在第二章,基本的線程同步中,我們學會了以下2個同步機制:
- 關鍵詞同步(synchronized)
- Lock接口和它的實現類們:ReentrantLock, ReentrantReadWriteLock.ReadLock, 和 ReentrantReadWriteLock.WriteLock
在此章節,我們將學習怎樣使用高等級的機制來達到多線程的同步。這些高等級機制有:
- Semaphores: 控制訪問多個共享資源的計數器。此機制是并發編程的最基本的工具之一,而且大部分編程語言都會提供此機制。
- CountDownLatch: CountDownLatch 類是Java語言提供的一個機制,它允許線程等待多個操作的完結。
- CyclicBarrier: CyclicBarrier 類是又一個java語言提供的機制,它允許多個線程在同一個點同步。
- Phaser: Phaser類是又一個java語言提供的機制,它控制并發任務分成段落來執行。全部的線程在繼續執行下一個段之前必須等到之前的段執行結束。這是Java 7 API的一個新特性。
- Exchanger: Exchanger類也是java語言提供的又一個機制,它提供2個線程間的數據交換點。
控制并發訪問一個資源
使用Semaphore(信號量)類來實現一種比較特殊的semaphores種類,稱為binary semaphores。
控制并發訪問多個資源
使用semaphores來保護多個資源的副本,也就是說當你有一個代碼片段可以同時被多個線程執行。關于這一點可以查看該章節的示例代碼,它很好的說明了一個使用它的場景。信號量也可以用來控制并發線程數量。
等待多個并發事件完成
CountDownLatch 類的基本使用方法。有一個線程調用CountDownLatch對象的await()方法等待,等待其他線程完成必須在await()方法之后的之前完成的任務。
在一個相同點同步任務
CyclicBarrier 類有一個內部計數器控制到達同步點的線程數量。每次線程到達同步點,它調用 await() 方法告知 CyclicBarrier 對象到達同步點了。CyclicBarrier 把線程放入睡眠狀態直到全部的線程都到達他們的同步點。
當全部的線程都到達他們的同步點,CyclicBarrier 對象叫醒全部正在 await() 方法中等待的線程們,然后,選擇性的,為CyclicBarrier的構造函數 傳遞的 Runnable 對象(例子里,是 Grouper 對象)創建新的線程執行外加任務。
運行并發階段性任務
Phaser類的使用,可以參考這里來加深對Phaser的使用。
控制并發階段性任務的改變
同上節,在參考的文章中有說明。
在并發任務間交換數據
Exchange類的使用,2個線程間定義同步點,當2個線程到達這個點,他們相互交換數據類型,使用第一個線程的數據類型變成第二個的,然后第二個線程的數據類型變成第一個的。
第四章: 線程執行者
介紹
提出Executor framework概念,這有點像我們之前提到的線程工廠,實際上這些線程執行者框架就是使用線程工廠來創建線程的。這里給出Executor框架類圖創建一個線程執行者
ThreadPoolExecutor的基本使用,execute提交任務,shutdown停止線程執行者。創建一個大小固定的線程執行者
上一節我們使用newCachedThreadPool()來創建ThreadPoolExecutor對象,它的線程池為無限大。但是在這里我們需要使用newFixedThreadPool來創建線程池大小固定的ThreadPoolExecutor對象,定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()。執行者執行返回結果的任務
Executor framework還可以并發執行返回結果的任務,這個時候我們使用的是submit函數提交任務,而不是execute。submit()函數將會返回一個Future對象,使用這個對象的get方法,你將會獲得執行結果,不止如此Future還提供了其他獲得任務執行信息和控制任務的方法,它使你不至于將任務交給執行器之后對任務狀態一無所知。運行多個任務并處理第一個結果
如果你不需要像前面一樣,需要所有提交的任務的結果,而是只需要最先完成的任務的結果,你可以使用執行器的invokeAny()方法。該方法接收一個任務列表作為參數,返回最先執行完成的任務的結果。運行多個任務并處理所有的結果
如果你覺得像第4小節講到的那樣一個一個submit()任務,然后獲得Future比較繁雜,你可以直接將任務列表使用invokeAll()方法直接全部給執行器,它會返回一個對應的Future列表,對應的Future在列表中的位置和提交的任務一致,invokeAll將所有的任務提交后,等待全部任務完成才返回;在延遲后執行者運行任務
創建ScheduledThreadPoolExecutor和利用這個執行器安排任務在指定的時間后執行。它是使用這個執行器對象的schedule()函數來完成這個操作的,如:executor.schedule(task, delayTime, TimeUnit.SECONDS);執行者定期的執行任務
使用ScheduledThreadPoolExecutor類還可以執行定期任務,這一點是調用執行器對象的scheduleAtFixedRate()方法來實現的。當執行器對象調用shutdown()方法后,將不會執行周期性任務。執行者取消任務
通過調用執行器返回的Future的cancel方法,可以取消任務的執行。執行者控制一個結束任務
重寫FutureTask類的done()方法來實現在執行者執行任務完成后執行額外的一些代碼。執行者分離運行任務和處理結果
使用CompletionService類來實現一邊執行任務線程,一邊處理執行完任務線程的結果。原文在這里的解釋不是特別清除,可以參考這里執行者控制被拒絕的任務
實現RejectedExecutionHandler,來處理線程池shutdown()后提交的任務。第五章: Fork/Join 框架
介紹
Fork/Join框架被設計用來解決可以使用分而治之技術將任務分解成更小的問題。具體可以查看該節內容。
創建 Fork/Join 池
創建一個ForkJoinPool對象來執行任務。創建一個ForkJoinPool執行的ForkJoinTask類。
加入任務的結果
在上一節的基礎上,添加說明如果處理任務的子線程有返回結果如何處理。主要就是在Override的compute()函數中返回結果,然后可以使用處理任務的子線程的get方法獲得這個返回結果。并且從例子中可以看出,Fork/Join框架可以提交不一樣的子任務,具體表現如下;
所以Fork/Join框架更加強調的是,父線程等待invokeAll出來的子線程完成后,從invokeAll退出繼續完成接下來的邏輯程序。父線程從邏輯上來說像是從invokeAll處暫停了(等待子線程完成后退出),但是實際上父線程的線程資源還可以去執行其他沒有被執行的任務,但是執行的內容不是父線程原來的內容,這樣可以減少創建的線程的數量,這使用了work-stealing算法,即分配一個新的任務給正在執行睡眠任務的工作線程。
異步運行任務
使用ForkJoinTask的異步方法(比如fork()),來向線程池提交任務。之前我們使用的是同步方法(比如invokeAll()),這將阻塞父線程。而異步方法將繼續父線程的內容,我們使用異步方法也就意味著不能使用work-stealing算法,除非在父線程中使用調用join()或get()方法來等待任務的完成時。
任務中拋出異常
處理任務中拋出的異常,檢查異常不能從compute()中拋出,你必須手動解決(使用try-catch)。未檢查異常將不會輸入任何信息到控制臺。就像沒有發生一樣,但是拋出非檢查異常的線程和它的所有父輩異常不會正常執行。我們自然不會希望,任務沒有正常執行但是又沒有給任何提示,所以最后我們可以使用task.isCompletedAbnormally()來檢查execute()給線程池的任務到底有沒有拋出非檢查異常。
非檢查異常也可以使用try-catch來捕獲,但是一般我們是不會這樣做的,因為非檢查異常的拋出一般是意味著我們的代碼有錯誤,應該去改正它。
取消任務
ForkJoinTask類提供cancel()方法用于這個目的。當你想要取消一個任務時,有一些點你必須考慮一下,這些點如下:
- ForkJoinPool類并沒有提供任何方法來取消正在池中運行或等待的所有任務。
- 當你取消一個任務時,你不能取消一個已經執行的任務。
但是你依舊可以調用運行的任務的cancel()方法,但是具體執不執行就要看到底這個任務有沒有在運行了??梢詤⒖际纠a中,創建一個TaskManager類來管理任務。
第六章: 并發集合
介紹
這里需要了解到并發集合分為兩種:- 阻塞集合:這種集合包括添加和刪除數據的操作。如果操作不能立即進行,是因為集合已滿或者為空,該程序將被阻塞,直到操作可以進行。
- 非阻塞集合:這種集合也包括添加和刪除數據的操作。如果操作不能立即進行,這個操作將返回null值或拋出異常,但該線程將不會阻塞。
使用非阻塞線程安全列表
非阻塞并發列表ConcurrentLinkedDeque類。使用阻塞線程安全列表
阻塞列表LinkedBlockingDeque類。用優先級對使用阻塞線程安全列表排序
阻塞優先隊列PriorityBlockingQueue類使用線程安全與帶有延遲元素的列表
阻塞隊列DelayedQueue類,可以控制元素延遲出現使用線程安全的可遍歷映射
ConcurrentSkipListMap提供了多線程并發存取<Key, Value>數據并且希望保證數據有序時的數據結構,底層使用跳表實現。關于ConcurrentSkipListMap更加詳細的信息可以參考這里生成并行隨機數
使用ThreadLocalRandom類來生成隨機數使用原子變量
使用原子變量,原子變量不使用任何鎖或者其他同步機制來保護它們的值的訪問。他們的全部操作都是基于CAS操作。關于CAS操作更具體的內容網上有很多,務必理解。特別是要理解底層是如何實現的。我建議查看這里。特別是其一直深入到底層的分析,非常有意思。使用原子陣列
使用原子數組,這和原子變量差不多。第七章: 定制并發類
介紹
定制ThreadPoolExecutor 類
復寫ThreadPoolExecutor 類的一些方法。這里需要去理解ThreadPoolExecutor類構造函數的一些參數。
實現一個優先級制的執行者類
實現一個按優先級執行的ThreadPoolExecutor 類,實際上就是TASK(任務)實現Comparable接口,而后將PriorityBlockingQueue作為ThreadPoolExecutor的提交任務時所使用的隊列。
實現ThreadFactory接口來生成自定義線程
使用線程工廠來創造線程
在執行者對象中使用我們的 ThreadFactory
以自己的線程工廠為參數構造執行器對象,使執行器對象使用我們的線程工廠創造線程。
在計劃好的線程池中定制運行任務
使用ScheduledThreadPoolExecutor 來運行計劃任務。
實現ThreadFactory接口來生成自定義線程給Fork/Join框架
實現ThreadFactory接口創建自己的線程工廠提供給Fork/Join框架使用,而不是使用其默認的線程工廠。需要明確的是,不要將任務(callable或runnable)和線程(thread)混為一談。一個線程可以在不同的時期執行多個任務。線程也可以有自己的內存空間,保存自己的數據。如果能將任務和線程這兩個概念區分開來,代碼還是比較好理解的。
在Fork/Join框架中定制運行任務
上一節,我們為Fork/Join框架創建的線程工廠,這一節我們將定制我們自己的任務類,加入到Fork/Join框架的任務類一般都繼承ForkJoinTask這個抽象類,一般我們使用:
- RecursiveAction: 如果你的任務沒有返回結果
- RecursiveTask: 如果你的任務返回結果
但是,這里我們將自己實現一個類繼承ForkJoinTask這個抽象類,從而使任務運行更加更加符合我們的想法。
當我們想去實現和任務具體的內容沒有關系的功能的時候,比如統計,控制線程的時候,我們可以嘗試定制自己的并發類。
實現一個自定義鎖類
實現Lock接口,從而實現自定義鎖類。
實現一個基于優先級傳輸Queue
本節實現一個有優先級的傳輸隊列,用來解決生產者/消費者問題。
實現你自己的原子對象
繼承原子變量,實現自己的類似原子變量的一些操作,比如并發安全的加1或者減1。
第八章: 測試并發應用程序
介紹
本節主要講述如何測試你的并發應用程序是否正確。2-5節都是說明如何從并發類中獲取相應的信息,也就是通過獲取狀態變量來對其相關對象進行監控。下面每節的內容很直白的體現在小節題目中了。
監控鎖接口
監控Phaser類
監控執行者框架
監控Fork/Join池
編寫有效的日志
FindBugs分析并發代碼
配置Eclipse來調試并發代碼
配置NetBeans來調試并發代碼
MultithreadedTC測試并發代碼
總結
以上是生活随笔為你收集整理的Java 7 并发编程指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 理解java并发工具Phaser
- 下一篇: JMM中的原子性、可见性、有序性和vol