43道多執行緒面試題,附帶答案(二)

TigerJin發表於2021-09-09

1.執行緒的sleep()方法和yield()方法有什麼區別?

答: ① sleep()方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低優先順序的執行緒以執行的機會;yield()方法只會給相同優先順序或更高優先順序的執行緒以執行的機會; ② 執行緒執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態; ③ sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常; ④ sleep()方法比yield()方法(跟作業系統CPU排程相關)具有更好的可移植性。

2.請說出與執行緒同步以及執行緒排程相關的方法。

答:

  • wait():使一個執行緒處於等待(阻塞)狀態,並且釋放所持有的物件的鎖;

  • sleep():使一個正在執行的執行緒處於睡眠狀態,是一個靜態方法,呼叫此方法要處理InterruptedException異常;

  • notify():喚醒一個處於等待狀態的執行緒,當然在呼叫此方法的時候,並不能確切的喚醒某一個等待狀態的執行緒,而是由JVM確定喚醒哪個執行緒,而且與優先順序無關;

  • notityAll():喚醒所有處於等待狀態的執行緒,該方法並不是將物件的鎖給所有執行緒,而是讓它們競爭,只有獲得鎖的執行緒才能進入就緒狀態;

3.舉例說明同步和非同步。

答:如果系統中存在臨界資源(資源數量少於競爭資源的執行緒數量的資源),例如正在寫的資料以後可能被另一個執行緒讀到,或者正在讀的資料可能已經被另一個執行緒寫過了,那麼這些資料就必須進行同步存取(資料庫操作中的排他鎖就是最好的例子)。當應用程式在物件上呼叫了一個需要花費很長時間來執行的方法,並且不希望讓程式等待方法的返回時,就應該使用非同步程式設計,在很多情況下采用非同步途徑往往更有效率。事實上,所謂的同步就是指阻塞式操作,而非同步就是非阻塞式操作。

4.不使用stop停止執行緒?

當run() 或者 call() 方法執行完的時候執行緒會自動結束,如果要手動結束一個執行緒,你可以用volatile 布林變數來退出run()方法的迴圈或者是取消任務來中斷執行緒。

使用自定義的標誌位決定執行緒的執行情況

public class SafeStopThread implements Runnable{  
   private volatile boolean stop=false;//此變數必須加上volatile  
   int a=0;  
   @Override  
    public void run() {  
        // TODO Auto-generated method stub  
        while(!stop){  
               synchronized ("") {  
                    a++;  
                    try {  
                        Thread.sleep(100);  
                    } catch (Exception e) {  
                        // TODO: handle exception  
                    }  
                    a--;  
                    String tn=Thread.currentThread().getName();  
                    System.out.println(tn+":a="+a);  
                }  
        }  
      //執行緒終止  
     public void terminate(){  
         stop=true;  
      }  
  public static void main(String[] args) {  
       SafeStopThread t=new SafeStopThread();  
       Thread t1=new Thread(t);  
       t1.start();  
       for(int i=0;i<5;i++){   
           new Thread(t).start();  
       }  
     t.terminate();  
   }  
}

5.Java中如何實現執行緒?各有什麼優缺點,比較常用的是那種,為什麼?

在語言層面有兩種方式。java.lang.Thread 類的例項就是一個執行緒但是它需要呼叫java.lang.Runnable介面來執行,由於執行緒類本身就是呼叫的Runnable介面所以你可以繼承java.lang.Thread 類或者直接呼叫Runnable介面來重寫run()方法實現執行緒。

Java不支援類的多重繼承,但允許你呼叫多個介面。所以如果你要繼承其他類,當然是呼叫Runnable介面好了。

6.如何控制某個方法允許併發訪問執行緒的大小?

Semaphore兩個重要的方法就是semaphore.acquire() 請求一個訊號量,這時候的訊號量個數-1(一旦沒有可使用的訊號量,也即訊號量個數變為負數時,再次請求的時候就會阻塞,直到其他執行緒釋放了訊號量)semaphore.release()釋放一個訊號量,此時訊號量個數+1

public class SemaphoreTest {  
    private Semaphore mSemaphore = new Semaphore(5);  
    public void run(){  
        for(int i=0; i< 100; i++){  
            new Thread(new Runnable() {  
                @Override  
                public void run() {  
                    test();  
                }  
            }).start();  
        }  
    }  
  
    private void test(){  
        try {  
            mSemaphore.acquire();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName() + " 進來了");  
        try {  
            Thread.sleep(1000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName() + " 出去了");  
        mSemaphore.release();  
    }  
}

7.在Java中什麼是執行緒排程?

執行緒排程是指系統為執行緒分配處理器使用權的過程。 主要排程方式有兩種,分別是協同式執行緒排程和搶佔式執行緒排程。

協同式執行緒排程:執行緒的執行時間由執行緒本身控制,當執行緒把自己的工作執行完了之後,主動通知系統切換到另一個執行緒上。

  • 好處是切換操作對於執行緒自己是可知的,沒什麼執行緒同步問題。

