萬字圖解Java多執行緒

猿小布發表於2020-09-06

前言

java多執行緒我個人覺得是javaSe中最難的一部分,我以前也是感覺學會了,但是真正有多執行緒的需求卻不知道怎麼下手,實際上還是對多執行緒這塊知識瞭解不深刻,不知道多執行緒api的應用場景,不知道多執行緒的執行流程等等,本篇文章將使用例項+圖解+原始碼的方式來解析java多執行緒。

文章篇幅較長,大家也可以有選擇的看具體章節,建議多執行緒的程式碼全部手敲,永遠不要相信你看到的結論,自己編碼後執行出來的,才是自己的。

什麼是java多執行緒?

程式與執行緒

程式

  • 當一個程式被執行,就開啟了一個程式, 比如啟動了qq,word
  • 程式由指令和資料組成,指令要執行,資料要載入,指令被cpu載入執行,資料被載入到記憶體,指令執行時可由cpu排程硬碟、網路等裝置

執行緒

  • 一個程式內可分為多個執行緒
  • 一個執行緒就是一個指令流,cpu排程的最小單位,由cpu一條一條執行指令

並行與併發

併發:單核cpu執行多執行緒時,時間片進行很快的切換。執行緒輪流執行cpu

並行:多核cpu執行 多執行緒時,真正的在同一時刻執行

java提供了豐富的api來支援多執行緒。

為什麼用多執行緒?

多執行緒能實現的都可以用單執行緒來完成,那單執行緒執行的好好的,為什麼java要引入多執行緒的概念呢?

多執行緒的好處:

  1. 程式執行的更快!快!快!

  2. 充分利用cpu資源,目前幾乎沒有線上的cpu是單核的,發揮多核cpu強大的能力

多執行緒難在哪裡?

單執行緒只有一條執行線,過程容易理解,可以在大腦中清晰的勾勒出程式碼的執行流程

多執行緒卻是多條線,而且一般多條線之間有互動,多條線之間需要通訊,一般難點有以下幾點

  1. 多執行緒的執行結果不確定,受到cpu排程的影響
  2. 多執行緒的安全問題
  3. 執行緒資源寶貴,依賴執行緒池操作執行緒,執行緒池的引數設定問題
  4. 多執行緒執行是動態的,同時的,難以追蹤過程
  5. 多執行緒的底層是作業系統層面的,原始碼難度大

有時候希望自己變成一個位元組穿梭於伺服器中,搞清楚來龍去脈,就像無敵破壞王一樣(沒看過這部電影的可以看下,腦洞大開)。

java多執行緒的基本使用

定義任務、建立和執行執行緒

任務: 執行緒的執行體。也就是我們的核心程式碼邏輯

定義任務

  1. 繼承Thread類 (可以說是 將任務和執行緒合併在一起)
  2. 實現Runnable介面 (可以說是 將任務和執行緒分開了)
  3. 實現Callable介面 (利用FutureTask執行任務)

Thread實現任務的侷限性

  1. 任務邏輯寫在Thread類的run方法中,有單繼承的侷限性
  2. 建立多執行緒時,每個任務有成員變數時不共享,必須加static才能做到共享

Runnable和Callable解決了Thread的侷限性

但是Runbale相比Callable有以下的侷限性

  1. 任務沒有返回值
  2. 任務無法拋異常給呼叫方

如下程式碼 幾種定義執行緒的方式

@Slf4j
class T extends Thread {
    @Override
    public void run() {
        log.info("我是繼承Thread的任務");
    }
}
@Slf4j
class R implements Runnable {

    @Override
    public void run() {
        log.info("我是實現Runnable的任務");
    }
}
@Slf4j
class C implements Callable<String> {

    @Override
    public String call() throws Exception {
        log.info("我是實現Callable的任務");
        return "success";
    }
}

建立執行緒的方式

  1. 通過Thread類直接建立執行緒
  2. 利用執行緒池內部建立執行緒

啟動執行緒的方式

  • 呼叫執行緒的start()方法
// 啟動繼承Thread類的任務
new T().start();

// 啟動繼承Thread匿名內部類的任務 可用lambda優化
Thread t = new Thread(){
  @Override
  public void run() {
    log.info("我是Thread匿名內部類的任務");
  }
};

//  啟動實現Runnable介面的任務
new Thread(new R()).start();

//  啟動實現Runnable匿名實現類的任務
new Thread(new Runnable() {
    @Override
    public void run() {
        log.info("我是Runnable匿名內部類的任務");
    }
}).start();

//  啟動實現Runnable的lambda簡化後的任務
new Thread(() -> log.info("我是Runnable的lambda簡化後的任務")).start();

// 啟動實現了Callable介面的任務 結合FutureTask 可以獲取執行緒執行的結果
FutureTask<String> target = new FutureTask<>(new C());
new Thread(target).start();
log.info(target.get());

以上各個執行緒相關的類的類圖如下

上下文切換

多核cpu下,多執行緒是並行工作的,如果執行緒數多,單個核又會併發的排程執行緒,執行時會有上下文切換的概念

cpu執行執行緒的任務時,會為執行緒分配時間片,以下幾種情況會發生上下文切換。

  1. 執行緒的cpu時間片用完
  2. 垃圾回收
  3. 執行緒自己呼叫了 sleep、yield、wait、join、park、synchronized、lock 等方法

當發生上下文切換時,作業系統會儲存當前執行緒的狀態,並恢復另一個執行緒的狀態,jvm中有塊記憶體地址叫程式計數器,用於記錄執行緒執行到哪一行程式碼,是執行緒私有的。

