Java多執行緒

当脸遇上面發表於2024-03-09

Java多執行緒——<一>概述、定義任務

一、概述

  為什麼使用執行緒?從c開始,任何一門高階語言的預設執行順序是“按照編寫的程式碼的順序執行”,日常開發過程中寫的業務邏輯,但凡不涉及併發的,都是讓一個任務順序執行以確保得到想要的結果。但是,當你的任務需要處理的業務比較多時,且這些業務前後之間沒有依賴(比如, a執行的過程中b也可以執行,b沒有必要必須等待a執行完畢再去執行),那麼此時,我們可以將一個任務拆分成多個小任務。

  例如,任務a負責接收鍵盤的輸入,b負責將一些引數及計算提前做好(假設計算量比較大),c負責將a的輸入和b的結果做和。此時,abc順序執行的話,如果a的輸入被阻塞了即正在等待使用者輸入,b就無法執行,而此時cpu處於空置狀態(假設是單cpu且單核),明顯效率不高。

  換一個思路,假如:abc分開成為三個任務,a的輸入被阻塞了,那麼此時就把b交給cpu去執行,待使用者輸入結果之後,b已經將計算結果輸出給了c,此時,使用者提交後,c便立即計算出了結果。

  綜上:多執行緒解決的是併發的問題,目的是使任務執行效率更高,實現前提是“阻塞”。它們看上去時同時在執行的,但實際上只是分時間片試用cpu而已。

二、java中的多執行緒

  1.定義任務

  任務:簡單來說,就是一序列工作的集合,這些工作之間有前後順序,這一系列過程執行過後將實現一個結果或達到一個目的。

  首先,思考一個問題,為什麼要定義任務?作為java程式設計師,我們不關心底層的多執行緒機制是如何執行的,只關心我寫個怎樣的任務,java的底層的多執行緒機制才能認識,才能呼叫你的任務去執行。java是定義了Runnable介面讓你去實現,意思就是:你實現Runnable介面類定義一個類,該類的物件就是我能識別的任務,其他方式定義的程式,我都不會將它認為是任務。

  好,到這裡要明確一點,我們此時只談論任務,不說多執行緒。任務和你平時在一個類中編寫的程式碼並無區別,只是按照java的要求實現了一個介面,並在該介面的run方法中編寫了你的程式碼。也就是說,你平時想編寫一個類,該類能夠完成一些功能,這個類裡的任何方法、變數由你自己來定義,而編寫任務時,你需要實現Runnable介面,把你想讓該任務實現的程式碼寫到run方法中,當然,你可以在你定義的任務類中再定義其他變數、方法以在run中呼叫。

  2.程式碼實現

複製程式碼
public class Task implements Runnable {
    protected int countDown = 10;
    private static int taskCount = 0 ;
    private final int id = taskCount++;
    public Task(){}
    public Task(int countDown){
        this.countDown = countDown;
    }
    public String status(){
        return "#"+id+"("+(countDown>0?countDown:"Task!")+").    ";
    }
    @Override
    public void run() {
        while(countDown-->0){
            System.out.print(status());
            Thread.yield();
        }
    }
}
複製程式碼

注:此處程式碼源於《thinking in java》

  定義了任務,此時並不涉及多執行緒,所以,任務本身就是一個類,它的物件我們可以在任意試用到的地方呼叫,例如:

複製程式碼
public class TaskMain {
    public static void main(String[] args){
        Task task = new Task();
        task.run();
    }
}
複製程式碼

  就是在main中宣告瞭該例項的物件,並呼叫了它的run方法,同我們平時建立類一樣來呼叫物件的方法即可。

  至此,一個任務定義完了。也就是說按照java的要求,我們實現了一個簡單的任務。然而,實現任務的目的不只是為了實現任務,而是為了讓多執行緒機制能夠呼叫該任務去執行。請看:Java多執行緒——<二>將任務交給執行緒,執行緒宣告


Java多執行緒——<二>將任務交給執行緒,執行緒宣告及啟動