  • 壞處是執行緒執行時間不可控,可能會一直阻塞然後系統崩潰。

搶佔式執行緒排程:每個執行緒由系統分配執行時間,不由執行緒本身決定。執行緒的執行時間是系統可控的,不會有一直阻塞的問題。

Java使用搶佔式排程

8.Java中用到的執行緒排程演算法是什麼?

搶佔式。一個執行緒用完CPU之後,作業系統會根據執行緒優先順序、執行緒飢餓情況等資料算出一個總的優先順序並分配下一個時間片給某個執行緒執行。

9.執行緒類的構造方法、靜態塊是被哪個執行緒呼叫的?

執行緒類的構造方法、靜態塊是被new這個執行緒類所在的執行緒所呼叫的,而run方法裡面的程式碼才是被執行緒自身所呼叫的。

10.在實現Runnable的介面中怎麼樣訪問當前執行緒物件,比如拿到當前執行緒的名字?

Thread t = Thread.currentThread();
String name = t.getName();
System.out.println("name=" + name);

11.什麼是執行緒池?為什麼要使用它?為什麼使用Executor框架比使用應用建立和管理執行緒好?

建立執行緒要花費昂貴的資源和時間,如果任務來了才建立執行緒那麼響應時間會變長,而且一個程式能建立的執行緒數有限。

為了避免這些問題,在程式啟動的時候就建立若干執行緒來響應處理,它們被稱為執行緒池,裡面的執行緒叫工作執行緒。

Executor框架讓你可以建立不同的執行緒池。比如單執行緒池,每次處理一個任務;數目固定的執行緒池或者是快取執行緒池(一個適合很多生存期短的任務的程式的可擴充套件執行緒池)。

12常用的執行緒池模式以及不同執行緒池的使用場景?

以下是Java自帶的幾種執行緒池: 1、newFixedThreadPool 建立一個指定工作執行緒數量的執行緒池。 每當提交一個任務就建立一個工作執行緒,如果工作執行緒數量達到執行緒池初始的最大數,則將提交的任務存入到池佇列中。

2、newCachedThreadPool 建立一個可快取的執行緒池。 這種型別的執行緒池特點是:

  • 1).工作執行緒的建立數量幾乎沒有限制(其實也有限制的,數目為Interger. MAX_VALUE),這樣可靈活的往執行緒池中新增執行緒。

  • 2).如果長時間沒有往執行緒池中提交任務,即如果工作執行緒空閒了指定的時間(預設為1分鐘),則該工作執行緒將自動終止。終止後,如果你又提交了新的任務,則執行緒池重新建立一個工作執行緒。

3、newSingleThreadExecutor建立一個單執行緒化的Executor,即只建立唯一的工作者執行緒來執行任務,如果這個執行緒異常結束,會有另一個取代它,保證順序執行(我覺得這點是它的特色)。

單工作執行緒最大的特點是可保證順序地執行各個任務,並且在任意給定的時間不會有多個執行緒是活動的。

4、newScheduleThreadPool 建立一個定長的執行緒池,而且支援定時的以及週期性的任務執行,類似於Timer。

13.在Java中Executor、ExecutorService、Executors的區別?

Executor 和 ExecutorService 這兩個介面主要的區別是:

  • ExecutorService 介面繼承了 Executor 介面,是 Executor 的子介面

  • Executor 和 ExecutorService 第二個區別是:Executor 介面定義了 execute()方法用來接收一個Runnable介面的物件,而 ExecutorService 介面中的 submit()方法可以接受Runnable和Callable介面的物件。

  • Executor 和 ExecutorService 介面第三個區別是 Executor 中的 execute() 方法不返回任何結果,而 ExecutorService 中的 submit()方法可以透過一個 Future 物件返回運算結果。

  • Executor 和 ExecutorService 介面第四個區別是除了允許客戶端提交一個任務,ExecutorService 還提供用來控制執行緒池的方法。比如:呼叫 shutDown() 方法終止執行緒池。

Executors 類提供工廠方法用來建立不同型別的執行緒池。

比如: newSingleThreadExecutor() 建立一個只有一個執行緒的執行緒池,newFixedThreadPool(int numOfThreads)來建立固定執行緒數的執行緒池,newCachedThreadPool()可以根據需要建立新的執行緒,但如果已有執行緒是空閒的會重用已有執行緒。

14.如何建立一個Java執行緒池?

Java透過Executors提供四種執行緒池,分別為:

newCachedThreadPool建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。

newFixedThreadPool 建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。

newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。

newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。

15.Thread 類中的start() 和 run() 方法有什麼區別?

start()方法被用來啟動新建立的執行緒,而且start()內部呼叫了run()方法,這和直接呼叫run()方法的效果不一樣。

當你呼叫run()方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動,start()方法才會啟動新執行緒。

16.Java執行緒池中submit() 和 execute()方法有什麼區別?

兩個方法都可以向執行緒池提交任務,execute()方法的返回型別是void,它定義在Executor介面中, 而submit()方法可以返回持有計算結果的Future物件,它定義在ExecutorService介面中,它擴充套件了Executor介面,其它執行緒池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。

17.Java中notify 和 notifyAll有什麼區別?

notify()方法不能喚醒某個具體的執行緒,所以只有一個執行緒在等待的時候它才有用武之地。而notifyAll()喚醒所有執行緒並允許他們爭奪鎖確保了至少有一個執行緒能繼續執行。

當有執行緒呼叫了物件的 notifyAll()方法(喚醒所有 wait 執行緒)或 notify()方法(只隨機喚醒一個 wait 執行緒),被喚醒的的執行緒便會進入該物件的鎖池中,鎖池中的執行緒會去競爭該物件鎖。也就是說,呼叫了notify後只要一個執行緒會由等待池進入鎖池,而notifyAll會將該物件等待池內的所有執行緒移動到鎖池中,等待鎖競爭

優先順序高的執行緒競爭到物件鎖的機率大,假若某執行緒沒有競爭到該物件鎖,它還會留在鎖池中,唯有執行緒再次呼叫 wait()方法,它才會重新回到等待池中。

18.為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?

一個很明顯的原因是JAVA提供的鎖是物件級的而不是執行緒級的,每個物件都有鎖,透過執行緒獲得。

如果執行緒需要等待某些鎖那麼呼叫物件中的wait()方法就有意義了。如果wait()方法定義在Thread類中,執行緒正在等待的是哪個鎖就不明顯了。

簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬於物件。

19.為什麼wait和notify方法要在同步塊中呼叫?

主要是因為Java API強制要求這樣做,如果你不這麼做,你的程式碼會丟擲IllegalMonitorStateException異常。還有一個原因是為了避免wait和notify之間產生競態條件。

最主要的原因是為了防止以下這種情況

// 等待者(Thread1)while (condition != true) { // step.1
    lock.wait() // step.4}// 喚醒者(Thread2)condition = true; // step.2lock.notify(); // step.3

在對之前的程式碼去掉 synchronized 塊之後,如果在等待者判斷 condition != true 之後而呼叫 wait() 之前,喚醒者**將 condition 修改成了 true 同時呼叫了 notify() **的話,那麼等待者在呼叫了 wait() 之後就沒有機會被喚醒了。

