【程式設計師翻身計劃】Java高效能程式設計第一章-Java多執行緒概述

劉墨澤 發表於 2021-11-28
Java 程式設計師

目標

  1. 重點:
    • 執行緒安全的概念
    • 執行緒通訊的方式與應用
    • reactor執行緒模型
    • 執行緒數量的優化
    • jdk常用命令
    • Netty框架的作用
  2. 難點
    • java執行的原理
    • 同步關鍵字的原理
    • AQS的抽象
    • JUC的原始碼
    • 網路程式設計的概念
    • GC機制

class檔案內容

檔案開頭有一個0xcafebabe特殊的標誌。

包含版本、訪問標誌、常量池、當前類、超級類、介面、欄位、方法、屬性

image-20211108203041410.png 

把class檔案的資訊存在方法區裡面,有了類 根據類建立物件,儲存在堆記憶體中,垃圾回收就是這裡。這是執行緒共享的部分,隨虛擬機器或者GC建立或銷燬。除了這個區域 還有執行緒獨佔空間,隨執行緒生命週期而建立和銷燬。

JVM執行時資料區.png

  • 方法區:用來儲存載入的類資訊、常量、靜態變數、編譯後的程式碼等資料。虛擬機器規範中,這是一個邏輯區劃,不同的虛擬機器不同的實現。oracle的HotSpot在java7中,方法區放在永久代,java8放在後設資料空間,並且通過GC機制對這個區域進行管理。

  • 堆記憶體:分為老年代、新生代(Eden、From Survivor、To Survivor) JVM啟動時建立,存放物件的例項。垃圾回收主要管理堆記憶體。

堆記憶體.png

  • 虛擬機器棧:每個執行緒都有一個私有的空間。執行緒棧由多個棧幀(Stack Frame)組成,一個執行緒會執行一個或多個方法,一個方法對應一個棧幀。

    棧幀包括:區域性變數表、運算元棧、動態連結、方法返回地址、附加資訊。棧記憶體預設最大1M,超出丟擲StackOverflowError

  • 本地方法棧:使用Native本地方法準備的,超出也會報StackOverflowError,不同虛擬機器廠商不同的實現。

  • 程式計數器:記錄當前執行緒執行位元組碼的位置,儲存的是位元組碼指令地址,如果執行Native方法,計數器值會為空。

    CPU同一時間只會執行一條執行緒中的指令。JVM多執行緒會輪流切換並分配CPU執行時間的方式。為了執行緒切換後,需要通過程式計數器來恢復正確的執行位置。

接下來是原始檔編譯後位元組碼相關的東西,暫不在本次筆記中記錄。【記得有本書是位元組碼相關的解讀,立個flag,日後學習!】

image-20211109153611935.png

image-20211109153903730.png

執行緒狀態

6個狀態

  1. new:尚未啟動的執行緒的狀態
  2. runnable:可執行執行緒的執行緒狀態,等待CPU排程
  3. blocked:執行緒阻塞等待監視器鎖定的狀態,處於synchronized同步程式碼塊或方法中被阻塞。
  4. waiting:等待執行緒的狀態,不帶超時的方式:object.wait Thread.join LockSupport.pard
  5. timed waiting : 具有指定等待時間的等待執行緒的執行緒狀態。帶超時的方式:Thread.sleep Object.wait Thread.join LockSupport.parkNanos LockSupport.parkUntil
  6. Terminated:終止執行緒的狀態,執行完畢或出現異常。

image-20211109162009061.png

案例1

//新建  執行  終止
System.out.println("#####第一種狀態新建  執行  終止");
Thread thread1 = new Thread(new Runnable() {

    @Override
    public void run() {
        System.out.println("thread1當前狀態:"+Thread.currentThread().getState().toString());
        System.err.println("thread1執行了");

    }
});
System.out.println("沒呼叫start方法,thread1當前狀態:"+thread1.getState().toString());
thread1.start();
Thread.sleep(2000);
System.out.println("等待兩秒,thread1當前狀態:"+thread1.getState().toString());
複製程式碼
#####第一種狀態新建  執行  終止
沒呼叫start方法,thread1當前狀態:NEW
thread1當前狀態:RUNNABLE
thread1執行了
等待兩秒,thread1當前狀態:TERMINATED
複製程式碼

