1. 程式人生 > 其它 >執行緒詳解

執行緒詳解

執行緒概述

執行一個音樂播放器播放一首歌,音樂播放器就是一個程序,在程式執行時,既有聲音的輸出,同時還有該歌曲的字幕展示,這就是程序中的兩個執行緒

執行緒與程序

程式進入記憶體就變成了程序,程序就是處於執行中的程式

程序特徵:

  • 獨立性:每個程序都有自己的私有地址,一個程序不能直接訪問其他程序
  • 動態性:程序有自己的生命週期和不同狀態,而程式不具備
  • 併發性:多個程序可以在單個處理器上併發執行,程序之間互不影響

併發: 程序在cpu中切換執行 並行:程序在cpu上一起執行

對於一個CPU而言,它在某個時間點只能執行一個程式,也就是說,只能執行一個程序,
CPU不斷地在這些程序之間輪換執行,雖然CPU在多個程序間輪換執行,但是我們感覺到好像有多個程序在同時進行

執行緒是程序的執行單元,對於絕大多數的應用程式來說,通常僅要求有一個主執行緒,
但也可以在該程序內建立多條順序執行流,這些順序執行流就是執行緒(子執行緒),
每個執行緒也是相互獨立的

執行緒可以擁有自己的堆疊、自己的程式計數器和自己的區域性變數,
但不擁有系統資源,它與父程序的其他執行緒共享該程序所有擁有的全部資源

一個執行緒可以建立和撤銷另一個執行緒,同一個程序中的多個執行緒之間可以併發執行。




多執行緒的優勢

  • 程序中的執行緒之間的隔離程度要小。它們共享記憶體、檔案控制代碼和其他的每個執行緒的狀態
  • 程序在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體,提高執行效率
  • 執行緒共享的環境包括:程序程式碼段、程序的公有資料
  • 程序之間不能共享記憶體,但執行緒之間共享記憶體非常容易
  • 系統建立程序是需要為該程序重新分配系統資源,但建立執行緒則代價小得多

執行緒的建立與啟動

Java使用Thread類代表執行緒,所有的執行緒物件都必須是Thread類或其子類的例項

每個執行緒的作用是完成一定的任務,實際上就是執行一段程式程式碼。

Java使用執行緒執行體來代表這段程式程式碼。

Ø 繼承Thread建立執行緒

  1. 定義Thread類的子類,並重寫該類的run()方法,run()方法的方法體代表執行緒需要完成的任務。因此把run方法稱為執行緒執行體。

  2. 建立Thread子類的例項,即建立了執行緒物件
  3. 呼叫執行緒物件的start()方法來啟動該執行緒。
public class Test2 extends Thread{

    // 重寫run方法,run方法的方法體就是子執行緒的執行體
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            
// 繼承Thread類後,從父類繼承的getName方法可以獲取當前執行緒的名稱 System.out.println("執行緒名稱:"+this.getName()+" "+i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { // 呼叫Thread的currentThread()方法獲取當前執行緒物件 // 這裡就不能用this來獲取name了 System.out.println("執行緒名稱;"+Thread.currentThread().getName()+"="+i); //建立兩個子執行緒,並執行 if (i == 20){ new Test2().start(); new Test2().start(); } } } }

執行緒是以搶佔式的方式執行的,雖然只建立了兩個執行緒例項,實際上有三個執行緒在執行
(兩個子執行緒,一個主執行緒main)

通過setName(String name)的方式來為執行緒設定名稱,也可以通過getName的方式來得到執行緒的名稱。
在預設情況下,主執行緒的名稱為main,使用者啟動的多執行緒的名稱依次為Thread-0,Thread-1,Thread-3..Thread-n

實現Runnable介面建立執行緒

  1. 定義Runnable介面的實現類,並重寫該介面的run方法
  2. 建立Runnable實現類的例項物件,並以此例項物件作為Thread的target來建立Thread類,該Thread物件才是真正的執行緒物件。