20.講下join,yield方法的作用,以及什麼場合用它們?

join() 的作用:讓“主執行緒”等待“子執行緒”結束之後才能繼續執行。

yield方法可以暫停當前正在執行的執行緒物件,讓其它有相同優先順序的執行緒執行。它是一個靜態方法而且只保證當前執行緒放棄CPU佔用而不能保證使其它執行緒一定能佔用CPU,執行yield()的執行緒有可能在進入到暫停狀態後馬上又被執行。

21.sleep方法有什麼作用,一般用來做什麼?

sleep()方法(休眠)是執行緒類(Thread)的靜態方法,呼叫此方法會讓當前執行緒暫停執行指定的時間,將執行機會(CPU)讓給其他執行緒,但是物件的鎖依然保持,因此休眠時間結束後會自動恢復。注意這裡的恢復並不是恢復到執行的狀態,而是恢復到可執行狀態中等待CPU的寵幸。

22.Java多執行緒中呼叫wait() 和 sleep()方法有什麼不同?

Java程式中wait和sleep都會造成某種形式的暫停,它們可以滿足不同的需要。

  • wait存在於Object類中;sleep存在於Thread類中。

  • wait會讓出CPU資源以及釋放鎖;sleep只會釋放CPU資源。

  • wait只能在同步塊中使用;sleep沒這限制。

  • wait需要notify(或 notifyAll)喚醒,進入等鎖狀態;sleep到指定時間便會自動恢復到執行狀態。

23.為什麼Thread裡面的大部分方法都是final的?

不能被重寫,執行緒的很多方法都是由系統呼叫的,不能透過子類覆寫去改變他們的行為。

24.為什麼Thread類的sleep()和yield()方法是靜態的?

Thread類的sleep()和yield()方法將在當前正在執行的執行緒上執行。

該程式碼只有在某個A執行緒執行時會被執行,這種情況下通知某個B執行緒yield是無意義的(因為B執行緒本來就沒在執行)。因此只有當前執行緒執行yield才是有意義的。透過使該方法為static,你將不會浪費時間嘗試yield 其他執行緒。

只能給自己喂安眠藥,不能給別人喂安眠藥。

25.什麼是阻塞式方法?

阻塞式方法是指程式會一直等待該方法完成期間不做其他事情。

ServerSocket的accept()方法就是一直等待客戶端連線。這裡的阻塞是指呼叫結果返回之前,當前執行緒會被掛起,直到得到結果之後才會返回。

此外,還有非同步和非阻塞式方法在任務完成前就返回。

26.如何強制啟動一個執行緒?

在Java裡面沒有辦法強制啟動一個執行緒,它是被執行緒排程器控制著

27.一個執行緒執行時發生異常會怎樣?

簡單的說,如果異常沒有被捕獲該執行緒將會停止執行。

Thread.UncaughtExceptionHandler是用於處理未捕獲異常造成執行緒突然中斷情況的一個內嵌介面。

當一個未捕獲異常將造成執行緒中斷的時候JVM會使用Thread.getUncaughtExceptionHandler()來查詢執行緒的UncaughtExceptionHandler並將執行緒和異常作為引數傳遞給handler的uncaughtException()方法進行處理。

28.線上程中你怎麼處理不可控制異常?

在Java中有兩種異常。

非執行時異常(Checked Exception):這種異常必須在方法宣告的throws語句指定,或者在方法體內捕獲。例如:IOException和ClassNotFoundException。

執行時異常(Unchecked Exception):這種異常不必在方法宣告中指定,也不需要在方法體中捕獲。例如,NumberFormatException。

因為run()方法不支援throws語句,所以當執行緒物件的run()方法丟擲非執行異常時,我們必須捕獲並且處理它們。當執行時異常從run()方法中丟擲時,預設行為是在控制檯輸出堆疊記錄並且退出程式。

好在,java提供給我們一種線上程物件裡捕獲和處理執行時異常的一種機制。實現用來處理執行時異常的類,這個類實現UncaughtExceptionHandler介面並且實現這個介面的uncaughtException()方法。示例:

package concurrency;

import java.lang.Thread.UncaughtExceptionHandler;

public class Main2 {
    public static void main(String[] args) {
        Task task = new Task();
        Thread thread = new Thread(task);
        thread.setUncaughtExceptionHandler(new ExceptionHandler());
        thread.start();
    }
}

class Task implements Runnable{
    @Override
    public void run() {
        int numero = Integer.parseInt("TTT");
    }
}

class ExceptionHandler implements UncaughtExceptionHandler{
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.printf("An exception has been capturedn");
        System.out.printf("Thread:  %sn", t.getId());
        System.out.printf("Exception:  %s:  %sn", e.getClass().getName(),e.getMessage());
        System.out.printf("Stack Trace:  n");
        e.printStackTrace(System.out);
        System.out.printf("Thread status:  %sn",t.getState());
    }
}

當一個執行緒丟擲了異常並且沒有被捕獲時(這種情況只可能是執行時異常),JVM檢查這個執行緒是否被預置了未捕獲異常處理器。如果找到,JVM將呼叫執行緒物件的這個方法,並將執行緒物件和異常作為傳入引數。

Thread類還有另一個方法可以處理未捕獲到的異常,即靜態方法setDefaultUncaughtExceptionHandler()。這個方法在應用程式中為所有的執行緒物件建立了一個異常處理器。

當執行緒丟擲一個未捕獲到的異常時,JVM將為異常尋找以下三種可能的處理器。

  • 首先,它查詢執行緒物件的未捕獲異常處理器。

  • 如果找不到,JVM繼續查詢執行緒物件所在的執行緒組(ThreadGroup)的未捕獲異常處理器。

  • 如果還是找不到,如同本節所講的,JVM將繼續查詢預設的未捕獲異常處理器。

  • 如果沒有一個處理器存在,JVM則將堆疊異常記錄列印到控制檯,並退出程式。

29.為什麼你應該在迴圈中檢查等待條件?