idea打斷點的時候可以設定為Thread模式,idea的debug模式可以看出棧幀的變化

執行緒的禮讓-yield()&執行緒的優先順序

yield()方法會讓執行中的執行緒切換到就緒狀態,重新爭搶cpu的時間片,爭搶時是否獲取到時間片看cpu的分配。

程式碼如下

// 方法的定義
public static native void yield();

Runnable r1 = () -> {
    int count = 0;
    for (;;){
       log.info("---- 1>" + count++);
    }
};
Runnable r2 = () -> {
    int count = 0;
    for (;;){
        Thread.yield();
        log.info("            ---- 2>" + count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();

// 執行結果
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield -             ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518

如上述結果所示,t2執行緒每次執行時進行了yield(),執行緒1執行的機會明顯比執行緒2要多。

執行緒的優先順序

​ 執行緒內部用1~10的數來調整執行緒的優先順序,預設的執行緒優先順序為NORM_PRIORITY:5

​ cpu比較忙時,優先順序高的執行緒獲取更多的時間片

​ cpu比較閒時,優先順序設定基本沒用

 public final static int MIN_PRIORITY = 1;

 public final static int NORM_PRIORITY = 5;

 public final static int MAX_PRIORITY = 10;
 
 // 方法的定義
 public final void setPriority(int newPriority) {
 }

cpu比較忙時

Runnable r1 = () -> {
    int count = 0;
    for (;;){
       log.info("---- 1>" + count++);
    }
};
Runnable r2 = () -> {
    int count = 0;
    for (;;){
        log.info("            ---- 2>" + count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

// 可能的執行結果
11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>44102
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135903
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135904
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135905
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135906

cpu比較閒時

Runnable r1 = () -> {
    int count = 0;
    for (int i = 0; i < 10; i++) {
        log.info("---- 1>" + count++);
    }
};
Runnable r2 = () -> {
    int count = 0;
    for (int i = 0; i < 10; i++) {
        log.info("            ---- 2>" + count++);

    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

// 可能的執行結果 執行緒1優先順序低 卻先執行完
12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>7
12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>8
12:01:09.916 [t1] INFO thread.TestYieldPriority - ---- 1>9
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>2
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>3
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>4
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>5
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>6
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>7
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>8
12:01:09.916 [t2] INFO thread.TestYieldPriority -             ---- 2>9

守護執行緒

預設情況下,java程式需要等待所有執行緒都執行結束,才會結束,有一種特殊執行緒叫守護執行緒,當所有的非守護執行緒都結束後,即使它沒有執行完,也會強制結束。

預設的執行緒都是非守護執行緒。

垃圾回收執行緒就是典型的守護執行緒

// 方法的定義
public final void setDaemon(boolean on) {
}

Thread thread = new Thread(() -> {
    while (true) {
    }
});
// 具體的api。設為true表示未守護執行緒,當主執行緒結束後,守護執行緒也結束。
// 預設是false,當主執行緒結束後,thread繼續執行,程式不停止
thread.setDaemon(true);
thread.start();
log.info("結束");

執行緒的阻塞

執行緒的阻塞可以分為好多種,從作業系統層面和java層面阻塞的定義可能不同,但是廣義上使得執行緒阻塞的方式有下面幾種

  1. BIO阻塞,即使用了阻塞式的io流
  2. sleep(long time) 讓執行緒休眠進入阻塞狀態
  3. a.join() 呼叫該方法的執行緒進入阻塞,等待a執行緒執行完恢復執行
  4. sychronized或ReentrantLock 造成執行緒未獲得鎖進入阻塞狀態 (同步鎖章節細說)
  5. 獲得鎖之後呼叫wait()方法 也會讓執行緒進入阻塞狀態 (同步鎖章節細說)
  6. LockSupport.park() 讓執行緒進入阻塞狀態 (同步鎖章節細說)

sleep()

​ 使執行緒休眠,會將執行中的執行緒進入阻塞狀態。當休眠時間結束後,重新爭搶cpu的時間片繼續執行

// 方法的定義 native方法
public static native void sleep(long millis) throws InterruptedException; 

try {
   // 休眠2秒
   // 該方法會丟擲 InterruptedException異常 即休眠過程中可被中斷,被中斷後丟擲異常
   Thread.sleep(2000);
 } catch (InterruptedException異常 e) {
 }
 try {
   // 使用TimeUnit的api可替代 Thread.sleep 
   TimeUnit.SECONDS.sleep(1);
 } catch (InterruptedException e) {
 }

join()

​ join是指呼叫該方法的執行緒進入阻塞狀態,等待某執行緒執行完成後恢復執行

// 方法的定義 有過載
// 等待執行緒執行完才恢復執行
public final void join() throws InterruptedException {
}
// 指定join的時間。指定時間內 執行緒還未執行完 呼叫方執行緒不繼續等待就恢復執行
public final synchronized void join(long millis)
    throws InterruptedException{}

Thread t = new Thread(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    r = 10;
});

t.start();
// 讓主執行緒阻塞 等待t執行緒執行完才繼續執行 
// 去除該行,執行結果為0,加上該行 執行結果為10
t.join();
log.info("r:{}", r);

// 執行結果
13:09:13.892 [main] INFO thread.TestJoin - r:10

執行緒的打斷-interrupt()

// 相關方法的定義
public void interrupt() {
}
public boolean isInterrupted() {
}
public static boolean interrupted() {
}

打斷標記:執行緒是否被打斷,true表示被打斷了,false表示沒有

isInterrupted() 獲取執行緒的打斷標記 ,呼叫後不會修改執行緒的打斷標記

interrupt()方法用於中斷執行緒

  1. 可以打斷sleep,wait,join等顯式的丟擲InterruptedException方法的執行緒,但是打斷後,執行緒的打斷標記還是false
  2. 打斷正常執行緒 ,執行緒不會真正被中斷,但是執行緒的打斷標記為true

interrupted() 獲取執行緒的打斷標記,呼叫後清空打斷標記 即如果獲取為true 呼叫後打斷標記為false (不常用)

interrupt例項: 有個後臺監控執行緒不停的監控,當外界打斷它時,就結束執行。程式碼如下

@Slf4j
class TwoPhaseTerminal{
    // 監控執行緒
    private Thread monitor;

    public void start(){
        monitor = new Thread(() ->{
           // 不停的監控
            while (true){
                Thread thread = Thread.currentThread();
             	// 判斷當前執行緒是否被打斷
                if (thread.isInterrupted()){
                    log.info("當前執行緒被打斷,結束執行");
                    break;
                }
                try {
                    Thread.sleep(1000);
                	// 監控邏輯中被打斷後,打斷標記為true
                    log.info("監控");
                } catch (InterruptedException e) {
                    // 睡眠時被打斷時丟擲異常 在該處捕獲到 此時打斷標記還是false
                    // 在呼叫一次中斷 使得中斷標記為true
                    thread.interrupt();
                }
            }
        });
        monitor.start();
    }

    public void stop(){
        monitor.interrupt();
    }
}

執行緒的狀態

上面說了一些基本的api的使用,呼叫上面的方法後都會使得執行緒有對應的狀態。

執行緒的狀態可從 作業系統層面分為五種狀態 從java api層面分為六種狀態。

五種狀態

  1. 初始狀態:建立執行緒物件時的狀態
  2. 可執行狀態(就緒狀態):呼叫start()方法後進入就緒狀態,也就是準備好被cpu排程執行
  3. 執行狀態:執行緒獲取到cpu的時間片,執行run()方法的邏輯
  4. 阻塞狀態: 執行緒被阻塞,放棄cpu的時間片,等待解除阻塞重新回到就緒狀態爭搶時間片
  5. 終止狀態: 執行緒執行完成或丟擲異常後的狀態

六種狀態

Thread類中的內部列舉State

public enum State {
	NEW,
	RUNNABLE,
	BLOCKED,
	WAITING,
	TIMED_WAITING,
	TERMINATED;
}
  1. NEW 執行緒物件被建立
  2. Runnable 執行緒呼叫了start()方法後進入該狀態,該狀態包含了三種情況
    1. 就緒狀態 :等待cpu分配時間片
    2. 執行狀態:進入Runnable方法執行任務
    3. 阻塞狀態:BIO 執行阻塞式io流時的狀態
  3. Blocked 沒獲取到鎖時的阻塞狀態(同步鎖章節會細說)
  4. WAITING 呼叫wait()、join()等方法後的狀態
  5. TIMED_WAITING 呼叫 sleep(time)、wait(time)、join(time)等方法後的狀態
  6. TERMINATED 執行緒執行完成或丟擲異常後的狀態

六種執行緒狀態和方法的對應關係

執行緒的相關方法總結

主要總結Thread類中的核心方法

方法名稱 是否static 方法說明
start() 讓執行緒啟動,進入就緒狀態,等待cpu分配時間片
run() 重寫Runnable介面的方法,執行緒獲取到cpu時間片時執行的具體邏輯
yield() 執行緒的禮讓,使得獲取到cpu時間片的執行緒進入就緒狀態,重新爭搶時間片
sleep(time) 執行緒休眠固定時間,進入阻塞狀態,休眠時間完成後重新爭搶時間片,休眠可被打斷
join()/join(time) 呼叫執行緒物件的join方法,呼叫者執行緒進入阻塞,等待執行緒物件執行完或者到達指定時間才恢復,重新爭搶時間片
isInterrupted() 獲取執行緒的打斷標記,true:被打斷,false:沒有被打斷。呼叫後不會修改打斷標記
interrupt() 打斷執行緒,丟擲InterruptedException異常的方法均可被打斷,但是打斷後不會修改打斷標記,正常執行的執行緒被打斷後會修改打斷標記
interrupted() 獲取執行緒的打斷標記。呼叫後會清空打斷標記
stop() 停止執行緒執行 不推薦
suspend() 掛起執行緒 不推薦
resume() 恢復執行緒執行 不推薦
currentThread() 獲取當前執行緒

Object中與執行緒相關方法

方法名稱 方法說明
wait()/wait(long timeout) 獲取到鎖的執行緒進入阻塞狀態
notify() 隨機喚醒被wait()的一個執行緒
notifyAll(); 喚醒被wait()的所有執行緒,重新爭搶時間片

同步鎖

執行緒安全

  • 一個程式執行多個執行緒本身是沒有問題的
  • 問題有可能出現在多個執行緒訪問共享資源
    • 多個執行緒都是讀共享資源也是沒有問題的
    • 當多個執行緒讀寫共享資源時,如果發生指令交錯,就會出現問題

臨界區: 一段程式碼如果對共享資源的多執行緒讀寫操作,這段程式碼就被稱為臨界區。

注意的是 指令交錯指的是 java程式碼在解析成位元組碼檔案時,java程式碼的一行程式碼在位元組碼中可能有多行,線上程上下文切換時就有可能交錯。

執行緒安全指的是多執行緒呼叫同一個物件的臨界區的方法時,物件的屬性值一定不會發生錯誤,這就是保證了執行緒安全。

如下面不安全的程式碼

// 物件的成員變數
private static int count = 0;

public static void main(String[] args) throws InterruptedException {
  // t1執行緒對變數+5000次
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            count++;
        }
    });
  // t2執行緒對變數-5000次
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            count--;
        }
    });

    t1.start();
    t2.start();

    // 讓t1 t2都執行完
    t1.join();
    t2.join();
    System.out.println(count);
}

// 執行結果 
-1399

上面的程式碼 兩個執行緒,一個+5000次,一個-5000次,如果執行緒安全,count的值應該還是0。

但是執行很多次,每次的結果不同,且都不是0,所以是執行緒不安全的。

執行緒安全的類一定所有的操作都執行緒安全嗎?

開發中經常會說到一些執行緒安全的類,如ConcurrentHashMap,執行緒安全指的是類裡每一個獨立的方法是執行緒安全的,但是方法的組合就不一定是執行緒安全的

成員變數和靜態變數是否執行緒安全?

  • 如果沒有多執行緒共享,則執行緒安全
  • 如果存在多執行緒共享
    • 多執行緒只有讀操作,則執行緒安全
    • 多執行緒存在寫操作,寫操作的程式碼又是臨界區,則執行緒不安全

區域性變數是否執行緒安全?

  • 區域性變數是執行緒安全的
  • 區域性變數引用的物件未必是執行緒安全的
    • 如果該物件沒有逃離該方法的作用範圍,則執行緒安全
    • 如果該物件逃離了該方法的作用範圍,比如:方法的返回值,需要考慮執行緒安全

synchronized

同步鎖也叫物件鎖,是鎖在物件上的,不同的物件就是不同的鎖。

該關鍵字是用於保證執行緒安全的,是阻塞式的解決方案。

讓同一個時刻最多隻有一個執行緒能持有物件鎖,其他執行緒在想獲取這個物件鎖就會被阻塞,不用擔心上下文切換的問題。

注意: 不要理解為一個執行緒加了鎖 ,進入 synchronized程式碼塊中就會一直執行下去。如果時間片切換了,也會執行其他執行緒,再切換回來會緊接著執行,只是不會執行到有競爭鎖的資源,因為當前執行緒還未釋放鎖。

當一個執行緒執行完synchronized的程式碼塊後 會喚醒正在等待的執行緒

synchronized實際上使用物件鎖保證臨界區的原子性 臨界區的程式碼是不可分割的 不會因為執行緒切換所打斷

基本使用

// 加在方法上 實際是對this物件加鎖
private synchronized void a() {
}

// 同步程式碼塊,鎖物件可以是任意的,加在this上 和a()方法作用相同
private void b(){
    synchronized (this){

    }
}

// 加在靜態方法上 實際是對類物件加鎖
private synchronized static void c() {

}

// 同步程式碼塊 實際是對類物件加鎖 和c()方法作用相同
private void d(){
    synchronized (TestSynchronized.class){
        
    }
}

// 上述b方法對應的位元組碼原始碼 其中monitorenter就是加鎖的地方
 0 aload_0
 1 dup
 2 astore_1
 3 monitorenter
 4 aload_1
 5 monitorexit
 6 goto 14 (+8)
 9 astore_2
10 aload_1
11 monitorexit
12 aload_2
13 athrow
14 return

執行緒安全的程式碼

private static int count = 0;

private static Object lock = new Object();

private static Object lock2 = new Object();

 // t1執行緒和t2物件都是對同一物件加鎖。保證了執行緒安全。此段程式碼無論執行多少次,結果都是0
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (lock) {
                count++;
            }
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (lock) {
                count--;
            }
        }
    });
 
    t1.start();
    t2.start();

    // 讓t1 t2都執行完
    t1.join();
    t2.join();
    System.out.println(count);
}