案例2

System.out.println("######第二種 新建 執行  等待 執行  終止(sleep)");
Thread thread2 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            Thread.sleep(1500L);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("thread2當前狀態:"+Thread.currentThread().getState().toString());
        System.err.println("thread2執行了");
    }
});
Thread.sleep(2000);
System.out.println("沒呼叫start方法,thread2當前狀態:" + thread2.getState().toString());
thread2.start();
System.out.println("呼叫start方法,thread2當前狀態:" + thread2.getState().toString());
Thread.sleep(200);
System.out.println("等待200毫秒,thread2當前狀態:" + thread2.getState().toString());
Thread.sleep(3000);
System.out.println("等待3秒,thread2當前狀態:" + thread2.getState().toString());
複製程式碼
######第二種 新建 執行  等待 執行  終止(sleep)
沒呼叫start方法,thread2當前狀態:NEW
呼叫start方法,thread2當前狀態:RUNNABLE
等待200毫秒,thread2當前狀態:TIMED_WAITING
thread2當前狀態:RUNNABLE
thread2執行了
等待3秒,thread2當前狀態:TERMINATED
複製程式碼

案例3

System.out.println("###第三種 新建  執行  阻塞  執行 終止");
    	
Thread thread = new Thread(new Runnable() {

    @Override
    public void run() {
        synchronized (Test.class) {
            System.out.println("當前狀態:"+Thread.currentThread().getState().toString());
            System.out.println("執行了");
        }
    }
});

synchronized (Test.class) {
    System.out.println("沒呼叫start方法,當前狀態:"+thread.getState().toString());
    thread.start();
    System.out.println("呼叫start方法,當前狀態:"+thread.getState().toString());
    Thread.sleep(200);
    System.out.println("200毫秒後,當前狀態:"+thread.getState().toString());
}
Thread.sleep(3000);
System.out.println("3秒後,當前狀態:"+thread.getState().toString());
複製程式碼
###第三種 新建  執行  阻塞  執行 終止
沒呼叫start方法,當前狀態:NEW
呼叫start方法,當前狀態:RUNNABLE
200毫秒後,當前狀態:BLOCKED
當前狀態:RUNNABLE
執行了
3秒後,當前狀態:TERMINATED
複製程式碼