處於等待狀態的執行緒可能會收到錯誤警報和偽喚醒,如果不在迴圈中檢查等待條件,程式就會在沒有滿足結束條件的情況下退出。

1、一般來說,wait肯定是在某個條件呼叫的,不是if就是while 2、放在while裡面,是防止出於waiting的物件被別的原因呼叫了喚醒方法,但是while裡面的條件並沒有滿足(也可能當時滿足了,但是由於別的執行緒操作後,又不滿足了),就需要再次呼叫wait將其掛起。 3、其實還有一點,就是while最好也被同步,這樣不會導致錯失訊號。

while(condition){    wait();
}

30.多執行緒中的忙迴圈是什麼?

忙迴圈就是程式設計師用迴圈讓一個執行緒等待,不像傳統方法wait()、 sleep() 或 yield(),它們都放棄了CPU控制,而忙迴圈不會放棄CPU,它就是在執行一個空迴圈。

這麼做的目的是為了保留CPU快取,在多核系統中,一個等待執行緒醒來的時候可能會在另一個核心執行,這樣會重建快取。為了避免重建快取和減少等待重建的時間就可以使用它了。

31.什麼是自旋鎖?

沒有獲得鎖的執行緒一直迴圈在那裡看是否該鎖的保持者已經釋放了鎖,這就是自旋鎖。

32.什麼是互斥鎖?

互斥鎖:從等待到解鎖過程,執行緒會從sleep狀態變為running狀態,過程中有執行緒上下文的切換,搶佔CPU等開銷。

33.自旋鎖的優缺點?

自旋鎖不會引起呼叫者休眠,如果自旋鎖已經被別的執行緒保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者釋放了鎖。由於自旋鎖不會引起呼叫者休眠,所以自旋鎖的效率遠高於互斥鎖。

雖然自旋鎖效率比互斥鎖高,但它會存在下面兩個問題: 1、自旋鎖一直佔用CPU,在未獲得鎖的情況下,一直執行,如果不能在很短的時間內獲得鎖,會導致CPU效率降低。 2、試圖遞迴地獲得自旋鎖會引起死鎖。遞迴程式決不能在持有自旋鎖時呼叫它自己,也決不能在遞迴呼叫時試圖獲得相同的自旋鎖。

由此可見,我們要慎重的使用自旋鎖,自旋鎖適合於鎖使用者保持鎖時間比較短並且鎖競爭不激烈的情況。正是由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。

34.如何在兩個執行緒間共享資料?

同一個Runnable,使用全域性變數。

第一種:將共享資料封裝到一個物件中,把這個共享資料所在的物件傳遞給不同的Runnable

第二種:將這些Runnable物件作為某一個類的內部類,共享的資料作為外部類的成員變數,對共享資料的操作分配給外部類的方法來完成,以此實現對操作共享資料的互斥和通訊,作為內部類的Runnable來操作外部類的方法,實現對資料的操作

class ShareData {
 private int x = 0;

 public synchronized void addx(){
   x++;
   System.out.println("x++ : "+x);
 }
 public synchronized void subx(){
   x--;
   System.out.println("x-- : "+x);
 }
}

public class ThreadsVisitData {
 
 public static ShareData share = new ShareData();
 
 public static void main(String[] args) {
  //final ShareData share = new ShareData();
  new Thread(new Runnable() {
    public void run() {
        for(int i = 0;i<100;i++){
            share.addx();
        }
    }
  }).start();
  new Thread(new Runnable() {
    public void run() {
        for(int i = 0;i<100;i++){
            share.subx();
        }
    }
   }).start(); 
 }
}

35Java中Runnable和Callable有什麼不同?

Runnable和Callable都是介面, 不同之處: 1.Callable可以返回一個型別V,而Runnable不可以 2.Callable能夠丟擲checked exception,而Runnable不可以。 3.Runnable是自從java1.1就有了,而Callable是1.5之後才加上去的 4.Callable和Runnable都可以應用於executors。而Thread類只支援Runnable.

import java.util.concurrent.Callable;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  
  
public class ThreadTestB {  
    public static void main(String[] args) {  
        ExecutorService e=Executors.newFixedThreadPool(10);  
        Future f1=e.submit(new MyCallableA());  
        Future f2=e.submit(new MyCallableA());  
        Future f3=e.submit(new MyCallableA());        
        System.out.println("--Future.get()....");  
        try {  
            System.out.println(f1.get());  
            System.out.println(f2.get());  
            System.out.println(f3.get());            
        } catch (InterruptedException e1) {  
            e1.printStackTrace();  
        } catch (ExecutionException e1) {  
            e1.printStackTrace();  
        }  
        e.shutdown();  
    }  
}  
  
class MyCallableA implements Callable<String>{  
    public String call() throws Exception {  
        System.out.println("開始執行Callable");  
        String[] ss={"zhangsan","lisi"};  
        long[] num=new long[2];  
        for(int i=0;i<1000000;i++){  
            num[(int)(Math.random()*2)]++;  
        }  
          
        if(num[0]>num[1]){  
            return ss[0];  
        }else if(num[0]<num[1]){  
            throw new Exception("棄權!");  
        }else{  
            return ss[1];  
        }  
    } 
}

36.Java中CyclicBarrier 和 CountDownLatch有什麼不同?

CountDownLatch和CyclicBarrier都能夠實現執行緒之間的等待,只不過它們側重點不同:

  • CountDownLatch一般用於某個執行緒A等待若干個其他執行緒執行完任務之後,它才執行;

  • CyclicBarrier一般用於一組執行緒互相等待至某個狀態,然後這一組執行緒再同時執行;

  • 另外,CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。

CountDownLatch的用法:

