Java多線程-線程的概念和建立

前言

聲明:該文章中全部測試都是在JDK1.8的環境下。html

該文章是我在學習Java中的多線程這方面知識時,作的一些總結和記錄。java

若是有不正確的地方請你們多多包涵並做出指點,謝謝!編程

本文章有些內容參考並採用如下做品內容:數組

https://www.cnblogs.com/vipst...緩存

https://www.bilibili.com/vide...服務器

1、基礎概念

咱們知道CPU執行代碼都是一條一條順序執行的,因此本質上單核CPU的電腦不會在同一個時間點執行多個任務。可是在現實中,咱們在使用電腦的時候,能夠一邊聊微信,一邊聽歌。那這是怎麼作到的呢?其實操做系統多任務就是讓CPU對多個任務輪流交替執行。微信

舉個例子:在一個教室中同時坐着一年級,二年級,三年級三批學生,老師花一分鐘教一年級,花一分教二年級,花一分鐘教三年級,這樣輪流下去,看上去就像同時在教三個年級。多線程

一樣的,咱們使用電腦時,一邊聊微信,一邊聽歌也是這個原理。CPU讓微信執行0.001秒,讓音樂播放器執行0.001秒,在咱們看來,CPU就是在同時執行多個任務。異步

1.1 程序、進程和線程的概念

程序:被存儲在磁盤或其餘的數據存儲設備中的可執行文件,也就是一堆靜態的代碼。ide

進程:運行在內存中可執行程序實例

線程:線程是進程的一個實體,是CPU調度和分派的基本單位。

看着這些概念是否是很抽象,看的很不舒服,那麼下面我來用實例解釋一下以上幾個概念。

1.2 程序的運行實例

上面說到,咱們使用電腦時,能夠一邊聊微信,一邊聽歌。那這些軟件運行的整個過程是怎樣的呢?

  • 若是咱們要用微信聊天,大部分的人都是雙擊擊桌面上的"微信"快捷方式,而雙擊這個快捷方式打開的其實是一個.exe文件,這個.exe文件就是一個程序,請看下圖:

  • 雙擊.exe文件後,加載可執行程序到內存中,方便CPU讀取,那這個可執行文件程序實例就是一個進程。請看下圖:
  • 而咱們在使用微信的時候,微信會作不少事情,好比加載微信UI界面,顯示微信好友列表,與好友聊天,這些能夠當作微信進程中一個個單獨的線程。

我用一張圖來總結一下整個過程:

根據上面內容對於線程概念的瞭解,是否有個疑問,線程是怎麼建立出來的?帶着這個疑問咱們就來學習一下java中的線程是怎麼如何建立的。

2、線程的建立

2.1 Thread類的概念

java.lang.Thread類表明線程,任何線程都是Thread類(子類)的實例。

2.2 經常使用的方法

構造方法 功能介紹
Thread() 使用無參的方式構造對象
Thread(String name) 根據參數指定的名稱來構造對象
Thread(Runnable target) 根據參數指定的引用來構造對象,其中Runnable是個接口類型
Thread(Runnable target, String name) 根據參數指定引用和名稱來構造對象
成員方法 功能介紹
run() 1.使用Runnable引用構造線程對象,調用該方法時最終調用接口中的版本
2.沒有使用Runnable引用構造線程對象,調用該方法時則啥也不作
start() 用於啓動線程,Java虛擬機會自動調用該線程的run方法

2.3 建立方式

2.3.1 自定義Thread類建立

自定義類繼承Thread類並根據本身的需求重寫run方法,而後在主類中建立該類的對象調用start方法,這樣就啓動了一個線程。

示例代碼以下:

//建立一個自定義類SubThreadRun繼承Thread類,做爲一個能夠備用的線程類
public class SubThreadRun extends Thread {
    @Override
    public void run() {
        //打印1~20的整數值
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("SubThreadRun線程中:" + i);
        }
    }
}

//在主方法中建立該線程並啓動
public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread類型的引用指向子類類型的對象
        Thread t1 = new SubThreadRun();
        //用於啓動線程,java虛擬機會自動調用該線程中的run方法
        t1.start();
    }
}


輸出結果:
    SubThreadRun線程中:0
    SubThreadRun線程中:2
    。。。。。。
    SubThreadRun線程中:19

到這裏你們會不會有如下一個疑問,看示例代碼:

public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread類型的引用指向子類類型的對象
        Thread t1 = new SubThreadRun();
        //調用run方法測試
        t1.run();
    }
}

輸出結果:
    SubThreadRun線程中:0
    SubThreadRun線程中:1
    。。。。。。
    SubThreadRun線程中:19

咱們不調用start方法,而是直接調用run方法,發現結果和調用start方法同樣,他們兩個方法的區別是啥呢?

咱們在主方法中也加入一個打印1-20的數,而後分別用run方法和start方法進行測試,實例代碼以下:

//使用run方法進行測試
public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread類型的引用指向子類類型的對象
        Thread t1 = new SubThreadRun();
        //2.調用run方法測試
        t1.run();

        //打印1~20的整數值
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("-----mian-----方法中:" + i);
        }
    }
}

輸出結果:
    SubThreadRun線程中:0
    SubThreadRun線程中:1
    。。。。。。//都是SubThreadRun線程中
    SubThreadRun線程中:19
    -----mian-----方法中:0
    -----mian-----方法中:1
    。。。。。。//都是-----mian-----方法中
    -----mian-----方法中:19
     
    
//使用start方法進行測試
public class SubThreadRunTest {

    public static void main(String[] args) {

        //1.申明Thread類型的引用指向子類類型的對象
        Thread t1 = new SubThreadRun();
        //用於啓動線程,java虛擬機會自動調用該線程中的run方法
        t1.start();

        //打印1~20的整數值
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("-----mian-----方法中:" + i);
        }
    }
}

輸出結果:
    SubThreadRun線程中:0
    -----mian-----方法中:0
    SubThreadRun線程中:1
    SubThreadRun線程中:2
    -----mian-----方法中:1
    。。。。。。//SubThreadRun線程和mian方法相互穿插
    SubThreadRun線程中:19
    -----mian-----方法中:19

從上面的例子可知:

  • 調用run方法測試時,本質上就是至關於對普通成員方法的調用,所以執行流程就是run方法的代碼執行完畢後才能繼續向下執行。
  • 調用start方法測試時,至關於又啓動了一個線程,加上執行main方法的線程,一共有兩個線程,這兩個線程同時運行,因此輸出結果是交錯的。(如今回過頭來想一想,如今是否是有點理解我能夠用qq音樂一邊聽歌,一邊在打字評論了呢。)

第一種建立線程的方式咱們已經學會了,那這種建立線程的方式有沒有什麼缺陷呢?假如SubThreadRun類已經繼承了一個父類,這個時候咱們又要把該類做爲自定義線程類,若是仍是用繼承Thread類的方法來實現的話就違背了Java不可多繼承的概念。因此第二種建立方式就能夠避免這種問題。

2.3.2 經過實現Runnable接口實現建立

自定義類實現Runnable接口並重寫run方法,建立該類的對象做爲實參來構造Thread類型的對象,而後使用Thread類型的對象調用start方法。

示例代碼以下:

//建立一個自定義類SubRunnableRun實現Runnable接口
public class SubRunnableRun implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("SubRunnableRun線程中:" + i);
        }
    }
}

//在主方法中建立該線程並啓動
public class SunRunnableRunTest {

    public static void main(String[] args) {

        //1.建立自定義類型的對象
        SubRunnableRun srr = new SubRunnableRun();
        //2.使用該對象做爲實參構造Thread類型的對象
        Thread t1 = new Thread(srr);
        //3.使用Thread類型的對象調用start方法
        t1.start();

        //打印1~20之間的全部整數
        for (int i = 0; i < 20 ; i ++) {
            System.out.println("-----mian-----方法中:" + i);
        }
    }
}

輸出結果:
    SubRunnableRun線程中:0
    -----mian-----方法中:0
    SubRunnableRun線程中:1
    SubRunnableRun線程中:2
    -----mian-----方法中:1
    。。。。。。//SubRunnableRun線程和mian方法相互穿插
    SubRunnableRun線程中:19
    -----mian-----方法中:19

到這裏你們會不會有一個疑問呢?

我在SunRunnableRunTest類的main方法中也實例化了Thread類,爲何該線程調用的是實現了Runnable接口的SubRunnableRun類中的run方法,而不是Thread類中的run方法。