重點:加鎖是加在物件上,一定要保證是同一物件,加鎖才能生效

執行緒通訊

wait+notify

執行緒間通訊可以通過共享變數+wait()&notify()來實現

wait()將執行緒進入阻塞狀態,notify()將執行緒喚醒

當多執行緒競爭訪問物件的同步方法時,鎖物件會關聯一個底層的Monitor物件(重量級鎖的實現)

如下圖所示 Thread0,1先競爭到鎖執行了程式碼後,2,3,4,5執行緒同時來執行臨界區的程式碼,開始競爭鎖

  1. Thread-0先獲取到物件的鎖,關聯到monitor的owner,同步程式碼塊內呼叫了鎖物件的wait()方法,呼叫後會進入waitSet等待,Thread-1同樣如此,此時Thread-0的狀態為Waitting
  2. Thread2、3、4、5同時競爭,2獲取到鎖後,關聯了monitor的owner,3、4、5只能進入EntryList中等待,此時2執行緒狀態為 Runnable,3、4、5狀態為Blocked
  3. 2執行後,喚醒entryList中的執行緒,3、4、5進行競爭鎖,獲取到的執行緒即會關聯monitor的owner
  4. 3、4、5執行緒在執行過程中,呼叫了鎖物件的notify()或notifyAll()時,會喚醒waitSet的執行緒,喚醒的執行緒進入entryList等待重新競爭鎖