public class Test {
     public static void main(String[] args) {   
         final CountDownLatch latch = new CountDownLatch(2);
          
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子執行緒"+Thread.currentThread().getName()+"正在執行");
                    Thread.sleep(3000);
                    System.out.println("子執行緒"+Thread.currentThread().getName()+"執行完畢");
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
          
         new Thread(){
             public void run() {
                 try {
                     System.out.println("子執行緒"+Thread.currentThread().getName()+"正在執行");
                     Thread.sleep(3000);
                     System.out.println("子執行緒"+Thread.currentThread().getName()+"執行完畢");
                     latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             };
         }.start();
          
         try {
             System.out.println("等待2個子執行緒執行完畢...");
            latch.await();
            System.out.println("2個子執行緒已經執行完畢");
            System.out.println("繼續執行主執行緒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
     }
}

CyclicBarrier用法:

public class Test {
    public static void main(String[] args) {
        int N = 4;
        CyclicBarrier barrier  = new CyclicBarrier(N,new Runnable() {
            @Override
            public void run() {
                System.out.println("當前執行緒"+Thread.currentThread().getName());   
            }
        });
         
        for(int i=0;i<N;i++)
            new Writer(barrier).start();
    }
    static class Writer extends Thread{
        private CyclicBarrier cyclicBarrier;
        public Writer(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }
 
        @Override
        public void run() {
            System.out.println("執行緒"+Thread.currentThread().getName()+"正在寫入資料...");
            try {
                Thread.sleep(5000);      //以睡眠來模擬寫入資料操作
                System.out.println("執行緒"+Thread.currentThread().getName()+"寫入資料完畢,等待其他執行緒寫入完畢");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }catch(BrokenBarrierException e){
                e.printStackTrace();
            }
            System.out.println("所有執行緒寫入完畢,繼續處理其他任務...");
        }
    }
}

37.Java中interrupted和isInterruptedd方法的區別?

interrupt方法用於中斷執行緒。呼叫該方法的執行緒的狀態為將被置為”中斷”狀態。

注意:執行緒中斷僅僅是置執行緒的中斷狀態位,不會停止執行緒。需要使用者自己去監視執行緒的狀態為並做處理。支援執行緒中斷的方法(也就是執行緒中斷後會丟擲interruptedException的方法)就是在監視執行緒的中斷狀態,一旦執行緒的中斷狀態被置為“中斷狀態”,就會丟擲中斷異常。

isInterrupted 只是簡單的查詢中斷狀態,不會對狀態進行修改。

38.concurrentHashMap的原始碼理解以及內部實現原理,為什麼他是同步的且效率高

ConcurrentHashMap 分析

ConcurrentHashMap的結構是比較複雜的,都深究去本質,其實也就是陣列和連結串列而已。我們由淺入深慢慢的分析其結構。

先簡單分析一下,ConcurrentHashMap 的成員變數中,包含了一個 Segment 的陣列(final Segment<K,V>[] segments;),而 Segment 是 ConcurrentHashMap 的內部類,然後在 Segment 這個類中,包含了一個 HashEntry 的陣列(transient volatile HashEntry<K,V>[] table;)。而 HashEntry 也是ConcurrentHashMap 的內部類。HashEntry 中,包含了 key 和 value 以及 next 指標(類似於 HashMap 中 Entry),所以 HashEntry 可以構成一個連結串列。

所以通俗的講,ConcurrentHashMap 資料結構為一個 Segment 陣列,Segment 的資料結構為 HashEntry 的陣列,而 HashEntry 存的是我們的鍵值對,可以構成連結串列。

首先,我們看一下 HashEntry 類。

HashEntry

HashEntry 用來封裝雜湊對映表中的鍵值對。在 HashEntry 類中,key,hash 和 next 域都被宣告為 final 型,value 域被宣告為 volatile 型。其類的定義為:

static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        ...
        ...
}

HashEntry 的學習可以類比著 HashMap 中的 Entry。我們的儲存鍵值對的過程中,雜湊的時候如果發生“碰撞”,將採用“分離連結串列法”來處理碰撞:把碰撞的 HashEntry 物件連結成一個連結串列。

如下圖,我們在一個空桶中插入 A、B、C 兩個 HashEntry 物件後的結構圖(其實應該為鍵值對,在這進行了簡化以方便更容易理解):

圖片描述

Segment

Segment 的類定義為static final class Segment<K,V> extends ReentrantLock implements Serializable。其繼承於 ReentrantLock 類,從而使得 Segment 物件可以充當鎖的角色。Segment 中包含HashEntry 的陣列,其可以守護其包含的若干個桶(HashEntry的陣列)。Segment 在某些意義上有點類似於 HashMap了,都是包含了一個陣列,而陣列中的元素可以是一個連結串列。

table:table 是由 HashEntry 物件組成的陣列如果雜湊時發生碰撞,碰撞的 HashEntry 物件就以連結串列的形式連結成一個連結串列table陣列的陣列成員代表雜湊對映表的一個桶每個 table 守護整個 ConcurrentHashMap 包含桶總數的一部分如果併發級別為 16,table 則守護 ConcurrentHashMap 包含的桶總數的 1/16。

count 變數是計算器,表示每個 Segment 物件管理的 table 陣列(若干個 HashEntry 的連結串列)包含的HashEntry 物件的個數。之所以在每個Segment物件中包含一個 count 計數器,而不在 ConcurrentHashMap 中使用全域性的計數器,是為了避免出現“熱點域”而影響併發性。

/**
 * Segments are specialized versions of hash tables.  This
 * subclasses from ReentrantLock opportunistically, just to
 * simplify some locking and avoid separate construction.
 */
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    /**
     * The per-segment table. Elements are accessed via
     * entryAt/setEntryAt providing volatile semantics.
     */
    transient volatile HashEntry<K,V>[] table;

    /**
     * The number of elements. Accessed only either within locks
     * or among other volatile reads that maintain visibility.
     */
    transient int count;
    transient int modCount;
    /**
     * 裝載因子
     */
    final float loadFactor;
}

我們透過下圖來展示一下插入 ABC 三個節點後,Segment 的示意圖:

圖片描述

其實從我個人角度來說,Segment結構是與HashMap很像的。

ConcurrentHashMap

ConcurrentHashMap 的結構中包含的 Segment 的陣列,在預設的併發級別會建立包含 16 個 Segment 物件的陣列。透過我們上面的知識,我們知道每個 Segment 又包含若干個雜湊表的桶,每個桶是由 HashEntry 連結起來的一個連結串列。如果 key 能夠均勻雜湊,每個 Segment 大約守護整個雜湊表桶總數的 1/16。

下面我們還有透過一個圖來演示一下 ConcurrentHashMap 的結構:

圖片描述

併發寫操作

在 ConcurrentHashMap 中,當執行 put 方法的時候,會需要加鎖來完成。我們透過程式碼來解釋一下具體過程: 當我們 new 一個 ConcurrentHashMap 物件,並且執行put操作的時候,首先會執行 ConcurrentHashMap 類中的 put 方法,該方法原始碼為:

/**
 * Maps the specified key to the specified value in this table.
 * Neither the key nor the value can be null.
 *
 * <p> The value can be retrieved by calling the <tt>get</tt> method
 * with a key that is equal to the original key.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>
 * @throws NullPointerException if the specified key or value is null
 */
@SuppressWarnings("unchecked")
public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

我們透過註釋可以瞭解到,ConcurrentHashMap 不允許空值。該方法首先有一個 Segment 的引用 s,然後會透過 hash() 方法對 key 進行計算,得到雜湊值;繼而透過呼叫 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進行儲存操作。該方法原始碼為:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //加鎖,這裡是鎖定的Segment而不是整個ConcurrentHashMap
    HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        //得到hash對應的table中的索引index
        int index = (tab.length - 1) & hash;
        //找到hash對應的是具體的哪個桶,也就是哪個HashEntry連結串列
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            else {
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                int c = count + 1;
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    rehash(node);
                else
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        //解鎖
        unlock();
    }
    return oldValue;
}