爲了解決該疑問,咱們就進入Thread類去看一下源碼,源碼調用過程以下:

  1. 從上面的SunRunnableRunTest類中代碼可知,咱們建立線程調用的是Thread的有參構造方法,參數是Runnable類型的。

  2. 進入到Thread類中找到該有參構造方法,看到該構造方法調用init方法,而且把target參數繼續當參數傳遞過去。

  3. 轉到對應的init方法後,發現該init方法繼續調用另外一個重載的init方法,而且把target參數繼續當參數傳遞過去。

  4. 繼續進入到重載的init方法中,咱們發現,該方法中把參數中target賦值給成員變量target。

  5. 而後找到Thread類中的run方法,發現只要Thread的成員變量target存在,就調用target中的run方法。

經過查看源碼,咱們能夠知道爲何咱們建立的Thread類調用的是Runnable類中的run方法。

2.3.3 匿名內部類的方式實現建立

上面兩種建立線程的方式都須要單首創建一個類來繼承Thread類或者實現Runnable接口,並重寫run方法。而匿名內部類能夠不建立單獨的類而實現自定義線程的建立。

示例代碼以下:

public class ThreadNoNameTest {

    public static void main(String[] args) {

        //匿名內部類的語法格式:父類/接口類型 引用變量 = new 父類/接口類型 {方法的重寫};
        //1.使用繼承加匿名內部類的方式建立並啓動線程
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("繼承Thread類方式建立線程...");
            }
        };
        t1.start();
       
        //2.使用接口加匿名內部類的方式建立並啓動線程
        Runnable ra = new Runnable() {
            @Override
            public void run() {
                System.out.println("實現Runnable接口方式實現線程...");
            }
        };
        Thread t2 = new Thread(ra);
        t2.start();
    }
}

這兩個利用匿名內部類建立線程的方式還能繼續簡化代碼,尤爲是使用Runnable接口建立線程的方式,可使用Lambda表達式進行簡化。

示例代碼以下:

public class ThreadNoNameTest {

    public static void main(String[] args) {

        //1.使用繼承加匿名內部類簡化後的方式建立並啓動線程
        new Thread() {
            @Override
            public void run() {
                System.out.println("簡化後繼承Thread類方式建立線程...");
            }
        }.start();

        //2.使用接口加匿名內部類簡化後的方式建立並啓動線程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("簡化後實現Runnable接口方式實現線程...");
            }
        }).start();
        
        //2-1.對於接口加匿名內部建立線程,能夠繼續使用lambda表達式進行簡化。
        //Java8開始支持lambda表達式:(形參列表) -> {方法體;}
        Runnable ra = () -> System.out.println("lambda表達式簡化實現Runnable接口方式實現線程...");
        new Thread(ra).start();
        
        //繼續簡化
        new Thread(() -> System.out.println("lambda表達式簡化實現Runnable接口方式實現線程...")).start();
    }
}

2.3.4 經過實現Callable接口建立

經過上面幾個例子,咱們瞭解了兩種建立線程的方式,可是這兩種方式建立線程存在一個問題,就是run方法是沒有返回值的,因此若是咱們但願在線程結束以後給出一個結果,那就須要用到實現Callable接口建立線程。

(1)Callable接口

從Java5開始新增建立線程的第三中方式爲實現java.util.concurrent.Callable接口。

經常使用方法以下:

成員方法 功能介紹
V call() 計算結果,若是沒法計算結果,則拋出一個異常

咱們知道啓動線程只有建立一個Thread類並調用start方法,若是想讓線程啓動時調用到Callable接口中的call方法,就得用到FutureTask類。

(2)FutureTask類

java.util.concurrent.FutureTask類實現了RunnableFuture接口,RunnableFuture接口是Runnable和Future的綜合體,做爲一個Future,FutureTask能夠執行異步計算,能夠查看異步程序是否執行完畢,而且能夠開始和取消程序,並取得程序最終的執行結果,也能夠用於獲取調用方法後的返回結果。

經常使用方法以下:

構造方法 功能介紹
FutureTask(Callable<v> callable) 建立一個FutureTask,它將在運行時執行給定的Callable
成員方法 功能介紹
V get() 獲取call方法計算的結果

從上面的概念能夠了解到FutureTask類的一個構造方法是以Callable爲參數的,而後FutureTask類是Runnable的子類,因此FutureTask類能夠做爲Thread類的參數。這樣的話咱們就能夠建立一個線程並調用Callable接口中的call方法。