  3. 呼叫執行緒物件的start()方法來啟動該執行緒
public class Test3 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i < 100;i++) {
            // 實現了Runnable介面的類其本質並不是執行緒類,因此沒有getName方法,
            // 因此需要通過Thread類來獲取當前執行緒,僅僅是一個任務體,仍需交給Thread去執行
            System.out.println("執行緒名稱:"+Thread.currentThread().getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        /*new一個實現了Runnable介面的例項,這個例項不是執行緒物件
        * 不能test3.start()來執行子執行緒,執行run方法體
        * 實際的執行緒物件要通過new Thread()來獲取,只不過對於實現了Runnable介面的實現類的例項
        *   作為引數傳入到new Thread(test3).start()來執行子執行緒
        *   意義是讓執行緒物件來執行test3例項的run方法體
        * */
        Test3 test3 = new Test3();
        new Thread(test3).start();
        new Thread(test3).start();
    }

}

又因為Runnable是一個函式式介面,所以可以使用lamda表示式來進行程式碼編寫

public class Test4 {




    public static void main(String[] args) {

        /*
        * 用lamda表示式的寫法,{}裡面寫的就是run方法體
        * 將runnable傳入到 new Thread(runnable,"子執行緒1")裡面,就表示了建立了子執行緒
        *   並執行run方法體,第二個引數是為子執行緒起名字
        * */
        Runnable runnable = ()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("執行緒名字:"+Thread.currentThread().getName()+"="+i);
            }
        };

        Test4 test4 = new Test4();
        new Thread(runnable,"子執行緒1").start();
        new Thread(runnable,"子執行緒2").start();
    }

}

通過對比上面兩種建立執行緒的方式,繼承Thread 和 實現Runnable介面,第一種主執行緒和子執行緒

分別執行一遍任務。第二種主執行緒和子執行緒共同完成一個任務。

Ø 使用Callable&Future建立執行緒

在Java 1.5開始,Java提供了Callable介面,該介面實際上可看成是Runnable介面的增強版,Callable介面提供了一個call()的方法可以作為執行緒的執行體,但call()方法比run()方法功能更加強大。

這是因為:

1. call()方法可以有返回值

2. call()方法可以宣告丟擲異常

因此我們可以提供一個Callable物件作為Thread的target,而該執行緒的執行緒執行體就是該Callable物件的call()方法。但是存在以下兩個問題:

1. Callable介面是Java 5提供的一個新的介面而不是Runnable介面的子介面,所以Callable物件不能直接作為Thread類的target目標執行類。

2. call()方法還有一個返回值——call()方法並不是直接呼叫,它是作為執行緒執行體被呼叫的。如何獲取call()方法的返回值?

為了解決以上兩個問題,Java 1.5提供了Future介面來代表Callable接口裡call()方法的返回值,併為Future介面提供了一個FutureTask實現類,該實現類實現了Future介面,並實現了Runable介面——可以作為Thread類的target。

建立並啟動有返回值的執行緒的步驟如下:

  1. 建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且該call()方法有返回值
  2. 建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了Callable物件的call()方法的返回值。

  3. 使用FutureTask物件作為Thread物件的target建立並啟動新執行緒
  4. 呼叫FutureTask物件的get()方法來獲得子執行緒結束後的返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 實現Callable介面時指定的泛型為返回值的型別
 */
public class Test5 implements Callable<Integer> {

    /*
    * 對於實現了Callable介面的類,重寫的call方法就是子執行緒要執行的方法體
    *   這個call方法與run方法的區別就是,call有返回值,可以宣告丟擲異常
    *
    * 實現的Callable介面可以看做是Runnable介面的增強版,所以可以提供一個Callable物件作為target傳給執行緒物件
    *   但是問題就是,Callable介面不是Runnable介面的子介面,不能直接作為target
    *   call方法有返回值
    *
    * */
    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 100; i++) {
            /*與實現了Runnable介面的run方法相似,也是不能使用this來獲取name*/
            System.out.println("當前執行緒名稱:"+Thread.currentThread().getName()+" "+i);
            Thread.sleep(200);
        }
        return 100;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*建立Callable物件,因為當前類實現了Callable介面  多型*/
        Callable<Integer> callable = new Test5();
        // 建立FutureTask物件,並將call物件封裝在FutureTask內部,FutureTask的泛型為Callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        /*建立執行緒物件*/
        new Thread(futureTask).start();
        // 獲取執行緒結束後的返回值
        System.out.println("執行緒執行結束後的返回值:"+futureTask.get());

    }
}

