多執行緒與高併發(一)多執行緒入門

茶底世界發表於2019-06-25

一、基礎概念

多執行緒的學習從一些概念開始,程式和執行緒,併發與並行,同步與非同步,高併發。

1.1 程式與執行緒

幾乎所有的作業系統都支援同時執行期多個任務,所有執行中的任務通常就是一個程式,程式是處於執行過程中的程式,程式是作業系統進行資源分配和排程的一個獨立單位。

程式有三個如下特徵:

  • 獨立性:程式是系統中獨立存在的實體,它可以擁有自己獨立的資源,每一個程式都擁有自己私有的地址空間。在沒有經過程式本身允許的情況下,一個使用者程式不可以直接訪問其他程式的地址空間。

  • 動態性:程式與程式的區別在於,程式只是一個靜態的指令集合,而程式是一個正在系統中活動的指令集合。在程式中加入了時間的概念,程式具有自己的生命週期和各種不同的狀態,這些概念在程式中部是不具備的。

  • 併發性:多個程式可以在單個處理器上併發執行,多個程式之間不會互相影響。

執行緒是程式的組成部分,一個程式可以擁有多個執行緒,而執行緒必須有一個父程式,執行緒可以有自己的堆疊、自己的程式計數器和自己的區域性變數,但不擁有系統資源。比如使用QQ時,我們可以同事傳檔案,傳送圖片,聊天,這就是多個執行緒在進行。

執行緒可以完成一定的任務,執行緒能夠獨立執行的,它不知道有其他執行緒的存在,執行緒的執行是搶佔式的,當前執行緒隨時可能被掛起。

總之:一個程式執行後至少有一個程式,一個程式裡可以有多個執行緒,但至少要有一個執行緒。

1.2 併發和並行

併發和並行是比較容易混淆的概念,他們都表示兩個或者多個任務一起執行,但併發側重多個任務交替執行,同一時刻只能有一條指令執行,但多個程式指令被快速輪換執行,使得在巨集觀上具有多個程式同時執行的效果。而並行確實真正的同時執行,有多條指令在多個處理器上同時執行,並行的前提條件就是多核CPU。

1.3 同步和非同步

同步和非同步通常用來形容一次方法呼叫。同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續後續的行為。非同步方法呼叫更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者可以繼續後續的操作。

1.4 高併發

高併發一般是指在短時間內遇到大量操作請求,非常具有代表性的場景是秒殺活動與搶票,高併發是網際網路分散式系統架構設計中必須考慮的因素之一,高併發相關常用的一些指標有響應時間(Response Time),吞吐量(Throughput),每秒查詢率QPS(Query Per Second),併發使用者數等。

多執行緒在這裡只是在同/非同步角度上解決高併發問題的其中的一個方法手段,是在同一時刻利用計算機閒置資源的一種方式

1.5 多執行緒的好處

執行緒在程式中是獨立的、併發的執行流,擁有獨立的記憶體單元,多個執行緒共享父程式裡的全部資源,執行緒共享的環境有程式的程式碼段,程式的公有資料等,利用這些共享資料,執行緒很容易實現相互之間的通訊,可以提高程式的執行效率。

多執行緒的好處主要有:

  • 程式之間不能共享記憶體,但執行緒之間共享記憶體非常容易。

  • 系統建立程式時需要給程式重新分配系統資源,但建立執行緒代價小得多,所以使用多執行緒實現多工併發比多程式效率高

  • Java語言內建了多執行緒功能支援。

二、使用多執行緒

上面講了多執行緒的一些概念,都有些抽象,下面將學習如何使用多執行緒,建立多執行緒的方式有三種。

2.1 繼承Thread類建立

繼承Thread建立並啟動多執行緒有三個步驟:

  1. 定義類並繼承Thread,重寫run()方法,run()方法中為需要多執行緒執行的任務。

  2. 建立該類的例項,即建立了執行緒物件。

  3. 呼叫例項的start()方法啟動執行緒。

public class FirstThread extends Thread {

    private int i=0;
    public void run() {
        for (; i < 100; i++) {
            //獲取當前執行緒名稱
            System.out.println(this.getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前執行緒
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //建立執行緒並啟動
                new FirstThread().start();
                new FirstThread().start();
            }

        }
    }
}

執行結果可以看到兩個執行緒的i並不是連續的,說明他們並不共享資料。

2.2 實現Runnable介面

實現Runnable介面建立並啟動多執行緒也有以下步驟:

  1. 定義類並繼承Runnable介面,重寫run()方法,run()方法中為需要多執行緒執行的任務。

  2. 建立該類的例項,並以此例項作為target為引數來建立Thread物件,這個Thread物件才是真正的多執行緒物件。

