Java 多線程知識彙總-理解版

本文主要整理博主遇到的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的線程安全性問題


發佈了13 篇原創文章 · 獲贊 66 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章