實例代碼以下:

public class ThreadCallableTest implements Callable {
    @Override
    public Object call() throws Exception {
        //計算1 ~ 10000之間的累加和並打印返回
        int sum = 0;
        for (int i = 0; i <= 10000; i ++) {
            sum += i;
        }
        System.out.println("call方法中的結果:" + sum);
        return sum;
    }

    public static void main(String[] args) {

        ThreadCallableTest tct = new ThreadCallableTest();
        FutureTask ft = new FutureTask(tct);
        Thread t1 = new Thread(ft);
        t1.start();
        Object ob = null;
        try {
            ob = ft.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("main方法中的結果:" + ob);
    }
}

輸出結果:
    call方法中的結果:50005000
    main方法中的結果:50005000

2.3.5 線程池的建立

線程池的由來

在講線程池以前,先來說一個故事,一個老闆開個飯店,但這個飯店很奇怪,每來一個顧客,老闆就去招一個新的大廚來作菜,等這個顧客走後,老闆直接把這個大廚辭了。若是是按這種經營方式的話,老闆天天就忙着招大廚,啥事都幹不了。

對於上面講的這個故事,咱們現實生活中的飯店老闆可沒有這麼蠢,他們都是在開店前就直接招了好幾個大廚候在廚房,等有顧客來了,直接作菜上菜,顧客走後,廚師留在後廚待命,這樣就把老闆解放了。

如今咱們來說一下線程池的由來:好比說服務器編程中,若是爲每個客戶都分配一個新的工做線程,而且當工做線程與客戶通訊結束時,這個線程被銷燬,這就須要頻繁的建立和銷燬工做線程。若是訪問服務器的客戶端過多,那麼會嚴重影響服務器的性能。

那麼咱們該如何解放服務器呢?對了,就像上面講的飯店老闆同樣,打造一個後廚,讓廚師候着。相對於服務器來講,就建立一個線程池,讓線程候着,等待客戶端的鏈接,等客戶端結束通訊後,服務器不關閉該線程,而是返回到線程中待命。這樣就解放了服務器。

線程池的概念

首先建立一些線程,他們的集合稱爲線程池,當服務器接收到一個客戶請求後,就從線程池中取出一個空餘的線程爲之服務,服務完後不關閉線程,而是將線程放回到線程池中。

相關類和方法

  • 線程池的建立方法總共有 7 種,但整體來講可分爲 2 類:

  • Executors是一個工具類和線程池的工廠類,用於建立並返回不一樣類型的線程池,經常使用的方法以下:

    返回值 方法 功能介紹
    static ExecutorService newFixedThreadPool(int nThreads) 建立一個固定大小的線程池,若是任務數超出線程數,則超出的部分會在隊列中等待
    static ExecutorService newCachedThreadPool() 建立一個可已根據須要建立新線程的線程池,若是同一時間的任務數大於線程數,則能夠根據任務數建立新線程,若是任務執行完成,則緩存一段時間後線程被回收。
    static ExecutorService newSingleThreadExecutor() 建立單個線程數的線程池,能夠保證先進先出的執行順序
    static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 建立一個能夠執行延遲任務的線程池
    static ScheduledExecutorService newSingleThreadScheduledExecutor() 建立一個單線程的能夠執行延遲任務的線程池
    static ExecutorService newWorkStealingPool() 建立一個搶佔式執行的線程池(任務執行順序不肯定)【JDK 1.8 添加】
  • ThreadPoolExecutor經過構造方法建立線程池,最多能夠設置7個參數,建立線程池的構造方法以下:

    構造方法 功能介紹
    ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 經過最原始的方法建立線程池
  • 經過上面兩類方法建立完線程池後均可以用ExecutorService接口進行接收,它是真正的線程池接口,主要實現類是ThreadPoolExecutor,經常使用方法以下:

    方法聲明 功能介紹
    void execute(Runnable command) 執行任務和命令,一般用於執行Runnable
    <T> Future<T> submit(Callable<T> task) 執行任務和命令,一般用於執行Callable
    void shutdown() 啓動有序關閉

代碼實例

  1. 使用newFixedThreadPool方法建立線程池