Callable介面是一個函式式介面,所以可以用lamda表示式寫法

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class Test6 {

    public static void main(String[] args) throws Exception {
        /*
        * Callable介面也是一個函式式介面,所以可以用lamda表示式寫法
        * {} 裡面寫的就是call的方法體
        * */
        Callable<Integer> callable = ()->{
            for (int i = 0; i < 100; i++) {
                System.out.println("執行緒名稱:"+Thread.currentThread().getName()+"="+i);
            }
            return 100;
        };

        // 建立FutureTask物件,並將call物件封裝在FutureTask內部,FutureTask的泛型為Callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        /*建立執行緒物件  並將FutureTask封裝好的Callable物件傳入執行緒物件中*/
        new Thread(futureTask).start();

        // 獲取執行緒結束後的返回值
        System.out.println("執行緒執行結束後的返回值:"+futureTask.get());

    }

}

Ø 建立執行緒的三種方式比較

通過繼承Thread類或實現Runnable、Callable介面都可以實現多執行緒,不過實現Runnable介面與實現Callable介面的方式基本相同,只是Callable接口裡定義的方法有返回值,可以宣告丟擲異常,並且Callable需要FutureTask來進行封裝成Thread可識別的target目標。因此可以將實現Runnable介面和實現Callable介面歸納為一種方式。這種方式與繼承Thread方式之間的主要差別如下

執行緒的宣告週期

當執行緒被建立並啟動後,並不是一啟動就進入了執行狀態,也不是一直處於執行狀態,

線上程的生命週期中,它要經歷新建、就緒、執行、阻塞和死亡5種狀態。

尤其是當執行緒啟動以後,它不可能一直佔用CPU獨自執行,所以CPU需要在多條執行緒之間切換,於是執行緒狀態也會在執行、阻塞之間切換。

新建狀態:當new了一個執行緒之後,該執行緒就處於新建狀態,此時它和其他的Java物件一樣僅僅由Java虛擬機器為其分配記憶體,並初始化其他成員變數的值

就緒狀態:當執行緒物件呼叫了start方法之後,該執行緒就處於就緒狀態,Java虛擬機器會為這個執行緒物件建立方法呼叫棧和程式計數器,處於這個狀態中的執行緒並沒有開始執行,只是表示該執行緒可以運行了。至於什麼時候開始執行,則取決於JVM裡的執行緒排程器的排程。

執行狀態:處於就緒狀態的執行緒獲得了CPU,開始執行run方法的執行緒執行體,則該執行緒就處於執行狀態

阻塞狀態:

  • 執行緒呼叫sleep()方法主動放棄所佔用的處理器資源
  • 執行緒呼叫了一個阻塞式IO方法,在該方法返回之前,該執行緒被阻塞
  • 執行緒試圖獲得一個同步監視器,但該同步監視器正被其他執行緒所持有
  • 執行緒在等待某個通知(notify)

  • 程式呼叫了執行緒的suspend()方法將該執行緒掛起。但這個方法容易導致死鎖,所以應該儘量避免使用該方法

死亡狀態

  • run()或call()方法執行完成,執行緒正常結束
  • 執行緒丟擲一個未捕獲的Exception或者直接Error錯誤
  • 直接呼叫該執行緒的stop()方法來結束該執行緒——該方法容易導致死鎖,通常不推薦使用

當主執行緒結束時,其他執行緒不受任何影響,並不會隨之結束。一旦子執行緒啟動之後,它就擁有和主執行緒相同的地位

不要試圖對一個已經死亡的執行緒呼叫start()方法使它重新啟動,死亡就是死亡,該執行緒不可以再次作為執行緒執行。

控制執行緒