注意:

  1. Blocked狀態和Waitting狀態都是阻塞狀態

  2. Blocked執行緒會在owner執行緒釋放鎖時喚醒

  3. wait和notify使用場景是必須要有同步,且必須獲得物件的鎖才能呼叫,使用鎖物件去呼叫,否則會拋異常

  • wait() 釋放鎖 進入 waitSet 可傳入時間,如果指定時間內未被喚醒 則自動喚醒
  • notify()隨機喚醒一個waitSet裡的執行緒
  • notifyAll()喚醒waitSet中所有的執行緒
static final Object lock = new Object();
new Thread(() -> {
    synchronized (lock) {
        log.info("開始執行");
        try {
          	// 同步程式碼內部才能呼叫
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("繼續執行核心邏輯");
    }
}, "t1").start();

new Thread(() -> {
    synchronized (lock) {
        log.info("開始執行");
        try {
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("繼續執行核心邏輯");
    }
}, "t2").start();

try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
log.info("開始喚醒");

synchronized (lock) {
  // 同步程式碼內部才能呼叫
    lock.notifyAll();
}
// 執行結果
14:29:47.138 [t1] INFO TestWaitNotify - 開始執行
14:29:47.141 [t2] INFO TestWaitNotify - 開始執行
14:29:49.136 [main] INFO TestWaitNotify - 開始喚醒
14:29:49.136 [t2] INFO TestWaitNotify - 繼續執行核心邏輯
14:29:49.136 [t1] INFO TestWaitNotify - 繼續執行核心邏輯

wait 和 sleep的區別?

二者都會讓執行緒進入阻塞狀態,有以下區別

  1. wait是Object的方法 sleep是Thread的方法
  2. wait會立即釋放鎖 sleep不會釋放鎖
  3. wait後執行緒的狀態是Watting sleep後執行緒的狀態為 Time_Waiting

park&unpark

LockSupport是juc下的工具類,提供了park和unpark方法,可以實現執行緒通訊

與wait和notity相比的不同點

  1. wait 和notify需要獲取物件鎖 park unpark不要
  2. unpark 可以指定喚醒執行緒 notify隨機喚醒
  3. park和unpark的順序可以先unpark wait和notify的順序不能顛倒

生產者消費者模型

指的是有生產者來生產資料,消費者來消費資料,生產者生產滿了就不生產了,通知消費者取,等消費了再進行生產。

消費者消費不到了就不消費了,通知生產者生產,生產到了再繼續消費。

  public static void main(String[] args) throws InterruptedException {
        MessageQueue queue = new MessageQueue(2);
		
		// 三個生產者向佇列裡存值
        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(() -> {
                queue.put(new Message(id, "值" + id));
            }, "生產者" + i).start();
        }

        Thread.sleep(1000);

		// 一個消費者不停的從佇列裡取值
        new Thread(() -> {
            while (true) {
                queue.take();
            }
        }, "消費者").start();

    }
}