關於該方法的某些關鍵步驟,在原始碼上加上了註釋。

需要注意的是:加鎖操作是針對的 hash 值對應的某個 Segment,而不是整個 ConcurrentHashMap。因為 put 操作只是在這個 Segment 中完成,所以並不需要對整個 ConcurrentHashMap 加鎖。所以,此時,其他的執行緒也可以對另外的 Segment 進行 put 操作,因為雖然該 Segment 被鎖住了,但其他的 Segment 並沒有加鎖。同時,讀執行緒並不會因為本執行緒的加鎖而阻塞。

正是因為其內部的結構以及機制,所以 ConcurrentHashMap 在併發訪問的效能上要比Hashtable和同步包裝之後的HashMap的效能提高很多。在理想狀態下,ConcurrentHashMap 可以支援 16 個執行緒執行併發寫操作(如果併發級別設定為 16),及任意數量執行緒的讀操作。

總結

在實際的應用中,雜湊表一般的應用場景是:除了少數插入操作和刪除操作外,絕大多數都是讀取操作,而且讀操作在大多數時候都是成功的。正是基於這個前提,ConcurrentHashMap 針對讀操作做了大量的最佳化。透過 HashEntry 物件的不變性和用 volatile 型變數協調執行緒間的記憶體可見性,使得 大多數時候,讀操作不需要加鎖就可以正確獲得值。這個特性使得 ConcurrentHashMap 的併發效能在分離鎖的基礎上又有了近一步的提高。

ConcurrentHashMap 是一個併發雜湊對映表的實現,它允許完全併發的讀取,並且支援給定數量的併發更新。相比於 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的併發性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個全域性的鎖來同步不同執行緒間的併發訪問。同一時間點,只能有一個執行緒持有鎖,也就是說在同一時間點,只能有一個執行緒能訪問容器。這雖然保證多執行緒間的安全併發訪問,但同時也導致對容器的訪問變成序列化的了。

ConcurrentHashMap 的高併發性主要來自於三個方面:

  • 用分離鎖實現多個執行緒間的更深層次的共享訪問。

  • 用 HashEntery 物件的不變性來降低執行讀操作的執行緒在遍歷連結串列期間對加鎖的需求。

  • 透過對同一個 Volatile 變數的寫 / 讀訪問,協調不同執行緒間讀 / 寫操作的記憶體可見性。

使用分離鎖,減小了請求 同一個鎖的頻率。

透過 HashEntery 物件的不變性及對同一個 Volatile 變數的讀 / 寫來協調記憶體可見性,使得 讀操作大多數時候不需要加鎖就能成功獲取到需要的值。由於雜湊對映表在實際應用中大多數操作都是成功的 讀操作,所以 2 和 3 既可以減少請求同一個鎖的頻率,也可以有效減少持有鎖的時間。透過減小請求同一個鎖的頻率和儘量減少持有鎖的時間 ,使得 ConcurrentHashMap 的併發性相對於 HashTable 和用同步包裝器包裝的 HashMap有了質的提高。

39.BlockingQueue的使用?

BlockingQueue的原理

阻塞佇列(BlockingQueue)是一個支援兩個附加操作的佇列。這兩個附加的操作是:在佇列為空時,獲取元素的執行緒會等待佇列變為非空。當佇列滿時,儲存元素的執行緒會等待佇列可用。阻塞佇列常用於生產者和消費者的場景,生產者是往佇列裡新增元素的執行緒,消費者是從佇列裡拿元素的執行緒。阻塞佇列就是生產者存放元素的容器,而消費者也只從容器裡拿元素。

BlockingQueue的核心方法:

1)add(E e): 新增元素,如果BlockingQueue可以容納,則返回true,否則報異常

2)offer(E e): 新增元素,如果BlockingQueue可以容納,則返回true,否則返回false.

3)put(E e): 新增元素,如果BlockQueue沒有空間,則呼叫此方法的執行緒被阻斷直到BlockingQueue裡面有空間再繼續.

4)poll(long timeout, TimeUnit timeUnit): 取走BlockingQueue裡排在首位的物件,若不能立即取出,則可以等timeout引數規定的時間,取不到時返回null

5)take(): 取走BlockingQueue裡排在首位的物件,若BlockingQueue為空,阻斷進入等待狀態直到Blocking有新的物件被加入為止

BlockingQueue常用實現類

1)ArrayBlockingQueue: 有界的先入先出順序佇列,構造方法確定佇列的大小.