執行緒終止

  • stop()

    執行緒不安全,會強行終止執行緒的所有鎖定。

  • interrupt()

    如果目標執行緒在呼叫Object class的wait join sleep方法時被阻塞,那麼interrupt會生效,該執行緒的中斷狀態將被清除,丟擲interruptedException異常。

    如果目標執行緒是被IO或者NIO中的channel阻塞,IO操作會被中斷或者返回特殊異常值。達到終止的目的。

    如果以上條件都不滿足,則會設定此執行緒的中斷狀態。

  • 通過狀態位來判斷

    public class StopThread extends Thread{
    	public volatile static boolean flag = true;
    	public static void main(String[] args) throws InterruptedException {
    		new Thread(()-> {
    			while(flag) {
    				try {
    					System.out.println("執行中");
    					Thread.sleep(10000);
    				} catch (InterruptedException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    			}
    		}).start();
    		Thread.sleep(3000);
    		flag = false;
    		System.out.println("結束");
    	}
    }
    複製程式碼

CPU快取及記憶體屏障

CPU有三級快取,從123到記憶體再到硬碟。但是存在一個問題,如果多核cpu讀取同樣的資料進行快取計算,最終寫入主記憶體的是以哪個為準?

這個時候就出來了一個快取一致性協議,單個cpu對快取中的資料做了改動,需要通知給其他cpu。

CPU還有一個效能優化手段,執行時指令重排,把讀快取命令優先執行。

兩個問題:

  1. 快取中的資料與主記憶體中的資料並不是實時同步的,各個cpu間快取的資料也不是實時同步的,在同一個時間點,各個cpu看到的同一記憶體地址的資料的值可能是不一致的。
  2. 多核多執行緒中,指令邏輯無法分辨因果關係,可能出現亂序執行。

解決辦法:記憶體屏障

  1. 寫記憶體屏障:在指令後插入Store Barrier,能讓寫入快取中的最新資料更新寫入主記憶體,讓其他執行緒可見。
  2. 讀記憶體屏障:在指令前插入Load Barrier,可以讓快取記憶體中的資料失效,強制重新從主記憶體中載入資料。

執行緒通訊

要想實現多個執行緒之間的協同,如 執行緒執行先後順序,獲取某個執行緒執行的結果等,設計執行緒之間相互通訊。

  1. 檔案共享

  2. 網路共享

  3. 共享變數

  4. jdk提供的執行緒協調API

    suspend/resume、wait/notify、park/unpark

JDK中對於需要多執行緒協作的,提供了對應API支援,典型場景是:生產者-消費者模型(執行緒阻塞、執行緒喚醒)

suspend/resume

  1. 同步程式碼中使用,suspend掛起之後並不會釋放鎖,容易出現死鎖。

  2. suspend比resume後執行

    被棄用。

wait/notify notifyAll

只能由同一物件鎖的持有者執行緒呼叫,也就是寫在同步塊裡,否則會丟擲illegalMonitorStateException異常。

wait:加入該物件的等待集合中,並且放棄當前持有的物件鎖。

雖然wait會自動解鎖,但是對順序有要求,如果在notify被呼叫之後才開始wait方法的呼叫,執行緒會永遠處於WAITING狀態。

//正常的wait
public void waitNotify() throws Exception {
    new Thread(()->{
        if(baozidian == null) {
            synchronized (this) {
                System.out.println("進入等待");
            }
        }
        System.out.println("買到包子");
    }).start();
    Thread.sleep(3000);
    baozidian = new Object();
    synchronized (this) {
        this.notify();
        System.out.println("通知");
    }
}

結果:
進入等待
買到包子
通知
複製程式碼

park/unpark

執行緒呼叫park則等待許可,unpark為指定執行緒提供許可。

不要求方法的呼叫順序。但不會釋放鎖,所以在同步程式碼塊中使用可能會死鎖。

/** 死鎖的park/unpark */
public void parkUnparkDeadLockTest() throws Exception {
    // 啟動執行緒
    Thread consumerThread = new Thread(() -> {
        if (baozidian == null) { // 如果沒包子,則進入等待
            System.out.println("1、進入等待");
            // 當前執行緒拿到鎖,然後掛起
            synchronized (this) {
                LockSupport.park();
            }
        }
        System.out.println("2、買到包子,回家");
    });
    consumerThread.start();
    // 3秒之後,生產一個包子
    Thread.sleep(3000L);
    baozidian = new Object();
    // 爭取到鎖以後,再恢復consumerThread
    synchronized (this) {
        LockSupport.unpark(consumerThread);
    }
    System.out.println("3、通知消費者");
}

結果:
1、進入等待
複製程式碼

注意:最好不要使用if語句來判斷是否進入等待狀態。

官方建議應該在迴圈體中檢查等待狀態,原因是處於等待狀態的執行緒可能會收到錯誤警報和偽喚醒。

偽喚醒是指執行緒因為更底層的原因導致的。

執行緒封閉

並不是所有時候 都要用到共享資料,shuju被封閉在各自的執行緒中,就不需要同步。

具體體現有:ThreadLocal、區域性變數

ThreadLocal

是一個執行緒級別的變數,每個執行緒都有一個ThreadLocal,就是每個執行緒都擁有了自己獨立的一個變數,競爭條件被徹底消除了,在併發模式下,是絕對安全的變數。

執行緒池

  1. 執行緒在java中是一個物件,更是作業系統的資源,建立銷燬都需要時間。

  2. java物件佔用堆記憶體,作業系統執行緒佔用系統記憶體,根據jvm規範,一個執行緒預設最大棧大小是1M,執行緒過多會消耗很多記憶體。

  3. 作業系統需要頻繁切換執行緒上下文。

    ----->執行緒池就是為了解決這些問題。

執行緒池概念

  1. 執行緒池管理器:建立並管理,建立、銷燬執行緒池、新增新任務
  2. 工作執行緒:在沒有任務時處於等待狀態,可以迴圈執行任務
  3. 任務介面:每個任務必須實現的介面,以供工作執行緒排程任務的執行,它主要規定了任務的入口,任務執行完後的收尾工作,任務的執行狀態等。
  4. 任務佇列:存放沒有處理的任務。提供一種緩衝機制。

image-20211126151836333.png

執行緒池API-介面定義和實現類

型別名稱描述
介面 Executor 最上層的介面,定義了**執行任務的方法execute**
介面 ExecutorService 繼承了Executor介面,擴充了Callable、Future、關閉方法
介面 ScheduledExecutorService 繼承了ExecutorService介面,增加了定時任務相關的方法
實現類 ThreadPoolExecutor 基礎、標準的執行緒池實現
實現類 ScheduledThreadPoolExecutor 繼承了ThreadPoolExecutor,實現了
ScheduledExecutorService中相關定時任務的方法

程式碼示例

公共程式碼塊:

/**
     * 測試:提交15個執行時間需要三秒,看執行緒池的情況
     * @param threadPoolExecutor
     */
public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception{
    for (int i=0;i<15;i++){
        int n = i;
        threadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("開始執行:"+n);
                    Thread.sleep(3000l);
                    System.err.println("執行結束:"+n);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("提交任務成功:"+i);
    }

    Thread.sleep(500l);
    System.out.println("當前執行緒池的數量:"+threadPoolExecutor.getPoolSize());
    System.out.println("當前等待佇列的數量:"+threadPoolExecutor.getQueue().size());
    Thread.sleep(15000l);
    System.out.println("當前執行緒池的數量:"+threadPoolExecutor.getPoolSize());
    System.out.println("當前等待佇列的數量:"+threadPoolExecutor.getQueue().size());
}
複製程式碼

測試方法1:

/**
 * 1、執行緒池資訊: 核心執行緒數量5,最大數量10,無界佇列,超出核心執行緒數量的執行緒存活時間:5秒, 指定拒絕策略的
 *
 * @throws Exception
 */
public void threadPoolExecutorTest1() throws Exception{
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,10,5, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>());
    testCommon(threadPoolExecutor);
}