一、任務和執行緒

  《thinking in java》中專門有一小節中對執行緒和任務兩個概念進行了具體的區分,這也恰好說明任務和執行緒是有區別的。

  正如前文所提到的,任務只是一段程式碼,一段要達成你目的的程式碼,這段程式碼寫在哪,怎麼寫其實無所謂,只是因為你希望java的多執行緒機制能夠識別並呼叫你編寫的任務,所以規定了Runnable介面,讓你的任務來實現該介面,把你想做的工作在實現該介面的run方法中實現。

  那麼,已經定義了任務類,那任務和執行緒有什麼關係呢?

  java的執行緒是用來驅動任務執行的,也就是說你得把任務掛載到一個執行緒上,這樣該執行緒才能驅動你定義的任務來執行。

二、定義執行緒

  1.顯示的定義執行緒的過程就是將任務附著到執行緒的過程。執行緒Thread自身並不執行任何操作,它只是用來被多執行緒機制呼叫,並驅動賦予它的任務。

  如前次文章提到的任務類定義如下:

複製程式碼
public class Task implements Runnable {
    protected int countDown = 10;
    private static int taskCount = 0 ;
    private final int id = taskCount++;
    public Task(){}
    public Task(int countDown){
        this.countDown = countDown;
    }
    public String status(){
        return "#"+id+"("+(countDown>0?countDown:"Task!")+").    ";
    }
    @Override
    public void run() {
        while(countDown-->0){
            System.out.print(status());
            Thread.yield();
        }
    }
}
複製程式碼

  宣告執行緒並將任務附著到該執行緒上:

Thread t = new Thread(new Task());

  這樣,任務就附著給了執行緒,下面就是讓執行緒啟動,只需要如下的呼叫:

t.start();

  至此,執行緒宣告ok。

  有時,我會想,是不是像任務和執行緒的概念分離一樣,此時只是宣告瞭執行緒,而java的執行緒機制並不會呼叫該執行緒執行,還需要特殊的呼叫才能實現多執行緒執行。但是下面的一段程式碼告訴我,Thread類的start方法就是觸發了java的多執行緒機制,使得java的多執行緒能夠呼叫該執行緒

public static void main(String[] args){
        Thread t = new Thread(new Task());
        t.start();
        System.out.println("Waiting for Task");
}

輸出結果如下:

Waiting for Task
#0(9).    #0(8).    #0(7).    #0(6).    #0(5).    #0(4).    #0(3).    #0(2).    #0(1).    #0(Task!).    

  先輸出“Waiting for Task”證明呼叫完start()方法後,立即返回了主程式,並開始執行下面的語句。而你宣告的t執行緒已經去被java的多執行緒機制呼叫,並驅動著它的任務執行了。

  2.補充

  想看到更多的執行緒任務執行,可以用下面的這段程式碼

複製程式碼
public static void main(String[] args){
        for(int i = 0 ; i < 5 ; i++){
            new Thread(new Task()).start();
        }
        System.out.println("Waiting for Task");
}
複製程式碼

  輸出如下:

複製程式碼
Waiting for Task
#0(9).    #2(9).    #4(9).    #0(8).    #2(8).    #4(8).    #0(7).    #2(7).    #4(7).    #0(6).    #2(6).    #4(6).    #0(5).    #2(5).    #4(5).    #0(4).    #2(4).    #4(4).    #3(9).    #2(3).    #4(3).    #2(2).    #4(2).    #2(1).    #4(1).    #2(Task!).    #4(Task!).    #1(9).    #0(3).    #0(2).    #0(1).    #0(Task!).    #3(8).    #1(8).    #3(7).    #1(7).    #3(6).    #1(6).    #3(5).    #3(4).    #3(3).    #3(2).    #3(1).    #3(Task!).    #1(5).    #1(4).    #1(3).    #1(2).    #1(1).    #1(Task!).    
複製程式碼

  上面的輸出說明不同任務的執行線上程被換進換出時混在了一起——由執行緒排程器自動控制。不同版本的jdk執行緒排程方式不同,所以產生的結果也不相同。

  這裡涉及了垃圾回收器的一個問題,每個Thread都註冊了它自己,因此確實有一個對它的引用,而且在它的任務退出其run並死亡之前,垃圾回收器無法清除它。

注:以上程式碼均來自《thinking in java》,內容大部分是個人理解和總結,如有錯誤請各位指正


Java多執行緒——<三>簡單的執行緒執行:Executor