// 訊息佇列被生產者和消費者持有
class MessageQueue {
    private LinkedList<Message> list = new LinkedList<>();

    // 容量
    private int capacity;

    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }

    /**
     * 生產
     */
    public void put(Message message) {
        synchronized (list) {
            while (list.size() == capacity) {
                log.info("佇列已滿,生產者等待");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.addLast(message);
            log.info("生產訊息:{}", message);
            // 生產後通知消費者
            list.notifyAll();
        }
    }

    public Message take() {
        synchronized (list) {
            while (list.isEmpty()) {
                log.info("佇列已空,消費者等待");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            Message message = list.removeFirst();
            log.info("消費訊息:{}", message);
            // 消費後通知生產者
            list.notifyAll();
            return message;
        }
    }


}
 // 訊息
class Message {

    private int id;

    private Object value;
}

同步鎖案例

為了更形象的表達加同步鎖的概念,這裡舉一個生活中的例子,儘量把以上的概念具體化出來。

這裡舉一個每個人非常感興趣的一件東西。 錢!!!(馬老師除外)。

現實中,我們去銀行門口的自動取款機取錢,取款機的錢就是共享變數,為了保障安全,不可能兩個陌生人同時進入同一個取款機內取錢,所以只能一個人進入取錢,然後鎖上取款機的門,其他人只能在取款機門口等待。

取款機有多個,裡面的錢互不影響,鎖也有多個(多個物件鎖),取錢人在多個取款機裡同時取錢也沒有安全問題。

假如每個取錢的陌生人都是執行緒,當取錢人進入取款機鎖了門後(執行緒獲得鎖),取到錢後出門(執行緒釋放鎖),下一個人競爭到鎖來取錢。

假設工作人員也是一個執行緒,如果取錢人進入後發現取款機錢不足了,這時通知工作人員來向取款機里加錢(呼叫notifyAll方法),取錢人暫停取錢,進入銀行大堂阻塞等待(呼叫wait方法)。

銀行大堂裡的工作人員和取錢人都被喚醒,重新競爭鎖,進入後如果是取錢人,由於取款機沒錢,還得進入銀行大堂等待。

當工作人員獲得取款機的鎖進入後,加了錢後會通知大廳裡的人來取錢(呼叫notifyAll方法)。自己暫停加錢,進入銀行大堂等待喚醒加錢(呼叫wait方法)。

這時大堂裡等待的人都來競爭鎖,誰獲取到誰進入繼續取錢。

和現實中不同的就是這裡沒有排隊的概念,誰搶到鎖誰進去取。

ReentrantLock

可重入鎖 : 一個執行緒獲取到物件的鎖後,執行方法內部在需要獲取鎖的時候是可以獲取到的。如以下程式碼

private static final ReentrantLock LOCK = new ReentrantLock();

private static void m() {
    LOCK.lock();
    try {
        log.info("begin");
      	// 呼叫m1()
        m1();
    } finally {
        // 注意鎖的釋放
        LOCK.unlock();
    }
}
public static void m1() {
    LOCK.lock();
    try {
        log.info("m1");
        m2();
    } finally {
        // 注意鎖的釋放
        LOCK.unlock();
    }
}

synchronized 也是可重入鎖,ReentrantLock有以下優點

  1. 支援獲取鎖的超時時間
  2. 獲取鎖時可被打斷
  3. 可設為公平鎖
  4. 可以有不同的條件變數,即有多個waitSet,可以指定喚醒

api

// 預設非公平鎖,引數傳true 表示未公平鎖
ReentrantLock lock = new ReentrantLock(false);
// 嘗試獲取鎖
lock()
// 釋放鎖 應放在finally塊中 必須執行到
unlock()
try {
    // 獲取鎖時可被打斷,阻塞中的執行緒可被打斷
    LOCK.lockInterruptibly();
} catch (InterruptedException e) {
    return;
}
// 嘗試獲取鎖 獲取不到就返回false
LOCK.tryLock()
// 支援超時時間 一段時間沒獲取到就返回false
tryLock(long timeout, TimeUnit unit)
// 指定條件變數 休息室 一個鎖可以建立多個休息室
Condition waitSet = ROOM.newCondition();
// 釋放鎖  進入waitSet等待 釋放後其他執行緒可以搶鎖
yanWaitSet.await()
// 喚醒具體休息室的執行緒 喚醒後 重寫競爭鎖
yanWaitSet.signal()

例項:一個執行緒輸出a,一個執行緒輸出b,一個執行緒輸出c,abc按照順序輸出,連續輸出5次

這個考的就是執行緒的通訊,利用 wait()/notify()和控制變數可以實現,此處使用ReentrantLock即可實現該功能。

  public static void main(String[] args) {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        // 構建三個條件變數
        Condition a = awaitSignal.newCondition();
        Condition b = awaitSignal.newCondition();
        Condition c = awaitSignal.newCondition();
        // 開啟三個執行緒
        new Thread(() -> {
            awaitSignal.print("a", a, b);
        }).start();

        new Thread(() -> {
            awaitSignal.print("b", b, c);
        }).start();

        new Thread(() -> {
            awaitSignal.print("c", c, a);
        }).start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        awaitSignal.lock();
        try {
            // 先喚醒a
            a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }


}

class AwaitSignal extends ReentrantLock {

    // 迴圈次數
    private int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    /**
     * @param print   輸出的字元
     * @param current 當前條件變數
     * @param next    下一個條件變數
     */
    public void print(String print, Condition current, Condition next) {

        for (int i = 0; i < loopNumber; i++) {
            lock();
            try {
                try {
                    // 獲取鎖之後等待
                    current.await();
                    System.out.print(print);
                } catch (InterruptedException e) {
                }
                next.signal();
            } finally {
                unlock();
            }
        }
    }

死鎖

說到死鎖,先舉個例子,

下面是程式碼實現

static Beer beer = new Beer();
static Story story = new Story();

public static void main(String[] args) {
    new Thread(() ->{
        synchronized (beer){
            log.info("我有酒,給我故事");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (story){
                log.info("小王開始喝酒講故事");
            }
        }
    },"小王").start();

    new Thread(() ->{
        synchronized (story){
            log.info("我有故事,給我酒");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (beer){
                log.info("老王開始喝酒講故事");
            }
        }
    },"老王").start();
}
class Beer {
}

class Story{
}

死鎖導致程式無法正常執行下去

檢測工具可以檢查到死鎖資訊

java記憶體模型(JMM)

jmm 體現在以下三個方面

  1. 原子性 保證指令不會受到上下文切換的影響
  2. 可見性 保證指令不會受到cpu快取的影響
  3. 有序性 保證指令不會受並行優化的影響

可見性

停不下來的程式

static boolean run = true;

public static void main(String[] args) throws InterruptedException {
    Thread t = new Thread(() -> {
        while (run) {
            // ....
        }
    });
    t.start();
    Thread.sleep(1000);
   // 執行緒t不會如預想的停下來
    run = false; 
}

如上圖所示,執行緒有自己的工作快取,當主執行緒修改了變數並同步到主記憶體時,t執行緒沒有讀取到,所以程式停不下來

有序性

JVM在不影響程式正確性的情況下可能會調整語句的執行順序,該情況也稱為 指令重排序

  static int i;
  static int j;
// 在某個執行緒內執行如下賦值操作
        i = ...;
        j = ...;
  有可能將j先賦值

原子性

原子性大家應該比較熟悉,上述同步鎖的synchronized程式碼塊就是保證了原子性,就是一段程式碼是一個整體,原子性保證了執行緒安全,不會受到上下文切換的影響。

volatile

該關鍵字解決了可見性和有序性,volatile通過記憶體屏障來實現的

  • 寫屏障

會在物件寫操作之後加寫屏障,會對寫屏障的之前的資料都同步到主存,並且保證寫屏障的執行順序在寫屏障之前

  • 讀屏障

會在物件讀操作之前加讀屏障,會在讀屏障之後的語句都從主存讀,並保證讀屏障之後的程式碼執行在讀屏障之後

注意: volatile不能解決原子性,即不能通過該關鍵字實現執行緒安全。

volatile應用場景:一個執行緒讀取變數,另外的執行緒操作變數,加了該關鍵字後保證寫變數後,讀變數的執行緒可以及時感知。

無鎖-cas

cas (compare and swap) 比較並交換

為變數賦值時,從記憶體中讀取到的值v,獲取到要交換的新值n,執行 compareAndSwap()方法時,比較v和當前記憶體中的值是否一致,如果一致則將n和v交換,如果不一致,則自旋重試。

cas底層是cpu層面的,即不使用同步鎖也可以保證操作的原子性。

private AtomicInteger balance;

// 模擬cas的具體操作
@Override
public void withdraw(Integer amount) {
    while (true) {
        // 獲取當前值
        int pre = balance.get();
        // 進行操作後得到新值
        int next = pre - amount;
        // 比較並設定成功 則中斷 否則自旋重試
        if (balance.compareAndSet(pre, next)) {
            break;
        }
    }
}

無鎖的效率是要高於之前的鎖的,由於無鎖不會涉及執行緒的上下文切換

cas是樂觀鎖的思想,sychronized是悲觀鎖的思想

cas適合很少有執行緒競爭的場景,如果競爭很強,重試經常發生,反而降低效率

juc併發包下包含了實現了cas的原子類

  1. AtomicInteger/AtomicBoolean/AtomicLong
  2. AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray
  3. AtomicReference/AtomicStampedReference/AtomicMarkableReference

AtomicInteger

常用api

new AtomicInteger(balance)
get()
compareAndSet(pre, next)
//        i.incrementAndGet() ++i
//        i.decrementAndGet() --i
//        i.getAndIncrement() i++
//        i.getAndDecrement() ++i
 i.addAndGet()
  // 傳入函式式介面 修改i
  int getAndUpdate(IntUnaryOperator updateFunction)
  // cas 的核心方法
  compareAndSet(int expect, int update)

ABA問題

cas存在ABA問題,即比較並交換時,如果原值為A,有其他執行緒將其修改為B,在有其他執行緒將其修改為A。

此時實際發生過交換,但是比較和交換由於值沒改變可以交換成功

解決方式

AtomicStampedReference/AtomicMarkableReference

上面兩個類解決ABA問題,原理就是為物件增加版本號,每次修改時增加版本號,就可以避免ABA問題

或者增加個布林變數標識,修改後調整布林變數值,也可以避免ABA問題

執行緒池

執行緒池的介紹

執行緒池是java併發最重要的一個知識點,也是難點,是實際應用最廣泛的。

執行緒的資源很寶貴,不可能無限的建立,必須要有管理執行緒的工具,執行緒池就是一種管理執行緒的工具,java開發中經常有池化的思想,如 資料庫連線池、Redis連線池等。

預先建立好一些執行緒,任務提交時直接執行,既可以節約建立執行緒的時間,又可以控制執行緒的數量。

執行緒池的好處

  1. 降低資源消耗,通過池化思想,減少建立執行緒和銷燬執行緒的消耗,控制資源
  2. 提高響應速度,任務到達時,無需建立執行緒即可執行
  3. 提供更多更強大的功能,可擴充套件性高

執行緒池的構造方法

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
 
}

構造器引數的意義

引數名 引數意義
corePoolSize 核心執行緒數
maximumPoolSize 最大執行緒數
keepAliveTime 救急執行緒的空閒時間
unit 救急執行緒的空閒時間單位
workQueue 阻塞佇列
threadFactory 建立執行緒的工廠,主要定義執行緒名
handler 拒絕策略

執行緒池案例

下面 我們通過一個例項來理解執行緒池的引數以及執行緒池的接收任務的過程

如上圖 銀行辦理業務。

  1. 客戶到銀行時,開啟櫃檯進行辦理,櫃檯相當於執行緒,客戶相當於任務,有兩個是常開的櫃檯,三個是臨時櫃檯。2就是核心執行緒數,5是最大執行緒數。即有兩個核心執行緒
  2. 當櫃檯開到第二個後,都還在處理業務。客戶再來就到排隊大廳排隊。排隊大廳只有三個座位。
  3. 排隊大廳坐滿時,再來客戶就繼續開櫃檯處理,目前最大有三個臨時櫃檯,也就是三個救急執行緒
  4. 此時再來客戶,就無法正常為其 提供業務,採用拒絕策略來處理它們
  5. 當櫃檯處理完業務,就會從排隊大廳取任務,當櫃檯隔一段空閒時間都取不到任務時,如果當前執行緒數大於核心執行緒數時,就會回收執行緒。即撤銷該櫃檯。

執行緒池的狀態

執行緒池通過一個int變數的高3位來表示執行緒池的狀態,低29位來儲存執行緒池的數量

狀態名稱 高三位 接收新任務 處理阻塞佇列任務 說明
Running 111 Y Y 正常接收任務,正常處理任務
Shutdown 000 N Y 不會接收任務,會執行完正在執行的任務,也會處理阻塞佇列裡的任務
stop 001 N N 不會接收任務,會中斷正在執行的任務,會放棄處理阻塞佇列裡的任務
Tidying 010 N N 任務全部執行完畢,當前活動執行緒是0,即將進入終結
Termitted 011 N N 終結狀態
// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

執行緒池的主要流程

執行緒池建立、接收任務、執行任務、回收執行緒的步驟

  1. 建立執行緒池後,執行緒池的狀態是Running,該狀態下才能有下面的步驟
  2. 提交任務時,執行緒池會建立執行緒去處理任務
  3. 當執行緒池的工作執行緒數達到corePoolSize時,繼續提交任務會進入阻塞佇列
  4. 當阻塞佇列裝滿時,繼續提交任務,會建立救急執行緒來處理
  5. 當執行緒池中的工作執行緒數達到maximumPoolSize時,會執行拒絕策略
  6. 當執行緒取任務的時間達到keepAliveTime還沒有取到任務,工作執行緒數大於corePoolSize時,會回收該執行緒

注意: 不是剛建立的執行緒是核心執行緒,後面建立的執行緒是非核心執行緒,執行緒是沒有核心非核心的概念的,這是我長期以來的誤解。

拒絕策略

  1. 呼叫者丟擲RejectedExecutionException (預設策略)
  2. 讓呼叫者執行任務
  3. 丟棄此次任務
  4. 丟棄阻塞佇列中最早的任務,加入該任務

提交任務的方法

// 執行Runnable
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}
// 提交Callable
public <T> Future<T> submit(Callable<T> task) {
  if (task == null) throw new NullPointerException();
   // 內部構建FutureTask
  RunnableFuture<T> ftask = newTaskFor(task);
  execute(ftask);
  return ftask;
}
// 提交Runnable,指定返回值
public Future<?> submit(Runnable task) {
  if (task == null) throw new NullPointerException();
  // 內部構建FutureTask
  RunnableFuture<Void> ftask = newTaskFor(task, null);
  execute(ftask);
  return ftask;
} 
//  提交Runnable,指定返回值
public <T> Future<T> submit(Runnable task, T result) {
  if (task == null) throw new NullPointerException();
   // 內部構建FutureTask
  RunnableFuture<T> ftask = newTaskFor(task, result);
  execute(ftask);
  return ftask;
}

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
}