//預計的結果:執行緒池數量5,其他進入等待佇列
複製程式碼
測試方法1輸出結果:
提交任務成功:0
開始執行:0
提交任務成功:1
開始執行:1
提交任務成功:2
開始執行:2
提交任務成功:3
提交任務成功:4
提交任務成功:5
提交任務成功:6
提交任務成功:7
提交任務成功:8
提交任務成功:9
提交任務成功:10
提交任務成功:11
提交任務成功:12
提交任務成功:13
提交任務成功:14
開始執行:3
開始執行:4
當前執行緒池的數量:5
當前等待佇列的數量:10
執行結束:2
執行結束:0
執行結束:4
執行結束:1
執行結束:3
開始執行:5
開始執行:6
開始執行:7
開始執行:8
開始執行:9
開始執行:10
開始執行:11
開始執行:12
開始執行:13
開始執行:14
執行結束:5
執行結束:6
執行結束:8
執行結束:7
執行結束:9
執行結束:13
執行結束:10
執行結束:14
執行結束:12
執行結束:11
當前執行緒池的數量:5
當前等待佇列的數量:0
複製程式碼

這裡有一個問題就是,最大執行緒數量設定的是10,當前執行緒池的數量為什麼達不到最大執行緒數量?

這就需要對execute的過程有個瞭解。