    public class FixedThreadPool {
        public static void main(String[] args) {
            
            // 建立含有兩個線程的線程池
            ExecutorService threadPool = Executors.newFixedThreadPool(2);
            // 建立任務
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("線程:" + Thread.currentThread().getName() + "執行了任務!");
                }
            };
            // 線程池執行任務
            threadPool.execute(runnable);
            threadPool.execute(runnable);
            threadPool.execute(runnable);
            threadPool.execute(runnable);
        }
    }
    
    輸出結果:
        線程:pool-1-thread-2執行了任務!
        線程:pool-1-thread-1執行了任務!
        線程:pool-1-thread-2執行了任務!
        線程:pool-1-thread-1執行了任務!

從結果上能夠看出,這四個任務分別被線程池中的固定的兩個線程所執行,線程池也不會建立新的線程來執行任務。

  1. 使用newCachedThreadPool方法建立線程池

    public class cachedThreadPool {
        public static void main(String[] args) {
    
            //1.建立線程池
            ExecutorService executorService = Executors.newCachedThreadPool();
            //2.設置任務
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("線程:" + Thread.currentThread().getName() + "執行了任務!");
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                    }
                }
            };
            //3.執行任務
            for (int i = 0; i < 100; i ++) {
                executorService.execute(runnable);
            }
        }
    }
    
    輸出結果:
        線程:pool-1-thread-1執行了任務!
        線程:pool-1-thread-4執行了任務!
        線程:pool-1-thread-3執行了任務!
        線程:pool-1-thread-2執行了任務!
        線程:pool-1-thread-5執行了任務!
        線程:pool-1-thread-7執行了任務!
        線程:pool-1-thread-6執行了任務!
        線程:pool-1-thread-8執行了任務!
        線程:pool-1-thread-9執行了任務!
        線程:pool-1-thread-10執行了任務!

從結果上能夠看出,線程池根據任務的數量來建立對應的線程數量。

  1. 使用newSingleThreadExecutor的方法建立線程池

    public class singleThreadExecutor {
    
        public static void main(String[] args) {
    
            //建立線程池
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            //執行任務
            for (int i = 0; i < 10; i ++) {
                final int task = i + 1;
                executorService.execute(()->{
                    System.out.println("線程:" + Thread.currentThread().getName() + "執行了第" + task +"任務!");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            }
        }
    }
    
    輸出結果:
        線程:pool-1-thread-1執行了第1任務!
        線程:pool-1-thread-1執行了第2任務!
        線程:pool-1-thread-1執行了第3任務!
        線程:pool-1-thread-1執行了第4任務!
        線程:pool-1-thread-1執行了第5任務!
        線程:pool-1-thread-1執行了第6任務!
        線程:pool-1-thread-1執行了第7任務!
        線程:pool-1-thread-1執行了第8任務!
        線程:pool-1-thread-1執行了第9任務!
        線程:pool-1-thread-1執行了第10任務!

從結果能夠看出,該方法建立的線程能夠保證任務執行的順序。

  1. 使用newScheduledThreadPool的方法建立線程池

    public class ScheduledThreadPool {
    
        public static void main(String[] args) {
    
            //建立包含2個線程的線程池
            ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
            //記錄建立任務時的當前時間
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Date startTime = new Date();
            String start = formatter.format(startTime);
            System.out.println("建立任務時的時間:" + start);
            //建立任務
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    Date endTime = new Date();
                    String end = formatter.format(endTime);
                    System.out.println("線程:" + Thread.currentThread().getName() + "任務執行的時間爲:" + end);
                }
            };
            //執行任務(參數:runnable-要執行的任務,2-從如今開始延遲執行的時間,TimeUnit.SECONDS-延遲參數的時間單位)
            for(int i = 0; i < 2; i ++) {
                scheduledExecutorService.schedule(runnable,2, TimeUnit.SECONDS);
            }
        }
    }
    
    輸出結果:
        建立任務的時間:2021-04-19 19:26:18
        線程:pool-1-thread-1任務執行的時間爲:2021-04-19 19:26:20
        線程:pool-1-thread-2任務執行的時間爲:2021-04-19 19:26:20