public class SecondThread implements Runnable {
    private int i = 0;
    
    @Override
    public void run() {
        for (; i < 100; i++) {
            //此時想要獲取到多執行緒物件,只能使用Thread.currentThread()方法
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前執行緒
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //建立執行緒並啟動
                SecondThread secondThread=new SecondThread();
                new Thread(secondThread,"執行緒一").start();
                new Thread(secondThread,"執行緒二").start();
            }

        }
    }
}

2.3 使用Callable和Future

Callable是Runnable的增加版,主要是介面中的call()方法可以有返回值,並且可以申明丟擲異常,使用Callable建立的步驟如下:

  1. 定義類並繼承Callable介面,重寫call()方法,run()方法中為需要多執行緒執行的任務。

  2. 建立類例項,使用FutureTask來包裝物件例項,

  3. 使用FutureTask物件作為Thread的target來建立多執行緒,並啟動執行緒。

  4. 呼叫FutureTask物件的get()方法來獲取子執行緒結束後的返回值。

public class ThirdThread {

    public static void main(String[] args) {
        //使用lambda表示式
        FutureTask<Integer> task = new FutureTask<>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + "的迴圈變數i的值:" + i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            //Thread的靜態方法currentThread,獲取當前執行緒
            System.out.println(Thread.currentThread().getName());
            if (i == 20) {
                //建立執行緒並啟動
                new Thread(task, "有返回值的執行緒").start();
            }
        }
        try {
            System.out.println("執行緒的返回值:" + task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

這裡使用了lambda表示式,不使用表示式的方式也很簡單,可以去原始碼中檢視。Callable與Runnable方式基本相同,只不過增加了返回值且可允許宣告丟擲異常。

使用三種方式都可以建立執行緒,且方式也相對簡單,大體分為實現介面和實現Thread類兩種,這兩種都各有優缺點。

繼承介面實現:

  • 優點:除了繼承介面之外,還可以繼承其他類。這種方式多個執行緒共享一個target物件,可以處理用於共同資源的情況。
  • 缺點:程式設計稍微複雜一些,並且沒有直接獲取當前執行緒物件的方式,必須使用Thread.currentThread()方式。

基礎Thread類:

  • 優點:程式設計簡單

  • 缺點:不能繼承其他類

三、多執行緒的生命週期

執行緒狀態是執行緒中非常重要的一個概念,然而我看過很多資料,執行緒的狀態理解有很多種方式,很多人將其分為五個基本狀態:新建、就緒、執行、阻塞、死亡,但在狀態列舉中並不是這五個狀態,我不知道是什麼原因(有大神可以解答更好),只能按照列舉中的狀態根據自己的理解。

  1. 初始(NEW):新建立了一個執行緒物件,但還沒有呼叫start()方法,而且就算呼叫了改方法也不代表狀態立即改變。

  2. 執行(RUNNABLE):在執行的狀態肯定就處於RUNNABLE狀態。

  3. 阻塞(BLOCKED):表示執行緒阻塞,或者說執行緒已經被掛起了。

  4. 等待(WAITING):進入該狀態的執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)。

  5. 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。

  6. 終止(TERMINATED):表示該執行緒已經執行完畢。

狀態流程圖如下:

理解:初始狀態很好理解,這個時候其實還不能被稱為一個執行緒,因為他還沒被啟動,當呼叫start()方法後,執行緒正式啟動,但是也不代表立即就改變了狀態。

執行狀態中其實包含兩種狀態,執行中(RUNING)就緒(READY)

就緒狀態表示你有資格執行,只要CPU還未排程到你,就處於就緒狀態,有幾個狀態會是執行緒狀態程式設計就緒狀態

  • 呼叫執行緒的start()方法。

  • 當前執行緒sleep()方法結束,其他執行緒join()結束,等待使用者輸入完畢,某個執行緒拿到物件鎖。

  • 當前執行緒時間片用完了,呼叫當前執行緒的yield()方法。

  • 鎖池裡的執行緒拿到物件鎖後。

執行中(RUNING)狀態比較好理解,執行緒排程程式選擇了當前執行緒作。

阻塞狀態是執行緒阻塞在進入synchronized關鍵字修飾的方法或程式碼塊(獲取鎖)時的狀態。

等待狀態是指執行緒沒有被CPU分配執行時間,需要等待,這種等待是需要被顯示的喚醒,否則會無限等待下去。

超時等待狀態是這現在沒有被CPU分配執行時間,需要等待,不過這種等待不需要被顯示的喚醒,會設定一定的時間後zi懂喚醒。

死亡狀態也很好理解,說明執行緒方法被執行完成,或者出錯了,執行緒一旦進入這個狀態就代表徹底的結束

 

相關文章