Execetors建立執行緒池

注意: 下面幾種方式都不推薦使用

1.newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • 核心執行緒數 = 最大執行緒數 沒有救急執行緒
  • 阻塞佇列無界 可能導致oom

2.newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • 核心執行緒數是0,最大執行緒數無限制 ,救急執行緒60秒回收
  • 佇列採用 SynchronousQueue 實現 沒有容量,即放入佇列後沒有執行緒來取就放不進去
  • 可能導致執行緒數過多,cpu負擔太大

3.newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 核心執行緒數和最大執行緒數都是1,沒有救急執行緒,無界佇列 可以不停的接收任務
  • 將任務序列化 一個個執行, 使用包裝類是為了遮蔽修改執行緒池的一些引數 比如 corePoolSize
  • 如果某執行緒丟擲異常了,會重新建立一個執行緒繼續執行
  • 可能造成oom

4.newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
  • 任務排程的執行緒池 可以指定延遲時間呼叫,可以指定隔一段時間呼叫

執行緒池的關閉

shutdown()

會讓執行緒池狀態為shutdown,不能接收任務,但是會將工作執行緒和阻塞佇列裡的任務執行完 相當於優雅關閉

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

shutdownNow()

會讓執行緒池狀態為stop, 不能接收任務,會立即中斷執行中的工作執行緒,並且不會執行阻塞佇列裡的任務, 會返回阻塞佇列的任務列表

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