一、概述

  按照《Java多執行緒——<一><二>》中所講,我們要使用執行緒,目前都是顯示的宣告Thread,並呼叫其start()方法。多執行緒並行,明顯我們需要宣告多個執行緒然後都呼叫他的start方法,這麼一看,似乎有些問題:第一、執行緒一旦多了,宣告勢必是個問題;第二、多執行緒啟動如果通過手動執行的話,那可能一個執行緒已經跑完了,另外一個還沒起來(我推測可能會出現這個問題)。所以,我們在想,如果有個管家,能夠幫我們管理這麼多執行緒,只需要把我們定義的任務交給管家,管家就能夠幫我們把任務附著到執行緒上,並且當我們給管家傳送指令讓所有的執行緒開始併發執行時,他也能夠幫助我們開啟所有執行緒執行。

二、java多執行緒管家——Executor

  Executor允許你管理非同步任務的執行,而無須顯示地管理執行緒的生命週期。ExecutorService知道如何構建恰當的上下文來執行Runnable物件。
  1.建立ExecutorService 

  通過Executors能夠建立兩種方式的ExectorService。第一種、CachedThreadPool會為每個傳入的任務新建立一個執行緒

  ExecutorService exec = Executors.newCachedThreadPool();

  第二種、FixedThreadPool可以一次性預先執行代價高昂的執行緒分配,所以可以用來限制執行緒的數量。這可以節省時間,因為你不必為每個任務都固定的付出建立執行緒的開銷。

  ExecutorService exeService = Executors.newFixedThreadPool(5);

  2.把任務附著給ExecutorService

  有了executor,你只需要定義任務並將任務物件傳遞給executor即可。

  exeService.execute(new Task());

  3.讓所有任務開始執行

  這兩個方法會讓之前提交到該exectuor的所有任務開始執行。為了避免啟動後,會被注入新的任務,必須在你將所有執行緒注入後,執行關閉操作以保證這一點。

  exeService.shutdown();

總結:

複製程式碼
    |——原來:想執行任務
    |            |            |——1.定義任務
    |            |            |——2.建立任務物件交由Thread物件操縱
    |            |            |——3.顯示的呼叫Thread物件的start()方法
    |            |——遇到問題:比較繁瑣,總得自己啟動執行緒呼叫;本質上是由main函式呼叫的
    |——現在:使用java.util.concurrent.Executor(執行器)來管理Thread物件
    |            |            |——1.ExecutorService exec = Executors.newAcahedThreadPool();
    |            |            |——2.exec.execute(new Task());
    |            |            |——3.exec.shutdown();
    |            |——在客戶端(顯示呼叫執行緒執行)和執行任務之間提供了一個間接層,用以執行任務;撇開了main函式,由executor直接進行了呼叫
    |            |——允許你管理非同步任務的執行,而無須顯示地管理執行緒的生命週期
複製程式碼

三、其他

1.何時使用哪種執行緒池呢?
  CachedThreadPool在程式執行過程中通常會建立與所需數量相同的執行緒,然後在它回收舊執行緒時停止建立新執行緒,因此它是合理的Executor的首選
  只有當這種方式會引起問題時,才需要切換到FixedThreadPool。

2.SingleThreadExecutor

SingleThreadExecutor就像是執行緒數量為1的FixedThreadPool
       |——在另一個執行緒中連續執行的任何事務來說都很有用(重點是連續執行,因為這樣可以順序接受處理),故SingleThreadPool會序列化所有提交給它的任務,       並會維護它自己隱藏的懸掛任務佇列
       |——例如:向SingleThreadExecutor提交多個任務,那麼這些任務將排隊,每個任務都會在下一個任務開始之前執行結束,所有任務使用相同執行緒。    

3.自定義執行緒工廠

  每個靜態的ExecutorService建立方法都被過載為接受一個ThreadFactory物件,該物件將被用來建立新的執行緒。例如:

public class TaskDaemonFactory implements ThreadFactory{
    public Thread newThread(Runnable r){
        Thread t = new Thread(r);return t;
    }
}

  想使用自己定義的執行緒工廠

  ExecutorService exec = Executors.newCachedThreadPool(new TaskDaemonFactory());

  這樣可以通過具體的要求來改造執行緒。


相關文章