從結果能夠看出,該方法建立的線程池能夠分配已有的線程執行一些須要延遲的任務。

  1. 使用newSingleThreadScheduledExecutor方法建立線程池

    public class SingleThreadScheduledExecutor {
    
        public static void main(String[] args) {
    
            //建立線程池
            ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
            //建立任務
            Date startTime = new Date();
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String start = formatter.format(startTime);
            System.out.println("建立任務的時間:" + start);
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    Date endTime = new Date();
                    String end = formatter.format(endTime);
                    System.out.println("線程:" + Thread.currentThread().getName() + "任務執行的時間爲:" + end);
                }
            };
            //執行任務
            for(int i = 0; i < 2; i ++) {
                scheduledExecutorService.schedule(runnable,2, TimeUnit.SECONDS);
            }
        }
    }
    
    輸出結果:
        建立任務的時間:2021-04-19 19:51:58
        線程:pool-1-thread-1任務執行的時間爲:2021-04-19 19:52:00
        線程:pool-1-thread-1任務執行的時間爲:2021-04-19 19:52:00

從結果能夠看出,該方法建立的線程池只有一個線程,該線程去執行一些須要延遲的任務。

  1. 使用newWorkStealingPool方法建立線程池

    public class newWorkStealingPool {
    
        public static void main(String[] args) {
    
            //建立線程池
            ExecutorService executorService = Executors.newWorkStealingPool();
            //執行任務
            for (int i = 0; i < 4; i ++) {
                final int task = i + 1;
                executorService.execute(()->{
                    System.out.println("線程:" + Thread.currentThread().getName() + "執行了第" + task +"任務!");
                });
            }
            //確保任務被執行
            while (!executorService.isTerminated()) {
            }
        }
    }
    
    輸出結果:
        線程:ForkJoinPool-1-worker-9執行了第1任務!
        線程:ForkJoinPool-1-worker-4執行了第4任務!
        線程:ForkJoinPool-1-worker-11執行了第3任務!
        線程:ForkJoinPool-1-worker-2執行了第2任務!

從結果能夠看出,該方法會建立一個含有足夠多線程的線程池,來維持相應的並行級別,任務會被搶佔式執行。(任務執行順序不肯定)

  1. 使用ThreadPoolExecutor建立線程池

    在編寫示例代碼以前我先來說一個生活的例子(去銀行辦理業務):

    描述業務場景:銀行一共有4個窗口,今天只開放兩個,而後等候區一共3個位置。以下圖所示:

    • 若是銀行同時辦理業務的人小於等於5我的,那麼正好,2我的先辦理,其餘的人在等候區等待。以下圖所示:

    • 若是銀行同時辦理業務的人等於6我的時,銀行會開放三號窗口來辦理業務。以下圖所示:

    • 若是銀行同時辦理業務的人等於7我的時,銀行會開放四號窗口來辦理業務。以下圖所示:

    • 若是銀行同時辦理業務的人大於7我的時,則銀行大廳經理就會告訴後面的人,該網點業務已滿,請去其餘網點辦理。

    如今咱們再來看一下咱們的ThreadPoolExecutor構造方法,該構造方法最多能夠設置7個參數:

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