2)LinkedBlockingQueue: 無界的先入先出順序佇列,構造方法提供兩種,一種初始化佇列大小,佇列即有界;第二種預設構造方法,佇列無界(有界即Integer.MAX_VALUE)

4)SynchronousQueue: 特殊的BlockingQueue,沒有空間的佇列,即必須有取的方法阻塞在這裡的時候才能放入元素。

3)PriorityBlockingQueue: 支援優先順序的阻塞佇列 ,存入物件必須實現Comparator介面 (需要注意的是 佇列不是在加入元素的時候進行排序,而是取出的時候,根據Comparator來決定優先順序最高的)。

BlockingQueue<> 佇列的作用

BlockingQueue 實現主要用於生產者-使用者佇列,BlockingQueue 實現是執行緒安全的。所有排隊方法都可以使用內部鎖或其他形式的併發控制來自動達到它們的目的

這是一個生產者-使用者場景的一個用例。注意,BlockingQueue 可以安全地與多個生產者和多個使用者一起使用 此用例來自jdk文件

//這是一個生產者類
class Producer implements Runnable {
   private final BlockingQueue queue;
   Producer(BlockingQueue q) { 
       queue = q; 
   }
   public void run() {
     try {
       while(true) { 
           queue.put(produce()); 
       }
     } catch (InterruptedException ex) { 
         ... handle ...
         }
   }
   Object produce() { 
       ... 
   }
 }

 //這是一個消費者類
 class Consumer implements Runnable {
   private final BlockingQueue queue;
   Consumer(BlockingQueue q) { queue = q; }
   public void run() {
     try {
       while(true) { 
           consume(queue.take()); 
       }
     } catch (InterruptedException ex) { 
         ... handle ...
     }
   }
   void consume(Object x) { 
       ... 
   }
 }

 //這是實現類
 class Setup {
   void main() {
     //例項一個非阻塞佇列
     BlockingQueue q = new SomeQueueImplementation();
     //將佇列傳入兩個消費者和一個生產者中
     Producer p = new Producer(q);
     Consumer c1 = new Consumer(q);
     Consumer c2 = new Consumer(q);
     new Thread(p).start();
     new Thread(c1).start();
     new Thread(c2).start();
   }
 }

40.ThreadPool的深入考察?

引言

合理利用執行緒池能夠帶來三個好處。第一:降低資源消耗。透過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。但是要做到合理的利用執行緒池,必須對其原理了如指掌。

執行緒池的使用

我們可以透過ThreadPoolExecutor來建立一個執行緒池。

new  ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

建立一個執行緒池需要輸入幾個引數:

  • corePoolSize(執行緒池的基本大小):當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使其他空閒的基本執行緒能夠執行新任務也會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立。如果呼叫了執行緒池的prestartAllCoreThreads方法,執行緒池會提前建立並啟動所有基本執行緒。

  • runnableTaskQueue(任務佇列):用於儲存等待執行的任務的阻塞佇列。 可以選擇以下幾個阻塞佇列。

    • ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按 FIFO(先進先出)原則對元素進行排序。

    • LinkedBlockingQueue:一個基於連結串列結構的阻塞佇列,此佇列按FIFO (先進先出) 排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。

    • SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列。

    • PriorityBlockingQueue:一個具有優先順序的無限阻塞佇列。

  • maximumPoolSize(執行緒池最大大小):執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是如果使用了無界的任務佇列這個引數就沒什麼效果。

  • ThreadFactory:用於設定建立執行緒的工廠,可以透過執行緒工廠給每個建立出來的執行緒設定更有意義的名字。

  • RejectedExecutionHandler(飽和策略):當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法處理新任務時丟擲異常。以下是JDK1.5提供的四種策略。

    • AbortPolicy:直接丟擲異常。

    • CallerRunsPolicy:只用呼叫者所線上程來執行任務。

    • DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。

    • DiscardPolicy:不處理,丟棄掉。 當然也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化不能處理的任務。

  • keepAliveTime(執行緒活動保持時間):執行緒池的工作執行緒空閒後,保持存活的時間。所以如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高執行緒的利用率。

  • TimeUnit(執行緒活動保持時間的單位):可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

向執行緒池提交任務

我們可以使用execute提交的任務,但是execute方法沒有返回值,所以無法判斷任務是否被執行緒池執行成功。透過以下程式碼可知execute方法輸入的任務是一個Runnable類的例項。

threadsPool.execute(new Runnable() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
});

我們也可以使用submit 方法來提交任務,它會返回一個future,那麼我們可以透過這個future來判斷任務是否執行成功,透過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後立即返回,這時有可能任務沒有執行完。

Future<Object> future = executor.submit(harReturnValuetask);try {     Object s = future.get();
} catch (InterruptedException e) {    // 處理中斷異常} catch (ExecutionException e) {    // 處理無法執行任務異常} finally {    // 關閉執行緒池
    executor.shutdown();
}

執行緒池的關閉

我們可以透過呼叫執行緒池的shutdown或shutdownNow方法來關閉執行緒池,它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而shutdown只是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。

只要呼叫了這兩個關閉方法的其中一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true。至於我們應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫shutdown來關閉執行緒池,如果任務不一定要執行完,則可以呼叫shutdownNow。

執行緒池的分析

流程分析:執行緒池的主要工作流程如下圖:

圖片描述

從上圖我們可以看出,當提交一個新任務到執行緒池時,執行緒池的處理流程如下:

  1. 首先執行緒池判斷基本執行緒池是否已滿?沒滿,建立一個工作執行緒來執行任務。滿了,則進入下個流程。

  2. 其次執行緒池判斷工作佇列是否已滿?沒滿,則將新提交的任務儲存在工作佇列裡。滿了,則進入下個流程。

  3. 最後執行緒池判斷整個執行緒池是否已滿?沒滿,則建立一個新的工作執行緒來執行任務,滿了,則交給飽和策略來處理這個任務。

原始碼分析

上面的流程分析讓我們很直觀的瞭解了執行緒池的工作原理,讓我們再透過原始碼來看看是如何實現的。執行緒池執行任務的方法如下:

public void execute(Runnable command) {    if (command == null)       throw new NullPointerException();    //如果執行緒數小於基本執行緒數,則建立執行緒並執行當前任務 
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {    //如執行緒數大於等於基本執行緒數或執行緒建立失敗,則將當前任務放到工作佇列中。
        if (runState == RUNNING && workQueue.offer(command)) {            if (runState != RUNNING || poolSize == 0)
                      ensureQueuedTaskHandled(command);
        }    //如果執行緒池不處於執行中或任務無法放入佇列,並且當前執行緒數量小於最大允許的執行緒數量,則建立一個執行緒執行任務。        else if (!addIfUnderMaximumPoolSize(command))        //丟擲RejectedExecutionException異常
            reject(command); // is shutdown or saturated
    }
}

工作執行緒。執行緒池建立執行緒時,會將執行緒封裝成工作執行緒Worker,Worker在執行完任務後,還會無限迴圈獲取工作佇列裡的任務來執行。我們可以從Worker的run方法裡看到這點:

public void run() {     try {
           Runnable task = firstTask;
           firstTask = null;            while (task != null || (task = getTask()) != null) {
                    runTask(task);
                    task = null;
            }
      } finally {
             workerDone(this);
      }
}

合理的配置執行緒池

要想合理的配置執行緒池,就必須首先分析任務特性,可以從以下幾個角度來進行分析:

  1. 任務的性質:CPU密集型任務,IO密集型任務和混合型任務。

  2. 任務的優先順序:高,中和低。

  3. 任務的執行時間:長,中和短。

  4. 任務的依賴性:是否依賴其他系統資源,如資料庫連線。

任務性質不同的任務可以用不同規模的執行緒池分開處理。CPU密集型任務配置儘可能小的執行緒,如配置Ncpu+1個執行緒的執行緒池。IO密集型任務則由於執行緒並不是一直在執行任務,則配置儘可能多的執行緒,如2*Ncpu。混合型的任務,如果可以拆分,則將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於序列執行的吞吐率,如果這兩個任務執行時間相差太大,則沒必要進行分解。我們可以透過Runtime.getRuntime().availableProcessors()方法獲得當前裝置的CPU個數。

優先順序不同的任務可以使用優先順序佇列PriorityBlockingQueue來處理。它可以讓優先順序高的任務先得到執行,需要注意的是如果一直有優先順序高的任務提交到佇列裡,那麼優先順序低的任務可能永遠不能執行。

執行時間不同的任務可以交給不同規模的執行緒池來處理,或者也可以使用優先順序佇列,讓執行時間短的任務先執行。

依賴資料庫連線池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,如果等待的時間越長CPU空閒時間就越長,那麼執行緒數應該設定越大,這樣才能更好的利用CPU。

建議使用有界佇列,有界佇列能增加系統的穩定性和預警能力,可以根據需要設大一點,比如幾千。有一次我們組使用的後臺任務執行緒池的佇列和執行緒池全滿了,不斷的丟擲拋棄任務的異常,透過排查發現是資料庫出現了問題,導致執行SQL變得非常緩慢,因為後臺任務執行緒池裡的任務全是需要向資料庫查詢和插入資料的,所以導致執行緒池裡的工作執行緒全部阻塞住,任務積壓線上程池裡。如果當時我們設定成無界佇列,執行緒池的佇列就會越來越多,有可能會撐滿記憶體,導致整個系統不可用,而不只是後臺任務出現問題。當然我們的系統所有的任務是用的單獨的伺服器部署的,而我們使用不同規模的執行緒池跑不同型別的任務,但是出現這樣問題時也會影響到其他任務。

執行緒池的監控

透過執行緒池提供的引數進行監控。執行緒池裡有一些屬性在監控執行緒池的時候可以使用

  • taskCount:執行緒池需要執行的任務數量。

  • completedTaskCount:執行緒池在執行過程中已完成的任務數量。小於或等於taskCount。

  • largestPoolSize:執行緒池曾經建立過的最大執行緒數量。透過這個資料可以知道執行緒池是否滿過。如等於執行緒池的最大大小,則表示執行緒池曾經滿了。

  • getPoolSize:執行緒池的執行緒數量。如果執行緒池不銷燬的話,池裡的執行緒不會自動銷燬,所以這個大小隻增不+getActiveCount:獲取活動的執行緒數。

透過擴充套件執行緒池進行監控。透過繼承執行緒池並重寫執行緒池的beforeExecute,afterExecute和terminated方法,我們可以在任務執行前,執行後和執行緒池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法線上程池裡是空方法。如:

protected void beforeExecute(Thread t, Runnable r) { }

41.Java中Semaphore是什麼?

Java中的Semaphore是一種新的同步類,它是一個計數訊號。

從概念上講,訊號量維護了一個許可集合。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release()新增一個許可,從而可能釋放一個正在阻塞的獲取者。

但是,不使用實際的許可物件,Semaphore只對可用許可的號碼進行計數,並採取相應的行動。

訊號量常常用於多執行緒的程式碼中,比如資料庫連線池。

42.同步方法和同步程式碼塊的區別是什麼?

同步方法預設用this或者當前類class物件作為鎖; 同步程式碼塊可以選擇以什麼來加鎖,比同步方法要更細顆粒度,我們可以選擇只同步會發生同步問題的部分程式碼而不是整個方法; 同步方法使用關鍵字 synchronized修飾方法,而同步程式碼塊主要是修飾需要進行同步的程式碼,用 synchronized(object){程式碼內容}進行修飾;

43.如何確保N個執行緒可以訪問N個資源同時又不導致死鎖?

使用多執行緒的時候,一種非常簡單的避免死鎖的方式就是:指定獲取鎖的順序,並強制執行緒按照指定的順序獲取鎖。因此,如果所有的執行緒都是以同樣的順序加鎖和釋放鎖,就不會出現死鎖了。

 

原文:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2144/viewspace-2823253/,如需轉載,請註明出處,否則將追究法律責任。

相關文章