本文主要整理博主遇到的Java多線程的相關知識點,適合速記,故命名爲“小抄集”。本文沒有特別重點,每一項針對一個多線程知識做一個概要性總結,也有一些會帶一點例子,習題方便理解和記憶。
1. interrupted與isInterrupted的區別
interrupted():測試當前線程是否已經是中斷狀態,執行後具有狀態標誌清除爲false的功能。
isInterrupted():測試線程Thread對象是否已經是中斷狀態,但不清除狀態標誌。
方法:
1 2 3 4 5 6 7 | public static boolean interrupted() { return currentThread().isInterrupted( true ); } public boolean isInterrupted() { return isInterrupted( false ); } private native boolean isInterrupted( boolean ClearInterrupted); |
2. 終止正在運行的線程的三種方法:
- 使用退出標誌,是線程正常退出,也就是當run方法完成後線程終止;
- 使用stop方法強行終止線程,但是不推薦使用這個方法,因爲stop和suspend及resume一樣都是作廢過期的方法,使用它們可能產生不可預料的結果;
- 使用interrupt方法中斷線程;(推薦)
3. yield方法
yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU執行時間。但放棄時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片。這裏需要注意的是yield()方法和sleep方法一樣,線程並不會讓出鎖,和wait不同。
4. 線程的優先級
Java中線程的優先級分爲1-10這10個等級,如果小於1或大於10則JDK拋出IllegalArgumentException()的異常,默認優先級是5。在Java中線程的優先級具有繼承性,比如A線程啓動B線程,則B線程的優先級與A是一樣的。注意程序正確性不能依賴線程的優先級高低,因爲操作系統可以完全不理會Java線程對於優先級的決定。
5. Java中線程的狀態
New, Runnable, Blocked, Waiting, Time_waiting, Terminated.
6. 守護線程
Java中有兩種線程,一種是用戶線程,另一種是守護線程。當進程中不存在非守護線程了,則守護線程自動銷燬。通過setDaemon(true)設置線程爲後臺線程。注意thread.setDaemon(true)必須在thread.start()之前設置,否則會報IllegalThreadStateException異常;在Daemon線程中產生的新線程也是Daemon的;在使用ExecutorSerice等多線程框架時,會把守護線程轉換爲用戶線程,並且也會把優先級設置爲Thread.NORM_PRIORITY。在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。更多詳細內容可參考《Java守護線程概述》
7. synchronized的類鎖與對象鎖
類鎖:在方法上加上static synchronized的鎖,或者synchronized(xxx.class)的鎖。如下代碼中的method1和method2:
對象鎖:參考method4, method5,method6.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class LockStrategy { public Object object1 = new Object(); public static synchronized void method1(){} public void method2(){ synchronized (LockStrategy. class ){} } public synchronized void method4(){} public void method5() { synchronized ( this ){} } public void method6() { synchronized (object1){} } } |
注意方法method4和method5中的同步塊也是互斥的。
下面做一道習題來加深一下對對象鎖和類鎖的理解:
有一個類這樣定義
1 2 3 4 5 6 7 | public class SynchronizedTest { public synchronized void method1(){} public synchronized void method2(){} public static synchronized void method3(){} public static synchronized void method4(){} } |
那麼,有SynchronizedTest的兩個實例a和b,對於一下的幾個選項有哪些能被一個以上的線程同時訪問呢?
A. a.method1() vs. a.method2()
B. a.method1() vs. b.method1()
C. a.method3() vs. b.method4()
D. a.method3() vs. b.method3()
E. a.method1() vs. a.method3()
答案是什麼呢?BE
有關Java中的鎖的詳細信息,可以參考《Java中的鎖》
8. 同步不具備繼承性
當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。同步不具有繼承性(聲明爲synchronized的父類方法A,在子類中重寫之後並不具備synchronized的特性)。
9. wait, notify, notifyAll用法
只能在同步方法或者同步塊中使用wait()方法。在執行wait()方法後,當前線程釋放鎖(這點與sleep和yield方法不同)。調用了wait函數的線程會一直等待,知道有其他線程調用了同一個對象的notify或者notifyAll方法才能被喚醒,需要注意的是:被喚醒並不代表立刻獲得對象的鎖,要等待執行notify()方法的線程執行完,即退出synchronized代碼塊後,當前線程纔會釋放鎖,而呈wait狀態的線程纔可以獲取該對象鎖。
如果調用wait()方法時沒有持有適當的鎖,則拋出IllegalMonitorStateException,它是RuntimeException的一個子類,因此,不需要try-catch語句進行捕獲異常。
notify方法只會(隨機)喚醒一個正在等待的線程,而notifyAll方法會喚醒所有正在等待的線程。如果一個對象之前沒有調用wait方法,那麼調用notify方法是沒有任何影響的。
詳細可以參考《JAVA線程間協作:wait.notify.notifyAll》
帶參數的wait(long timeout)或者wait(long timeout, int nanos)方法的功能是等待某一時間內是否有線程對鎖進行喚醒,如果超過這個時間則自動喚醒。
10. 管道
在Java中提供了各種各樣的輸入/輸出流Stream,使我們能夠很方便地對數據進行操作,其中管道流(pipeStream)是一種特殊的流,用於在不同線程間直接傳送數據。一個線程發送數據到輸出管道,另一個線程從輸入管道中讀數據,通過使用管道,實現不同線程間的通信,而無須藉助類似臨時文件之類的東西。在JDK中使用4個類來使線程間可以進行通信:PipedInputStream, PipedOutputStream, PipedReader, PipedWriter。使用代碼類似inputStream.connect(outputStream)或outputStream.connect(inputStream)使兩個Stream之間產生通信連接。
幾種進程間的通信方式
- 管道( pipe ):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關係的進程間使用。進程的親緣關係通常是指父子進程關係。
- 有名管道 (named pipe) : 有名管道也是半雙工的通信方式,但是它允許無親緣關係進程間的通信。
- 信號量( semophore ) : 信號量是一個計數器,可以用來控制多個進程對共享資源的訪問。它常作爲一種鎖機制,防止某進程正在訪問共享資源時,其他進程也訪問該資源。因此,主要作爲進程間以及同一進程內不同線程之間的同步手段。
- 消息隊列( message queue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
- 信號 ( sinal ) : 信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生。
- 共享內存( shared memory ) :共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的 IPC 方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號兩,配合使用,來實現進程間的同步和通信。
- 套接字( socket ) : 套解口也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同及其間的進程通信。
11. join方法
如果一個線程A執行了thread.join()語句,其含義是:當前線程A等待thread線程終止之後才從thread.join()返回。join與synchronized的區別是:join在內部使用wait()方法進行等待,而synchronized關鍵字使用的是“對象監視器”做爲同步。
join提供了另外兩種實現方法:join(long millis)和join(long millis, int nanos),至多等待多長時間而退出等待(釋放鎖),退出等待之後還可以繼續運行。內部是通過wait方法來實現的。
可以參考一下一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | System.out.println( "method main begin-----" ); Thread t = new Thread( new Runnable(){ int i = 0 ; @Override public void run() { while ( true ) { System.out.println(i++); try { TimeUnit.MILLISECONDS.sleep( 100 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start(); t.join( 2000 ); System.out.println( "method main end-----" ); |
運行結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | method main begin----- 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 method main end----- 19 20 21 |
12.ThreadLocal
ThreadLocal可以實現每個線程綁定自己的值,即每個線程有各自獨立的副本而互相不受影響。一共有四個方法:get, set, remove, initialValue。可以重寫initialValue()方法來爲ThreadLocal賦初值。如下:
1 2 3 4 5 6 7 | private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){ @Override protected Long initialValue() { return System.currentTimeMillis(); } }; |
ThreadLocal建議設置爲static類型的。
使用類InheritableThreadLocal可以在子線程中取得父線程繼承下來的值。可以採用重寫childValue(Object parentValue)方法來更改繼承的值。
查看案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public class InheriableThreadLocal { public static final InheritableThreadLocal<?> itl = new InheritableThreadLocal<Object>(){ @Override protected Object initialValue() { return new Date().getTime(); } @Override protected Object childValue(Object parentValue) { return parentValue+ " which plus in subThread." ; } }; public static void main(String[] args) { System.out.println( "Main: get value = " +itl.get()); Thread a = new Thread( new Runnable(){ @Override public void run() { System.out.println(Thread.currentThread().getName()+ ": get value = " +itl.get()); } }); a.start(); } } |
運行結果:
1 2 | Main: get value = 1461585405704 Thread- 0 : get value = 1461585405704 which plus in subThread. |
如果去掉@Override protected Object childValue(Object parentValue)方法運行結果:
1 2 | Main: get value = 1461585396073 Thread- 0 : get value = 1461585396073 |
注意:在線程池的情況下,在ThreadLocal業務週期處理完成時,最好顯式的調用remove()方法,清空”線程局部變量”中的值。正常情況下使用ThreadLocal不會造成內存溢出,弱引用的只是threadLocal,保存的值依然是強引用的,如果threadLocal依然被其他對象強引用,”線程局部變量”是無法回收的。
13. ReentrantLock
ReentrantLock提供了tryLock方法,tryLock調用的時候,如果鎖被其他線程持有,那麼tryLock會立即返回,返回結果爲false;如果鎖沒有被其他線程持有,那麼當前調用線程會持有鎖,並且tryLock返回的結果爲true。
1 2 | boolean tryLock() boolean tryLock( long timeout, TimeUnit unit) |
可以在構造ReentranLock時使用公平鎖,公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的先後順序來一次獲得鎖。synchronized中的鎖時非公平的,默認情況下ReentrantLock也是非公平的,但是可以在構造函數中指定使用公平鎖。
1 2 | ReentrantLock() ReentrantLock( boolean fair) |
對於ReentrantLock來說,還有一個十分實用的特性,它可以同時綁定多個Condition條件,以實現更精細化的同步控制。
ReentrantLock使用方式如下:
1 2 3 4 5 6 | Lock lock = new ReentrantLock(); lock.lock(); try { } finally { lock.unlock(); } |
14. ReentrantLock中的其餘方法
- int getHoldCount():查詢當前線程保持此鎖定的個數,也就是調用lock()方法的次數。
- int getQueueLength():返回正等待獲取此鎖定的線程估計數。比如有5個線程,1個線程首先執行await()方法,那麼在調用getQueueLength方法後返回值是4,說明有4個線程在等待lock的釋放。
- int getWaitQueueLength(Condition condition):返回等待此鎖定相關的給定條件Condition的線程估計數。比如有5個線程,每個線程都執行了同一個condition對象的await方法,則調用getWaitQueueLength(Condition condition)方法時返回的int值是5。
- boolean hasQueuedThread(Thread thread):查詢指定線程是否正在等待獲取此鎖定。
- boolean hasQueuedThreads():查詢是否有線程正在等待獲取此鎖定。
- boolean hasWaiters(Condition condition):查詢是否有線程正在等待與此鎖定有關的condition條件。
- boolean isFair():判斷是不是公平鎖。
- boolean isHeldByCurrentThread():查詢當前線程是否保持此鎖定。
- boolean isLocked():查詢此鎖定是否由任意線程保持。
- void lockInterruptibly():如果當前線程未被中斷,則獲取鎖定,如果已經被中斷則出現異常。
15. Condition
一個Condition和一個Lock關聯在一起,就想一個條件隊列和一個內置鎖相關聯一樣。要創建一個Condition,可以在相關聯的Lock上調用Lock.newCondition方法。正如Lock比內置加鎖提供了更爲豐富的功能,Condition同樣比內置條件隊列提供了更豐富的功能:在每個鎖上可存在多個等待、條件等待可以是可中斷的或者不可中斷的、基於時限的等待,以及公平的或非公平的隊列操作。與內置條件隊列不同的是,對於每個Lock,可以有任意數量的Condition對象。Condition對象繼承了相關的Lock對象的公平性,對於公平的鎖,線程會依照FIFO順序從Condition.await中釋放。
注意:在Condition對象中,與wait,notify和notifyAll方法對於的分別是await,signal,signalAll。但是,Condition對Object進行了擴展,因而它也包含wait和notify方法。一定要確保使用的版本——await和signal.
詳細可參考《JAVA線程間協作:Condition》
16. 讀寫鎖ReentrantReadWriteLock
讀寫鎖表示也有兩個鎖,一個是讀操作相關的鎖,也稱爲共享鎖;另一個是寫操作相關的鎖,也叫排它鎖。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。在沒有Thread進行寫操作時,進行讀取操作的多個Thread都可以獲取讀鎖,而進行寫入操作的Thread只有在獲取寫鎖後才能進行寫入操作。即多個Thread可以同時進行讀取操作,但是同一時刻只允許一個Thread進行寫入操作。(lock.readlock.lock(), lock.readlock.unlock, lock.writelock.lock, lock.writelock.unlock)
17. Timer的使用
JDK中的Timer類主要負責計劃任務的功能,也就是在指定時間開始執行某一任務。Timer類的主要作用就是設置計劃任務,但封裝任務的類卻是TimerTask類(public abstract class TimerTask extends Object implements Runnable)。可以通過new Timer(true)設置爲後臺線程。
有以下幾個方法:
- void schedule(TimerTask task, Date time):在指定的日期執行某一次任務。如果執行任務的時間早於當前時間則立刻執行。
- void schedule(TimerTask task, Date firstTime, long period):在指定的日期之後,按指定的間隔週期性地無限循環地執行某一任務。如果執行任務的時間早於當前時間則立刻執行。
- void schedule(TimerTask task, long delay):以當前時間爲參考時間,在此基礎上延遲指定的毫秒數後執行一次TimerTask任務。
- void schedule(TimerTask task, long delay, long period):以當前時間爲參考時間,在此基礎上延遲指定的毫秒數,再以某一間隔無限次數地執行某一任務。
- void scheduleAtFixedRate(TimerTask task, Date firstTime, long period):下次執行任務時間參考上次任務的結束時間,且具有“追趕性”。
TimerTask是以隊列的方式一個一個被順序執行的,所以執行的時間有可能和預期的時間不一致,因爲前面的任務有可能消耗的時間較長,則後面的任務運行時間也會被延遲。
TimerTask類中的cancel方法的作用是將自身從任務隊列中清除。
Timer類中的cancel方法的作用是將任務隊列中的全部任務清空,並且進程被銷燬。
Timer的缺陷:Timer支持基於絕對時間而不是相對時間的調度機制,因此任務的執行對系統時鐘變化很敏感,而ScheduledThreadPoolExecutor只支持相對時間的調度。Timer在執行所有定時任務時只會創建一個線程。如果某個任務的執行時間過長,那麼將破壞其他TimerTask的定時精確性。Timer的另一個問題是,如果TimerTask拋出了一個未檢查的異常,那麼Timer將表現出糟糕的行爲。Timer線程並不波或異常,因此當TimerTask拋出爲檢測的異常時將終止定時線程。
JDK5或者更高的JDK中已經很少使用Timer.
18. 線程安全的單例模式
建議不要採用DCL的寫法,建議使用下面這種寫法:
1 2 3 4 5 6 7 8 9 10 11 12 | public class LazyInitHolderSingleton { private LazyInitHolderSingleton() { } private static class SingletonHolder { private static final LazyInitHolderSingleton INSTANCE = new LazyInitHolderSingleton(); } public static LazyInitHolderSingleton getInstance() { return SingletonHolder.INSTANCE; } } |
或者這種:
1 2 3 4 | public enum SingletonClass { INSTANCE; } |
19. 線程組ThreadGroup
爲了有效地對一些線程進行組織管理,通常的情況下事創建一個線程組,然後再將部分線程歸屬到該組中,這樣可以對零散的線程對象進行有效的組織和規劃。參考以下案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | ThreadGroup tgroup = new ThreadGroup( "mavelous zzh" ); new Thread(tgroup, new Runnable(){ @Override public void run() { System.out.println( "A: Begin: " +Thread.currentThread().getName()); while (!Thread.currentThread().isInterrupted()) { } System.out.println( "A: DEAD: " +Thread.currentThread().getName()); }}).start();; new Thread(tgroup, new Runnable(){ @Override public void run() { System.out.println( "B: Begin: " +Thread.currentThread().getName()); while (!Thread.currentThread().isInterrupted()) { } System.out.println( "B: DEAD: " +Thread.currentThread().getName()); }}).start();; System.out.println(tgroup.activeCount()); System.out.println(tgroup.getName()); System.out.println(tgroup.getMaxPriority()); System.out.println(tgroup.getParent()); TimeUnit.SECONDS.sleep( 5 ); tgroup.interrupt(); |
輸出:
1 2 3 4 5 6 7 8 | A: Begin: Thread- 0 2 mavelous zzh 10 B: Begin: Thread- 1 java.lang.ThreadGroup[name=main,maxpri= 10 ] B: DEAD: Thread- 1 A: DEAD: Thread- 0 |
20. 多線程的異常捕獲UncaughtExceptionHandler
setUncaughtExceptionHandler()的作用是對指定線程對象設置默認的異常處理器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Thread thread = new Thread( new Runnable(){ @Override public void run() { int a= 1 / 0 ; } }); thread.setUncaughtExceptionHandler( new UncaughtExceptionHandler(){ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println( "線程:" +t.getName()+ " 出現了異常:" +e.getMessage()); } }); thread.start(); |
輸出:線程:Thread-0 出現了異常:/ by zero
setDefaultUncaughtExceptionHandler()方法對所有線程對象設置異常處理器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | Thread thread = new Thread( new Runnable(){ @Override public void run() { int a= 1 / 0 ; } }); Thread.setDefaultUncaughtExceptionHandler( new UncaughtExceptionHandler(){ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println( "線程:" +t.getName()+ " 出現了異常:" +e.getMessage()); } }); thread.start(); |
輸出同上,注意兩者之間的區別。如果既包含setUncaughtExceptionHandler又包含setDefaultUncaughtExceptionHandler那麼會被setUncaughtExceptionHandler處理,setDefaultUncaughtExceptionHandler則忽略。更多詳細信息參考《JAVA多線程之UncaughtExceptionHandler——處理非正常的線程中止》
21.ReentrantLock與synchonized區別
- ReentrantLock可以中斷地獲取鎖(void lockInterruptibly() throws InterruptedException)
- ReentrantLock可以嘗試非阻塞地獲取鎖(boolean tryLock())
- ReentrantLock可以超時獲取鎖。通過tryLock(timeout, unit),可以嘗試獲得鎖,並且指定等待的時間。
- ReentrantLock可以實現公平鎖。通過new ReentrantLock(true)實現。
- ReentrantLock對象可以同時綁定多個Condition對象,而在synchronized中,鎖對象的的wait(), notify(), notifyAll()方法可以實現一個隱含條件,如果要和多於一個的條件關聯的對象,就不得不額外地添加一個鎖,而ReentrantLock則無需這樣做,只需要多次調用newCondition()方法即可。
22. 使用多線程的優勢
更多的處理器核心;更快的響應時間;更好的編程模型。
23. 構造線程
一個新構造的線程對象是由其parent線程來進行空間分配的,而child線程繼承了parent線程的:是否爲Daemon、優先級、加載資源的contextClassLoader以及InheritableThreadLocal(參考第12條),同時還會分配一個唯一的ID來標誌這個child線程。
24. 使用多線程的方式
extends Thread 或者implements Runnable
25. 讀寫鎖
讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得併發性相比一般的排它鎖有了很大的提升。Java中使用ReentrantReadWriteLock實現讀寫鎖,讀寫鎖的一般寫法如下(修改自JDK7中的示例):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | class RWDictionary { private final Map<String, Object> m = new TreeMap<String, Object>(); private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); public Object get(String key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } public String[] allKeys() { r.lock(); try { return (String[]) m.keySet().toArray(); } finally { r.unlock(); } } public Object put(String key, Object value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } } public void clear() { w.lock(); try { m.clear(); } finally { w.unlock(); } } } |
26.鎖降級
鎖降級是指寫鎖降級成讀鎖。如果當前線程擁有寫鎖,然後將其釋放,最後獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,最後釋放(先前擁有的)寫鎖的過程。參考下面的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); private volatile static boolean update = false ; public void processData() { r.lock(); if (!update) { //必須先釋放讀鎖 r.unlock(); //鎖降級從寫鎖獲取到開始 w.lock(); try { if (!update) { //準備數據的流程(略) update = true ; } r.lock(); } finally { w.unlock(); } //鎖降級完成,寫鎖降級爲讀鎖 } try { //使用數據的流程(略) } finally { r.unlock(); } } |
鎖降級中的讀鎖是否有必要呢?答案是必要。主要是爲了保證數據的可見性,如果當前線程不獲取讀鎖而是直接釋放寫鎖,假設此刻另一個線程(T)獲取了寫鎖並修改了數據,那麼當前線程無法感知線程T的數據更新。如果當前線程獲取讀鎖,即遵循鎖降級的步驟,則線程T將會被阻塞,直到當前線程使用數據並釋放讀鎖之後,線程T才能獲取寫鎖進行數據更新。
27. ConcurrentHashMap
ConcurrentHashMap是線程安全的HashMap,內部採用分段鎖來實現,默認初始容量爲16,裝載因子爲0.75f,分段16,每個段的HashEntry<K,V>[]大小爲2。鍵值都不能爲null。每次擴容爲原來容量的2倍,ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。在獲取size操作的時候,不是直接把所有segment的count相加就可以可到整個ConcurrentHashMap大小,也不是在統計size的時候把所有的segment的put, remove, clean方法全部鎖住,這種方法太低效。在累加count操作過程中,之前累加過的count發生變化的機率非常小,所有ConcurrentHashMap的做法是先嚐試2(RETRIES_BEFORE_LOCK)次通過不鎖住Segment的方式統計各個Segment大小,如果統計的過程中,容器的count發生了變化,再採用加鎖的方式來統計所有的Segment的大小。
28. 線程安全的非阻塞隊列
非阻塞隊列有ConcurrentLinkedQueue, ConcurrentLinkedDeque。元素不能爲null。以ConcurrentLinkedQueue爲例,有頭head和尾tail兩個指針,遵循FIFO的原則進行入隊和出隊,方法有add(E e), peek()取出不刪除, poll()取出刪除, remove(Object o),size(), contains(Object o), addAll(Collection c), isEmpty()。ConcurrentLinkedDeque是雙向隊列,可以在頭和尾兩個方向進行相應的操作。
29. 阻塞隊列
阻塞隊列(BlockingQueue)是一個支持兩個附加操作的隊列。這兩個附加的操作支持阻塞的插入和移除方法。
支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
支持阻塞的移除方法:意思是隊列爲空時,獲取元素的線程會等待隊列變爲非空。
任何阻塞隊列中的元素都不能爲null.
30. 阻塞隊列的插入和移除操作處理方式:
方法-處理方法 拋出異常 返回特殊值 可能阻塞等待 可設定等待時間
入隊 add(e) offer(e) put(e) offer(e,timeout,unit)
出隊 remove() poll() take() poll(timeout,unit)
查看 element() peek() 無 無
如果是無界隊列,隊列不可能出現滿的情況,所以使用put或offer方法永遠不會被阻塞,而且使用offer方法時,該方法永遠返回true.
31. Java裏的阻塞隊列
ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列。
LinkedeBlockingQueue:一個有鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列
DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
32. ArrayBlockingQueue
此隊列按照FIFO的原則對元素進行排序,可以設定爲公平ArrayBlockingQueue(int capacity, boolean fair),默認爲不公平。初始化時必須設定容量大小ArrayBlockingQueue(int capactiy)。
33. LinkedBlockingQueue
與ArrayBlockingQueue一樣,按照FIFO原則進行排序,與ArrayBlockingQueue不同的是內部實現是一個鏈表結構,且不能設置爲公平的。默認和最大長度爲Integer.MAX_VALUE。
34. PriorityBlockingQueue
是一個支持優先級的無界阻塞隊列,默認初始容量爲11,默認情況下采用自然順序升序排列,不能保證同優先級元素的順序。內部元素要麼實現Comparable接口,要麼在初始化的時候指定構造函數的Comparator來對元素進行排序,有關Comparable與Comparator的細節可以參考:Comparable與Comparator淺析。
35. DelayQueue
DelayQueue是一個支持延時獲取元素的無界阻塞隊列。內部包含一個PriorityQueue來實現,隊列中的元素必須實現Delay接口,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。
DelayQueue非常有用,可以將DelayQueue運用在下面應用場景。
- 緩存系統的設計:可以用DelayQueue保存緩存元素的有效期,使用一個線程循環查詢DelayQueue,一旦能從DelayQueue中獲取元素時,表示緩存有效期到了。
- 定時任務調度:使用DelayQueue保存當天將會執行的任務和執行時間,一旦從DelayQueue中獲取到任務就開始執行,比如TimerQueue就是使用DelayQueue實現的。
36. SynchronousQueue
是一個不存儲元素的阻塞隊列,每一個put操作必須等待一個take操作,否則不能繼續添加元素,非常適合傳遞性場景。支持公平訪問隊列。默認情況下線程採用非公平策略訪問隊列。
37. LinkedTransferQueue
是一個由鏈表結構組成的無界阻塞TransferQueue隊列。相對於其他阻塞隊列,LinkedTransferQueue多了tryTransfer和transfer方法。
transfer方法:如果當前有消費者正在等待接收元素(消費者使用take()或者帶時間限制的poll方法時),transfer方法可以把生產者傳入的元素立刻transfer給消費者,如果沒有消費者在等待接收元素,transfer方法會將元素存放在隊列的tail節點,並等到該元素被消費者消費了才返回。
tryTransfer方法:用來試探生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,則返回false。和transfer方法的區別是tryTransfer方法無論消費者是否接收,方法立刻返回,而transfer方法是必須等到消費者消費了才返回。
38. LinkedBlockingDeque
LinkedBlockingDeque是一個由鏈表結構組成的雙向阻塞隊列。所謂雙向隊列是指可以從隊列的兩端插入和移除元素。雙向隊列因爲多了一個操作隊列的入口,在多線程同時入隊時,也就減少了一半的競爭。相對其他的阻塞隊列,LinkedBlockingDeque多了addFirst, addLast, offerFirst, offerLast, peekFirst, peekLast等方法,
39. Fork/Join框架
Fork/Join框架是JDK7提供的一個用於並行執行任務的框架,是一個把大任務切分爲若干子任務並行的執行,最終彙總每個小任務後得到大任務結果的框架。我們再通過Fork和Join來理解下Fork/Join框架。Fork就是把一個大任務劃分成爲若干子任務並行的執行,Join就是合併這些子任務的執行結果,最後得到這個大任務的結果。
使用Fork/Join框架時,首先需要創建一個ForkJoin任務,它提供在任務中執行fork()和join操作的機制。通常情況下,我們不需要直接繼承ForkJoinTask,只需要繼承它的子類,Fork/Join框架提供了兩個子類:RecursiveAction用於沒有返回結果的任務;RecursiveTask用於有返回結果的任務。ForkJoinTask需要通過ForkJoinPool來執行。
任務分割出的子任務會添加到當前工作線程所維護的雙端隊列中,進入隊列的頭部。當一個工作線程的隊列裏暫時沒有任務時,它會隨機從其他工作線程的隊列的尾部獲取一個任務。(工作竊取算法work-stealing)
示例:計算1+2+3+…+100的結果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; import java.util.concurrent.RecursiveTask; public class CountTask extends RecursiveTask<Integer> { private static final int THRESHOLD = 10 ; private int start; private int end; public CountTask( int start, int end) { super (); this .start = start; this .end = end; } @Override protected Integer compute() { int sum = 0 ; boolean canCompute = (end-start) <= THRESHOLD; if (canCompute) { for ( int i=start;i<=end;i++) { sum += i; } } else { int middle = (start+end)/ 2 ; CountTask leftTask = new CountTask(start,middle); CountTask rightTask = new CountTask(middle+ 1 ,end); leftTask.fork(); rightTask.fork(); int leftResult = leftTask.join(); int rightResult = rightTask.join(); sum = leftResult+rightResult; } return sum; } public static void main(String[] args) { ForkJoinPool forkJoinPool = new ForkJoinPool(); CountTask task = new CountTask( 1 , 100 ); Future<Integer> result = forkJoinPool.submit(task); try { System.out.println(result.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } if (task.isCompletedAbnormally()){ System.out.println(task.getException()); } } } |
40. 原子類
Java中Atomic包裏一共提供了12個類,屬於4種類型的原子更新方式,分別是原子更新基本類型、原子更新數組、原子更新引用、原子更新屬性(字段)。Atomic包裏的類基本都是使用Unsafe實現的包裝類。
1)原子更新基本類型:AtomicBoolean,AtomicInteger, AtomicLong.
2)原子更新數組:AtomicIntegerArray,AtomicLongArray, AtomicReferenceArray.
3)原子更新引用類型:AtomicReference, AtomicStampedReference, AtomicMarkableReference.
4 ) 原子更新字段類型:AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater, AtomicLongFieldUpdater.
41. 原子更新基本類型
AtomicBoolean,AtomicInteger, AtomicLong三個類提供的方法類似,以AtomicInteger爲例:有int addAndGet(int delta), boolean compareAndSet(int expect, int update), int getAndIncrement(), void lazySet(int newValue),int getAndSet(int newValue)。其中大多數的方法都是調用compareAndSet方法實現的,譬如getAndIncrement():
1 2 3 4 5 6 7 8 9 10 11 | public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1 ; if (compareAndSet(current, next)) return current; } } public final boolean compareAndSet( int expect, int update) { return unsafe.compareAndSwapInt( this , valueOffset, expect, update); } |
sun.misc.Unsafe只提供三種CAS方法:compareAndSwapObject, compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源碼,發現它是先把Boolean轉換成整形,再使用compareAndSwapInt進行CAS,原子更新char,float,double變量也可以用類似的思路來實現。
42. 原子更新數組
以AtomicIntegerArray爲例,此類主要提供原子的方式更新數組裏的整形,常用方法如下:
int addAndGet(int i, int delta):以原子的方式將輸入值與數組中索引i的元素相加。
boolean compareAndSet(int i, int expect, int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。
AtomicIntegerArray的兩個構造方法:
AtomicIntegerArray(int length):指定數組的大小,並初始化爲0
AtomicIntegerArray(int [] array):對給定的數組進行拷貝。
案例:
1 2 3 4 5 | int value[] = new int []{ 1 , 2 , 3 }; AtomicIntegerArray aia = new AtomicIntegerArray(value); System.out.println(aia.getAndSet( 1 , 9 )); System.out.println(aia.get( 1 )); System.out.println(value[ 1 ]); |
運行結果:2 9 2
43. CountDownLatch
CountDownLatch允許一個或多個線程等待其他線程完成操作。CountDownLatch的構造函數接收一個int類型的參數作爲計數器,如果你想等待N個點完成,這裏就傳入N(CountDownLatch(int count))。
CountDownLatch的方法有:await(), await(long timeout, TimeUnit unit), countDown(), getCount()等。
計數器必須大於等於0,只是等於0的時候,計數器就是零,調用await方法時不會阻塞當前線程。CountDownLatch不可能重新初始化或者修改CountDownLatch對象的內部計數器的值。一個線程調用countDown方法happens-before另一個線程調用的await()方法。
44. CyclicBarrier
讓一組線程達到一個屏障時被阻塞,知道最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續運行。CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經達到了屏障,然後當前線程被阻塞。CyclicBarrier還提供了一個更高級的構造函數CyclicBarrier(int parties, Runnable barrierAction)用於在線程達到屏障時,優先執行barrierAction,方便處理更復雜的業務場景,舉例如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierTest { static CyclicBarrier c = new CyclicBarrier( 2 , new A()); public static void main(String[] args) { new Thread( new Runnable(){ @Override public void run() { try { System.out.println( 1 ); c.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println( 2 ); } }).start(); try { System.out.println( 3 ); c.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println( 4 ); } static class A implements Runnable { @Override public void run() { System.out.println( 5 ); } } } |
輸出結果:3 1 5 2 4
45. CyclicBarrier和CountDownLatch的區別
CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。
46. Semaphore
Semaphore(信號量)是用來控制同事訪問特定資源的線程數量,它協調各個線程,以保證合理的使用公共資源。Semaphore有兩個構造函數:Semaphore(int permits)默認是非公平的,Semaphore(int permits, boolean fair)可以設置爲公平的。應用案例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class SemaphoreTest { private static final int THREAD_COUNT= 30 ; private static ExecutorService threadPool = Executors.newFixedThreadPool( 30 ); private static Semaphore s = new Semaphore( 10 ); public static void main(String[] args) { for ( int i= 0 ;i<THREAD_COUNT;i++) { final int a = i; threadPool.execute( new Runnable(){ @Override public void run() { try { s.acquire(); System.out.println( "do something...." +a); s.release(); } catch (InterruptedException e) { e.printStackTrace(); } } }); } threadPool.shutdown(); } } |
由上例可以看出Semaphore的用法非常的簡單,首先線程使用Semaphore的acquire()方法獲取一個許可證,使用完之後調用release()方法歸還許可證。還可以用tryAcquire()方法嘗試獲取許可證。Semaphore還提供了一些其他方法: int availablePermits()返回此信號量中當前可用的許可證數;int getQueueLength()返回正在等待獲取許可證的線程數;boolean hasQueuedThreads()是否有線程正在等待獲取許可證;void reducePermits(int reduction)減少reduction個許可證,是個protected方法;Collection<Thread> getQueuedThreads()返回所有等待獲取許可證的線程集合,也是一個protected方法。
47. 線程間交換數據的Exchanger
Exchanger是一個用於線程間協作的工具類。Exchanger用於進行線程間的數據交換。它提供一個同步點,在這個同步點,兩個線程可以交換彼此的數據。這兩個線程通過exchange方法交換數據,如果第一個線程先執行exchange()方法,它會一直等待第二個線程也執行exchange方法。當兩個線程都到達同步點時,這兩個線程就可以交換數據,將本現場生產出來的數據傳遞給對方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | import java.util.concurrent.Exchanger; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExchangerTest { private static final Exchanger<String> exchanger = new Exchanger<>(); private static ExecutorService threadPool = Executors.newFixedThreadPool( 2 ); public static void main(String[] args) { threadPool.execute( new Runnable(){ @Override public void run() { String A = "I'm A!" ; try { String B = exchanger.exchange(A); System.out.println( "In 1-" +Thread.currentThread().getName()+ ": " +B); } catch (InterruptedException e) { e.printStackTrace(); } } }); threadPool.execute( new Runnable(){ @Override public void run() { try { String B= "I'm B!" ; String A = exchanger.exchange(B); System.out.println( "In 2-" +Thread.currentThread().getName()+ ": " +A); } catch (InterruptedException e) { e.printStackTrace(); } } }); threadPool.shutdown(); } } |
輸出結果:
1 2 | In 2 -pool- 1 -thread- 2 : I'm A! In 1 -pool- 1 -thread- 1 : I'm B! |
如果兩個線程有一個沒有執行exchange(V x)方法,則會一直等待,如果擔心有特殊情況發生,避免一直等待,可以使用exchange(V x, long timeout, TimeUnit unit)設置最大等待時長。
48. Java中的線程池ThreadPoolExecutor
可以通過ThreadPoolExecutor來創建一個線程池:
1 | ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) |
1. corePoolSize(線程池基本大小):當向線程池提交一個任務時,若線程池已創建的線程數小於corePoolSize,即便此時存在空閒線程,也會通過創建一個新線程來執行該任務,直到已創建的線程數大於或等於corePoolSize時,纔會根據是否存在空閒線程,來決定是否需要創建新的線程。除了利用提交新任務來創建和啓動線程(按需構造),也可以通過 prestartCoreThread() 或 prestartAllCoreThreads() 方法來提前啓動線程池中的基本線程。
2. maximumPoolSize(線程池最大大小):線程池所允許的最大線程個數。當隊列滿了,且已創建的線程數小於maximumPoolSize,則線程池會創建新的線程來執行任務。另外,對於無界隊列,可忽略該參數。
3. keepAliveTime(線程存活保持時間):默認情況下,當線程池的線程個數多於corePoolSize時,線程的空閒時間超過keepAliveTime則會終止。但只要keepAliveTime大於0,allowCoreThreadTimeOut(boolean) 方法也可將此超時策略應用於核心線程。另外,也可以使用setKeepAliveTime()動態地更改參數。
4. unit(存活時間的單位):時間單位,分爲7類,從細到粗順序:NANOSECONDS(納秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小時),DAYS(天);
5. workQueue(任務隊列):用於傳輸和保存等待執行任務的阻塞隊列。可以使用此隊列與線程池進行交互:
- 如果運行的線程數少於 corePoolSize,則 Executor 始終首選添加新的線程,而不進行排隊。
- 如果運行的線程數等於或多於 corePoolSize,則 Executor 始終首選將請求加入隊列,而不添加新的線程。
- 如果無法將請求加入隊列,則創建新的線程,除非創建此線程超出 maximumPoolSize,在這種情況下,任務將被拒絕。
6. threadFactory(線程工廠):用於創建新線程。由同一個threadFactory創建的線程,屬於同一個ThreadGroup,創建的線程優先級都爲Thread.NORM_PRIORITY,以及是非守護進程狀態。threadFactory創建的線程也是採用new Thread()方式,threadFactory創建的線程名都具有統一的風格:pool-m-thread-n(m爲線程池的編號,n爲線程池內的線程編號);
7. handler(線程飽和策略):當線程池和隊列都滿了,則表明該線程池已達飽和狀態。
- ThreadPoolExecutor.AbortPolicy:處理程序遭到拒絕,則直接拋出運行時異常 RejectedExecutionException。(默認策略)
- ThreadPoolExecutor.CallerRunsPolicy:調用者所在線程來運行該任務,此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。
- ThreadPoolExecutor.DiscardPolicy:無法執行的任務將被刪除。
- ThreadPoolExecutor.DiscardOldestPolicy:如果執行程序尚未關閉,則位於工作隊列頭部的任務將被刪除,然後重新嘗試執行任務(如果再次失敗,則重複此過程)。
可以使用兩個方法向線程池提交任務,分別爲execute()和submit()方法。execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功。submit()方法用於提交需要返回值的任務,線程池會返回一個Future類型的對象,通過這個對象可以判斷任務是否執行成功。如Future<Object> future = executor.submit(task);
利用線程池提供的參數進行監控,參數如下:
- getTaskCount():線程池需要執行的任務數量。
- getCompletedTaskCount():線程池在運行過程中已完成的任務數量,小於或等於taskCount。
- getLargestPoolSize():線程池曾經創建過的最大線程數量,通過這個數據可以知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。
- getPoolSize():線程池的線程數量。如果線程池不銷燬的話,池裏的線程不會自動銷燬,所以這個大小隻增不減。
- getActiveCount():獲取活動的線程數。
49. shutdown和shutdownNow
可以調用線程池的shutdown或者shutdownNow方法來關閉線程池。他們的原理是遍歷線程池的工作線程,然後逐個調用線程的interrupt方法來中斷線程,所以無法響應中斷的任務可能永遠無法停止。
區別:shutdown方法將執行平緩的關閉過程:不在接收新的任務,同時等待已提交的任務執行完成——包括哪些還未開始執行的任務。shutdownNow方法將執行粗暴的關閉過程:它將嘗試取消所有運行中的任務,並且不再啓動隊列中尚未開始執行的任務。
只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true,當所有的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminated方法會返回true。至於應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定,通常調用shutdown方法來關閉線程池,如果任務不一定要執行完,則可以調用shutdownNow方法。
50. 擴展ThreadPoolExecutor
可以通過繼承線程池來自定義線程池,重寫線程池的beforeExecute, afterExecute和terminated方法。在執行任務的線程中將調用beforeExecute和afterExecute等方法,在這些方法中還可以添加日誌、計時、監視或者統計信息收集的功能。無論任務是從run中正常返回,還是拋出一個異常而返回,afterExecute都會被調用。如果任務在完成後帶有一個Error,那麼就不會調用afterExecute。如果beforeExecute拋出一個RuntimeException,那麼任務將不被執行,並且afterExecute也不會被調用。在線程池完成關閉時調用terminated,也就是在所有任務都已經完成並且所有工作者線程也已經關閉後,terminated可以用來釋放Executor在其生命週期裏分配的各種資源,此外還可以執行發送通知、記錄日誌或者手機finalize統計等操作。詳細可以參考《JAVA多線程之擴展ThreadPoolExecutor》
51. SimpleDateFormat非線程安全
當多個線程共享一個SimpleDateFormat實例的時候,就會出現難以預料的異常。
主要原因是parse()方法使用calendar來生成返回的Date實例,而每次parse之前,都會把calendar裏的相關屬性清除掉。問題是這個calendar是個全局變量,也就是線程共享的。因此就會出現一個線程剛把calendar設置好,另一個線程就把它給清空了,這時第一個線程再parse的話就會有問題了。
解決方案:1. 每次使用時創建一個新的SimpleDateFormat實例;2. 創建一個共享的SimpleDateFormat實例變量,並對這個變量進行同步;3. 使用ThreadLocal爲每個線程都創建一個獨享的SimpleDateFormat實例變量。
52. CopyOnWriteArrayList
在每次修改時,都會創建並重新發佈一個新的容器副本,從而實現可變現。CopyOnWriteArrayList的迭代器保留一個指向底層基礎數組的引用,這個數組當前位於迭代器的起始位置,由於它不會被修改,因此在對其進行同步時只需確保數組內容的可見性。因此,多個線程可以同時對這個容器進行迭代,而不會彼此干擾或者與修改容器的線程相互干擾。“寫時複製”容器返回的迭代器不會拋出ConcurrentModificationException並且返回的元素與迭代器創建時的元素完全一致,而不必考慮之後修改操作所帶來的影響。顯然,每當修改容器時都會複製底層數組,這需要一定的開銷,特別是當容器的規模較大時,僅當迭代操作遠遠多於修改操作時,才應該使用“寫入時賦值”容器。
53. 工作竊取算法(work-stealing)
工作竊取算法是指某個線程從其他隊列裏竊取任務來執行。在生產-消費者設計中,所有消費者有一個共享的工作隊列,而在work-stealing設計中,每個消費者都有各自的雙端隊列,如果一個消費者完成了自己雙端隊列中的全部任務,那麼它可以從其他消費者雙端隊列末尾祕密地獲取工作。
優點:充分利用線程進行並行計算,減少了線程間的競爭。
缺點:在某些情況下還是存在競爭,比如雙端隊列(Deque)裏只有一個任務時。並且該算法會消耗了更多的系統資源,比如創建多個線程和多個雙端隊列。
54. Future & FutureTask
FutureTask表示的計算是通過Callable來實現的,相當於一種可生產結果的Runnable,並且可以處於一下3種狀態:等待運行,正在運行和運行完成。運行表示計算的所有可能結束方式,包括正常結束、由於取消而結束和由於異常而結束等。當FutureTask進入完成狀態後,它會永遠停止在這個狀態上。Future.get的行爲取決於任務的狀態,如果任務已經完成,那麼get會立刻返回結果,否則get將阻塞知道任務進入完成狀態,然後返回結果或者異常。FutureTask的使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class Preloader { //method1 private final static FutureTask<Object> future = new FutureTask<Object>( new Callable<Object>(){ @Override public Object call() throws Exception { return "yes" ; } }); //method2 static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); private static final Future<Object> futureExecutor = executor.submit( new Callable<Object>(){ @Override public Object call() throws Exception { return "no" ; } }); public static void main(String[] args) throws InterruptedException, ExecutionException { executor.shutdown(); future.run(); System.out.println(future.get()); System.out.println(futureExecutor.get()); } } |
運行結果:yes no
Callable表示的任務可以拋出受檢查或未受檢查的異常,並且任何代碼都可能拋出一個Error.無論任務代碼拋出什麼異常,都會被封裝到一個ExecutionException中,並在Future.get中被重新拋出。
55. Executors
newFixedThreadPool:創建一個固定長度的線程池,每當提交一個任務時就創建一個線程,直到達到線程池的最大數量,這時線程池的規模將不再變化(如果某個線程由於發生了未預期的Exception而結束,那麼線程池會補充一個新的線程)。(LinkedBlockingQueue)
newCachedThreadPool:創建一個可換成的線程池,如果線程池的當前規模超過了處理需求時,那麼將回收空閒的線程,而當需求增加時,則可以添加新的線程,線程池的規模不存在任何限制。(SynchronousQueue)
newSingleThreadExecutor:是一個單線程的Executor,它創建單個工作者線程來執行任務,如果這個線程異常結束,會創建另一個線程來替代。能確保一組任務在隊列中的順序來串行執行。(LinkedBlockingQueue)
newScheduledThreadPool:創建了一個固定長度的線程池,而且以延遲或者定時的方式來執行任務,類似於Timer。
56. ScheduledThreadPoolExecutor替代Timer
由第17項可知Timer有兩個缺陷,在JDK5開始就很少使用Timer了,取而代之的可以使用ScheduledThreadPoolExecutor。使用實例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; public class ScheduleThreadPoolTest { private static ScheduledExecutorService exec = Executors.newScheduledThreadPool( 2 ); public static void method1() { exec.schedule( new Runnable(){ @Override public void run() { System.out.println( "1" ); }}, 2 , TimeUnit.SECONDS); } public static void method2() { ScheduledFuture<String> future = exec.schedule( new Callable<String>(){ @Override public String call() throws Exception { return "Callable" ; }}, 4 , TimeUnit.SECONDS); try { System.out.println(future.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } public static void main(String[] args) { method1(); method2(); } } |
運行結果:1 Callable
57. Callable & Runnable
Executor框架使用Runnable作爲基本的任務表示形式。Runnable是一種有很大侷限的抽象,雖然run能寫入到日誌文件或者將結果放入某個共享的數據結構,但它不能返回一個值或拋出一個受檢查的異常。
許多任務實際上都是存在延遲的計算——執行數據庫查詢,從網絡上獲取資源,或者計算某個複雜的功能。對於這些任務,Callable是一種更好的抽象:它認爲主入口點(call())將返回一個值,並可能拋出一個異常。
Runnable和Callable描述的都是抽象的計算任務。這些任務通常是有範圍的,即都有一個明確的起始點,並且最終會結束。
58. CompletionService
如果想Executor提交了一組計算任務,並且希望在計算完成後獲得結果,那麼可以保留與每個任務關聯的Future,然後反覆使用get方法,同事將參數timeout指定爲0,從而通過輪詢來判斷任務是否完成。這種方法雖然可行,但卻有些繁瑣。幸運的是,還有一種更好的方法:CompletionService。CompletionService將Executor和BlockingQueue的功能融合在一起。你可以將Callable任務提交給它來執行,然後使用類似於隊列操作的take和poll等方法來獲得已完成的結果,而這些結果會在完成時被封裝爲Future。ExecutorCompletionService實現了CompletionService,並將計算部分委託到一個Executor。代碼示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | int coreNum = Runtime.getRuntime().availableProcessors(); ExecutorService executor = Executors.newFixedThreadPool(coreNum); CompletionService<Object> completionService = new ExecutorCompletionService<Object>(executor); for ( int i= 0 ;i<coreNum;i++) { completionService.submit( new Callable<Object>(){ @Override public Object call() throws Exception { return Thread.currentThread().getName(); }}); } for ( int i= 0 ;i<coreNum;i++) { try { Future<Object> future = completionService.take(); System.out.println(future.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } |
運行結果:
1 2 3 4 | pool- 1 -thread- 1 pool- 1 -thread- 2 pool- 1 -thread- 3 pool- 1 -thread- 4 |
可以通過ExecutorCompletionService(Executor executor, BlockingQueue<Future<V>> completionQueue)構造函數指定特定的BlockingQueue(如下代碼剪輯),默認爲LinkedBlockingQueue。
1 2 | BlockingQueue<Future<Object>> bq = new LinkedBlockingQueue<Future<Object>>(); CompletionService<Object> completionService = new ExecutorCompletionService<Object>(executor,bq); |
ExecutorCompletionService的JDK源碼只有100行左右,有興趣的朋友可以看看。
59. 通過Future來實現取消
ExecutorService.submit將返回一個Future來描述任務。Future擁有一個cancel方法,該方法帶有一個boolean類型的參數mayInterruptIfRunning,表示取消操作是否成功。如果mayInterruptIfRunning爲true並且任務當前正在某個線程運行,那麼這個線程能被中斷。如果這個參數爲false,那麼意味着“若任務還沒啓動,就不要運行它”,這種方式應該用於那些不處理中斷的任務中。當Future.get拋出InterruptedException或TimeoutException時,如果你知道不再需要結果,那麼就可以調用Futuure.cancel來取消任務。
60. 處理不可中斷的阻塞
對於一下幾種情況,中斷請求只能設置線程的中斷狀態,除此之外沒有其他任何作用。
- Java.io包中的同步Socket I/O:雖然InputStream和OutputStream中的read和write等方法都不會響應中斷,但通過關閉底層的套接字,可以使得由於執行read或write等方法而被阻塞的線程拋出一個SocketException。
- Java.io包中的同步I/O:當中斷一個在InterruptibleChannel上等待的線程時會拋出ClosedByInterrptException並關閉鏈路。當關閉一個InterruptibleChannel時,將導致所有在鏈路操作上阻塞的線程都拋出AsynchronousCloseException。
- Selector的異步I/O:如果一個線程在調用Selector.select方法時阻塞了,那麼調用close或wakeup方法會使線程拋出ClosedSelectorException並提前返回。
- 獲得某個鎖:如果一個線程由於等待某個內置鎖而阻塞,那麼將無法響應中斷,因爲線程認爲它肯定會獲得鎖,所以將不會理會中斷請求,但是在Lock類中提供了lockInterruptibly方法,該方法允許在等待一個鎖的同時仍能響應中斷。
61. 關閉鉤子
JVM既可以正常關閉也可以強制關閉,或者說非正常關閉。關閉鉤子可以在JVM關閉時執行一些特定的操作,譬如可以用於實現服務或應用程序的清理工作。關閉鉤子可以在一下幾種場景中應用:1. 程序正常退出(這裏指一個JVM實例);2.使用System.exit();3.終端使用Ctrl+C觸發的中斷;4. 系統關閉;5. OutOfMemory宕機;6.使用Kill pid命令幹掉進程(注:在使用kill -9 pid時,是不會被調用的)。使用方法(Runtime.getRuntime().addShutdownHook(Thread hook))。更多內容可以參考JAVA虛擬機關閉鉤子(Shutdown Hook)
62. 終結器finalize
終結器finalize:在回收器釋放它們後,調用它們的finalize方法,從而保證一些持久化的資源被釋放。在大多數情況下,通過使用finally代碼塊和顯示的close方法,能夠比使用終結器更好地管理資源。唯一例外情況在於:當需要管理對象,並且該對象持有的資源是通過本地方法獲得的。但是基於一些原因(譬如對象復活),我們要儘量避免編寫或者使用包含終結器的類。
63. 線程工廠ThreadFactory
每當線程池(ThreadPoolExecutor)需要創建一個線程時,都是通過線程功夫方法來完成的。默認的線程工廠方法將創建一個新的、非守護的線程,並且不包含特殊的配置信息。通過指定一個線程工廠方法,可以定製線程池的配置信息。在ThreadFactory中只定義了一個方法newThread,每當線程池需要創建一個新線程時都會調用這個方法。默認的線程工廠(DefaultThreadFactory 是Executors的內部類)如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger( 1 ); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger( 1 ); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null ) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-" ; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0 ); if (t.isDaemon()) t.setDaemon( false ); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } } |
通過implements ThreadFactory可以定製線程工廠。譬如,你希望爲線程池中的線程指定一個UncaughtExceptionHandler,或者實例化一個定製的Thread類用於執行調試信息的記錄。
64. synchronized與ReentrantLock之間進行選擇
由第21條可知ReentrantLock與synchronized想必提供了許多功能:定時的鎖等待,可中斷的鎖等待、公平鎖、非阻塞的獲取鎖等,而且從性能上來說ReentrantLock比synchronized略有勝出(JDK6起),在JDK5中是遠遠勝出,爲嘛不放棄synchronized呢?ReentrantLock的危險性要比同步機制高,如果忘記在finnally塊中調用unlock,那麼雖然代碼表面上能正常運行,但實際上已經埋下了一顆定時炸彈,並很可能傷及其他代碼。僅當內置鎖不能滿足需求時,纔可以考慮使用ReentrantLock.
65. Happens-Before規則
程序順序規則:如果程序中操作A在操作B之前,那麼在線程中A操作將在B操作之前。
監視器鎖規則:一個unlock操作現行發生於後面對同一個鎖的lock操作。
volatile變量規則:對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作,這裏的“後面”同樣是指時間上的先後順序。
線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個動作。
線程終止規則:線程的所有操作都先行發生於對此線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值等於段檢測到線程已經終止執行。
線程中斷規則:線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
終結器規則:對象的構造函數必須在啓動該對象的終結器之前執行完成。
傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。
注意:如果兩個操作之間存在happens-before關係,並不意味着java平臺的具體實現必須要按照Happens-Before關係指定的順序來執行。如果重排序之後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法。
66. as-if-serial
不管怎麼重排序,程序執行結果不能被改變。
67. ABA問題
ABA問題發生在類似這樣的場景:線程1轉變使用CAS將變量A的值替換爲C,在此時,線程2將變量的值由A替換爲C,又由C替換爲A,然後線程1執行CAS時發現變量的值仍爲A,所以CAS成功。但實際上這時的現場已經和最初的不同了。大多數情況下ABA問題不會產生什麼影響。如果有特殊情況下由於ABA問題導致,可用採用AtomicStampedReference來解決,原理:樂觀鎖+version。可以參考下面的案例來了解其中的不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | public class ABAQuestion { private static AtomicInteger atomicInt = new AtomicInteger( 100 ); private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>( 100 , 0 ); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread( new Runnable(){ @Override public void run() { atomicInt.compareAndSet( 100 , 101 ); atomicInt.compareAndSet( 101 , 100 ); } }); Thread thread2 = new Thread( new Runnable(){ @Override public void run() { try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); } boolean c3 = atomicInt.compareAndSet( 100 , 101 ); System.out.println(c3); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); Thread thread3 = new Thread( new Runnable(){ @Override public void run() { try { TimeUnit.SECONDS.sleep( 1 ); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedRef.compareAndSet( 100 , 101 , atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+ 1 ); atomicStampedRef.compareAndSet( 101 , 100 , atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+ 1 ); } }); Thread thread4 = new Thread( new Runnable(){ @Override public void run() { int stamp = atomicStampedRef.getStamp(); try { TimeUnit.SECONDS.sleep( 2 ); } catch (InterruptedException e) { e.printStackTrace(); } boolean c3 = atomicStampedRef.compareAndSet( 100 , 101 , stamp, stamp+ 1 ); System.out.println(c3); } }); thread3.start(); thread4.start(); } } |
輸出結果:true false
參考資料
1. 《Java多線程編程核心技術》高洪巖著。
2. 《Java併發編程的藝術》方騰飛 等著。
3. 《Java併發編程實戰》Brian Goetz等著。
4. 深入JDK源碼之ThreadLocal類
5. JAVA多線程之擴展ThreadPoolExecutor
6. Comparable與Comparator淺析
7. JAVA多線程之UncaughtExceptionHandler——處理非正常的線程中止
8. JAVA線程間協作:Condition
9. JAVA線程間協作:wait.notify.notifyAll
10. Java中的鎖
11. Java守護線程概述
12. Java SimpleDateFormat的線程安全性問題