執行緒池的正確使用姿勢

執行緒池難就難在引數的配置,有一套理論配置引數

cpu密集型 : 指的是程式主要發生cpu的運算

​ 核心執行緒數: CPU核心數+1

IO密集型: 遠端呼叫RPC,運算元據庫等,不需要使用cpu進行大量的運算。 大多數應用的場景

​ 核心執行緒數=核數*cpu期望利用率 *總時間/cpu運算時間

但是基於以上理論還是很難去配置,因為cpu運算時間不好估算

實際配置大小可參考下表

cpu密集型 io密集型
執行緒數數量 核數<=x<=核數*2 核心數*50<=x<=核心數 *100
佇列長度 y>=100 1<=y<=10

1.執行緒池引數通過分散式配置,修改配置無需重啟應用

執行緒池引數是根據線上的請求數變化而變化的,最好的方式是 核心執行緒數、最大執行緒數 佇列大小都是可配置的

主要配置 corePoolSize maxPoolSize queueSize

java提供了可方法覆蓋引數,執行緒池內部會處理好引數 進行平滑的修改

public void setCorePoolSize(int corePoolSize) {
}

2.增加執行緒池的監控

3.io密集型可調整為先新增任務到最大執行緒數後再將任務放到阻塞佇列

程式碼 主要可重寫阻塞佇列 加入任務的方法