image-20211127101606123.png

測試方法2:

/**
     * 2、 執行緒池資訊: 核心執行緒數量5,最大數量10,佇列大小3,超出核心執行緒數量的執行緒存活時間:5秒, 指定拒絕策略的
     *
     * @throws Exception
     */
    public void threadPoolExecutorTest2() throws Exception{
        // 建立一個 核心執行緒數量為5,最大數量為10,等待佇列最大是3 的執行緒池,也就是最大容納13個任務。
        // 如果不指定拒絕策略,預設的策略是丟擲RejectedExecutionException異常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("任務決絕執行。");
            }
        });

        testCommon(threadPoolExecutor);
    }

//執行預期結果:
//執行緒池數量5,3個進入等待,這時候核心執行緒數量和佇列都滿了,會加開5個任務執行緒(注意,5秒後沒任務執行會銷燬),因為最大執行緒是10
//最大10+等待佇列3   總共13,剩下兩個拒絕執行
複製程式碼

測試方法3:Executors.newFixedThreadPool(int nThreads)

對於無界佇列,最大執行緒數量實際上是不起作用的。

    /**
     * 3、 執行緒池資訊: 核心執行緒數量5,最大數量5,無界佇列,超出核心執行緒數量的執行緒存活時間:5秒
     *
     * @throws Exception
     */
    private void threadPoolExecutorTest3() throws Exception {
        // 和Executors.newFixedThreadPool(int nThreads)一樣的
        ThreadPoolExecutor threadPoolExecutor1 = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
//        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
//                new LinkedBlockingQueue<Runnable>());
        testCommon(threadPoolExecutor1);
        // 預計結:執行緒池執行緒數量為:5,超出數量的任務,其他的進入佇列中等待被執行
    }
複製程式碼

Executors.newFixedThreadPool()的內部實現實際上就是 註釋掉的部分:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
複製程式碼

測試方法4:Executors.newCachedThreadPool()

此種方法適用於不可預估數量的情況

/**
     * 4、 執行緒池資訊:
     * 核心執行緒數量0,最大數量Integer.MAX_VALUE,SynchronousQueue佇列,超出核心執行緒數量的執行緒存活時間:60秒
     *
     * @throws Exception
     */
    private void threadPoolExecutorTest4() throws Exception {

        // SynchronousQueue,實際上它不是一個真正的佇列,因為它不會為佇列中元素維護儲存空間。與其他佇列不同的是,它維護一組執行緒,這些執行緒在等待著把元素加入或移出佇列。
        // 在使用SynchronousQueue作為工作佇列的前提下,客戶端程式碼向執行緒池提交任務時,
        // 而執行緒池中又沒有空閒的執行緒能夠從SynchronousQueue佇列例項中取一個任務,
        // 那麼相應的offer方法呼叫就會失敗(即任務沒有被存入工作佇列)。
        // 此時,ThreadPoolExecutor會新建一個新的工作者執行緒用於對這個入佇列失敗的任務進行處理(假設此時執行緒池的大小還未達到其最大執行緒池大小maximumPoolSize)。

        // 和Executors.newCachedThreadPool()一樣的
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
        testCommon(threadPoolExecutor);
        // 預計結果:
        // 1、 執行緒池執行緒數量為:15,超出數量的任務,其他的進入佇列中等待被執行
        // 2、 所有任務執行結束,60秒後,如果無任務可執行,所有執行緒全部被銷燬,池的大小恢復為0
        Thread.sleep(60000L);
        System.out.println("60秒後,再看執行緒池中的數量:" + threadPoolExecutor.getPoolSize());
    }
複製程式碼
 測試方法4輸出結果:
提交任務成功:0
提交任務成功:1
提交任務成功:2
提交任務成功:3
提交任務成功:4
提交任務成功:5
提交任務成功:6
提交任務成功:7
提交任務成功:8
提交任務成功:9
提交任務成功:10
提交任務成功:11
提交任務成功:12
提交任務成功:13
提交任務成功:14
開始執行:3
開始執行:2
開始執行:6
開始執行:7
開始執行:10
開始執行:11
開始執行:0
開始執行:1
開始執行:5
開始執行:4
開始執行:8
開始執行:9
開始執行:12
開始執行:13
開始執行:14
當前執行緒池的數量:15
當前等待佇列的數量:0
執行結束:3
執行結束:2
執行結束:6
執行結束:7
執行結束:10
執行結束:9
執行結束:0
執行結束:1
執行結束:5
執行結束:4
執行結束:14
執行結束:8
執行結束:11
執行結束:12
執行結束:13
當前執行緒池的數量:15
當前等待佇列的數量:0
60秒後,再看執行緒池中的數量:0

複製程式碼

測試方法5:一次性定時任務

/**
     * 5、 定時執行執行緒池資訊:3秒後執行,一次性任務,到點就執行 <br/>
     * 核心執行緒數量5,最大數量Integer.MAX_VALUE,DelayedWorkQueue延時佇列,超出核心執行緒數量的執行緒存活時間:0秒
     *
     * @throws Exception
     */
public void threadPoolExecutorTest5() throws Exception{
    //Executors.newScheduledThreadPool() 一樣的
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);
    scheduledThreadPoolExecutor.schedule(new Runnable() {
        @Override
        public void run() {
            System.out.println("任務被執行,現在時間:"+ DateUtil.now());
        }
    },3000,TimeUnit.MILLISECONDS);
    System.out.println("定時任務,提交成功,時間是:"+DateUtil.now());
}
複製程式碼

測試方法5輸出結果:

定時任務,提交成功,時間是:2021-11-27 13:42:43
任務被執行,現在時間:2021-11-27 13:42:46
複製程式碼

測試方法6:週期定時任務

/**
     * 6、 定時執行執行緒池資訊:執行緒固定數量5 ,<br/>
     * 核心執行緒數量5,最大數量Integer.MAX_VALUE,DelayedWorkQueue延時佇列,超出核心執行緒數量的執行緒存活時間:0秒
     *
     * @throws Exception
     */
public void threadPoolExecutorTest6() throws Exception{
    ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(5);
    //第一種方式:scheduleAtFixedRate,如果執行時間超過了週期時間
    //執行完畢後,立即執行,不考慮延遲時間
    scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000l);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("任務1被執行,現在時間:"+DateUtil.now());
        }
    },2000,1000,TimeUnit.MILLISECONDS);
    //第二種方式,scheduleWithFixedDelay
    //如果執行時間超過了週期時間,執行完畢後,加上延遲時間後再執行
    scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000l);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("任務2被執行,現在時間:"+DateUtil.now());
        }
    },2000,1000,TimeUnit.MILLISECONDS);
}
複製程式碼

測試方法6輸出結果:

//可以看出,任務1每隔3秒執行一次,任務2每隔4秒執行一次
任務1被執行,現在時間:2021-11-27 14:08:45
任務2被執行,現在時間:2021-11-27 14:08:45
任務1被執行,現在時間:2021-11-27 14:08:48
任務2被執行,現在時間:2021-11-27 14:08:49
任務1被執行,現在時間:2021-11-27 14:08:51
任務2被執行,現在時間:2021-11-27 14:08:53
任務1被執行,現在時間:2021-11-27 14:08:54
任務1被執行,現在時間:2021-11-27 14:08:57
任務2被執行,現在時間:2021-11-27 14:08:57
複製程式碼

終止執行緒的兩種方式

scheduledThreadPoolExecutor.shutdown();
//第二種會返回尚未執行的任務
List<Runnable> runnableList = scheduledThreadPoolExecutor.shutdownNow();
複製程式碼

本文同步公眾號【劉墨澤】,歡迎大家關注聊天!