參數介紹:

  1. corePoolSize:核心線程數,在線程池中一直存在的線程(對應銀行辦理業務模型:一開始就開放的窗口)
  2. maximumPoolSize:最大線程數,線程池中能建立最多的線程數,除了核心線程數之外的幾個線程會在線程池的任務隊列滿了以後建立(對應銀行辦理業務模型:全部窗口)
  3. keepAliveTime:最大線程數的存活時間,當長時間沒有任務時,線程池會銷燬一部分線程,保留核心線程
  4. unit:時間單位,是第三個參數的單位,這兩個參數組合成最大線程數的存活時間

    • TimeUnit.DAYS:天
    • TimeUnit.HOURS:小時
    • TimeUnit.MINUTES:分
    • TimeUnit.SECONDS:秒
    • TimeUnit.MILLISECONDS:毫秒
    • TimeUnit.MICROSECONDS:微秒
    • TimeUnit.NANOSECONDS:納秒
  5. workQueue:等待隊列,用於保存在線程池等待的任務(對應銀行辦理業務模型:等待區)

    • ArrayBlockingQueue:一個由數組支持的有界阻塞隊列。
    • LinkedBlockingQueue:一個由鏈表組成的有界阻塞隊列。
    • SynchronousQueue:該阻塞隊列不儲存任務,直接提交給線程,這樣就會造成對於提交的任務,若是有空閒線程,則使用空閒線程來處理,不然新建一個線程來處理任務。
    • PriorityBlockingQueue:一個帶優先級的無界阻塞隊列,每次出隊都返回優先級最高或者最低的元素
    • DelayQueue:一個使用優先級隊列實現支持延時獲取元素的無界阻塞隊列,只有在延遲期滿時才能從中提取元素,現實中的使用: 淘寶訂單業務:下單以後若是三十分鐘以內沒有付款就自動取消訂單。
    • LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
    • LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
  6. threadFactory:線程工廠,用於建立線程。
  7. handler:拒絕策略,任務超出線程池可接受範圍時,拒絕處理任務時的策略。

    • ThreadPoolExecutor.AbortPolicy:當任務添加到線程池中被拒絕時,它將拋出 RejectedExecutionException 異常(默認使用該策略
    • ThreadPoolExecutor.CallerRunsPolicy:當任務添加到線程池中被拒絕時,會調用當前線程池的所在的線程去執行被拒絕的任務
    • ThreadPoolExecutor.DiscardOldestPolicy:當任務添加到線程池中被拒絕時,會拋棄任務隊列中最舊的任務也就是最早加入隊列的,再把這個新任務添加進去
    • ThreadPoolExecutor.DiscardPolicy:若是該任務被拒絕,這直接忽略或者拋棄該任務

當任務數小於等於核心線程數+等待隊列數量的總和時

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 建立線程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //建立任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>執行任務");
            }
        };
        // 執行任務
        for (int i = 0; i < 5; i++) {
            threadPool.execute(runnable);
        }
        //關閉線程池
        threadPool.shutdown();
    }
}


輸出結果:
    pool-1-thread-2==>執行任務
    pool-1-thread-1==>執行任務
    pool-1-thread-2==>執行任務
    pool-1-thread-1==>執行任務
    pool-1-thread-2==>執行任務

從結果中能夠看出,只有兩個核心線程在執行任務。

當任務數大於核心線程數+等待隊列數量的總和,可是小於等於最大線程數時

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 建立線程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //建立任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>執行任務");
            }
        };
        // 執行任務
        for (int i = 0; i < 7; i++) {
            threadPool.execute(runnable);
        }
        //關閉線程池
        threadPool.shutdown();
    }
}

輸出結果:
    pool-1-thread-1==>執行任務
    pool-1-thread-4==>執行任務
    pool-1-thread-2==>執行任務
    pool-1-thread-2==>執行任務
    pool-1-thread-3==>執行任務
    pool-1-thread-4==>執行任務
    pool-1-thread-1==>執行任務

從結果中能夠看出,啓動了最大線程來執行任務。

當任務數大於最大線程數時

public class ThreadPoolExecutorTest {

    public static void main(String[] args) {

        // 建立線程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //建立任務
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "==>執行任務");
            }
        };
        // 執行任務
        for (int i = 0; i < 8; i++) {
            threadPool.execute(runnable);
        }
        //關閉線程池
        threadPool.shutdown();
    }
}

輸出結果:
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.zck.task18.ThreadPool.ThreadPoolExecutorTest$1@7f31245a rejected from java.util.concurrent.ThreadPoolExecutor@6d6f6e28[Running, pool size = 4, active threads = 0, queued tasks = 0, completed tasks = 7]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at com.zck.task18.ThreadPool.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:25)
    pool-1-thread-1==>執行任務
    pool-1-thread-4==>執行任務
    pool-1-thread-4==>執行任務
    pool-1-thread-4==>執行任務
    pool-1-thread-2==>執行任務
    pool-1-thread-3==>執行任務
    pool-1-thread-1==>執行任務

從結果中能夠看出,任務大於最大線程數,使用拒絕策略直接拋出異常。

3、總結

本文介紹了三種線程的建立方式:

  • 自定義類繼承Thread類並重寫run方法建立
  • 自定義類實現Runnable接口並重寫run方法建立
  • 實現Callable接口建立

介紹了七種線程池的建立方式:

  • 使用newFixedThreadPool方法建立線程池
  • 使用newCachedThreadPool方法建立線程池
  • 使用newSingleThreadExecutor的方法建立線程池
  • 使用newScheduledThreadPool的方法建立線程池
  • 使用newSingleThreadScheduledExecutor方法建立線程池
  • 使用newWorkStealingPool方法建立線程池
  • 使用ThreadPoolExecutor建立線程池
相關文章
相關標籤/搜索