public boolean offer(Runnable runnable) {
    if (executor == null) {
        throw new RejectedExecutionException("The task queue does not have executor!");
    }

    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        int currentPoolThreadSize = executor.getPoolSize();
       
        // 如果提交任務數小於當前建立的執行緒數, 說明還有空閒執行緒,
        if (executor.getTaskCount() < currentPoolThreadSize) {
            // 將任務放入佇列中,讓執行緒去處理任務
            return super.offer(runnable);
        }
		// 核心改動
        // 如果當前執行緒數小於最大執行緒數,則返回 false ,讓執行緒池去建立新的執行緒
        if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
            return false;
        }

        // 否則,就將任務放入佇列中
        return super.offer(runnable);
    } finally {
        lock.unlock();
    }
}

3.拒絕策略 建議使用tomcat的拒絕策略(給一次機會)

// tomcat的原始碼
@Override
public void execute(Runnable command) {
    if ( executor != null ) {
        try {
            executor.execute(command);
        } catch (RejectedExecutionException rx) {
            // 捕獲到異常後 在從佇列獲取,相當於重試1取不到任務 在執行拒絕任務
            if ( !( (TaskQueue) executor.getQueue()).force(command) ) throw new RejectedExecutionException("Work queue full.");
        }
    } else throw new IllegalStateException("StandardThreadPool not started.");
}

建議修改從佇列取任務的方式: 增加超時時間,超時1分鐘取不到在進行返回

public boolean offer(E e, long timeout, TimeUnit unit){}

結語

工作三四年了,還沒有正式的寫過部落格,自學一直都是通過筆記的方式積累,最近重新學了一下java多執行緒,想著週末把這部分內容認真的寫篇部落格分享出去。

文章篇幅較長,給看到這裡的小夥伴點個大大的贊!由於作者水平有限,加之第一次寫部落格,文章中難免會有錯誤之處,歡迎小夥伴們反饋指正。

如果覺得文章對你有幫助,麻煩 點贊、評論、轉發、在看 走起

你的支援是我最大的動力!!!

相關文章