淺談 Java多執行緒

城北有個混子發表於2020-09-25

執行緒與程式

什麼是程式?

  當一個程式進入記憶體中執行起來它就變為一個程式。因此,程式就是一個處於執行狀態的程式。同時程式具有獨立功能,程式是作業系統進行資源分配和排程的獨立單位。

什麼是執行緒?

  執行緒是程式的組成部分通常情況下,一個程式可擁有多個執行緒,而一個執行緒只能擁有一個父程式。

  執行緒可以擁有自己的堆疊、自己的程式計數器及自己的區域性變數,但是執行緒不能擁有系統資源,它與其父程式的其他執行緒共享程式中的全部資源,這其中包括程式的程式碼段、資料段、堆空間以及一些程式級的資源(例如,開啟的檔案等)。

  執行緒是程式的執行單元,是CPU排程和分派的基本單位,當程式被初始化之後,主執行緒就會被建立。同時如果有需要,還可以在程式執行過程中建立出其他執行緒,這些執行緒之間也是相互獨立的,並且在同一程式中併發執行。因此一個程式中可以包含多個執行緒,但是至少要包含一個執行緒,即主執行緒。

一個程式中的執行緒

 Java中的執行緒

  Java 中使用Thread類表示一個執行緒。所有的執行緒物件都必須是Thread或其子類的物件。Thread 類中的 run 方法是該執行緒的執行程式碼。讓我們來看一個例項:

public class Ticket extends Thread{
    // 重寫run方法
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        // 1.建立執行緒
        Thread thread1 = new Ticket();
        Thread thread2 = new Ticket();
        
        // 2.啟動執行緒
        thread1.start();
        thread2.start();
    }
}

  執行結果如下:

  通過上面的程式碼和執行結果,我們可以得到:

執行緒執行的幾個特點:

  1.同一程式下不同執行緒的排程不由程式控制。執行緒的執行是搶佔式的,執行的順序和執行緒的啟動順序是無關的,當前執行的執行緒隨時都可能被掛起,然後其他程式搶佔執行。

  2.執行緒獨享自己的堆疊程式計數器和區域性變數。兩個程式的區域性變數互不干擾,各自的執行順序也是互不干擾。

  3.兩個執行緒併發執行。兩個執行緒同時向前推進,並沒有說執行完一個後再執行另一個。

start()方法和run()方法:

  啟動一個執行緒必須呼叫Thread 類的 start()方法,使該執行緒處於就緒狀態,這樣該執行緒就可以被處理器排程。

    run()方法是一個執行緒所關聯的執行程式碼,無論是派生自 Thread類的執行緒類,還是實現Runnable介面的類,都必須實現run()方法,run()方法裡是我們需要執行緒所執行的程式碼。

  實現多執行緒必須呼叫Thread 類的 start()方法來啟動執行緒,使執行緒處於就緒狀態隨時供CPU排程。如果直接呼叫run()方法的話,只是呼叫了Thread類的一個普通方法,會立即執行該方法中的程式碼,並沒有實現多執行緒技術。

Java中多執行緒的實現方法

  在Java中有三種方法實現多執行緒。

    第一種方法:使用Thread類或者使用一個派生自Thread 類的類構建一個執行緒。

    第二種方法:實現Runnable 介面來構建一個執行緒。(推薦使用)

    第三種方法:實現Callable 介面來構建一個執行緒。(有返回值)

第一種方法

  使用Thread類或者使用一個派生自Thread 類的類構建一個執行緒。

public class Ticket extends Thread{
    // 重寫run方法
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(getName() + ": " + i);
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        // 1.建立執行緒
        Thread thread1 = new Ticket();
        Thread thread2 = new Ticket();
        
        // 2.啟動執行緒
        thread1.start();
        thread2.start();
    }
}

  看上面的程式碼,我們建立了一個Ticket類,它繼承了Thread類,重寫了Thread類的run方法。然後我們用Ticket類建立了兩個執行緒,並且啟動了它們。我們不推薦使用這種方法,因為一個類繼承了Thread類,那它就沒有辦法繼承其他類了,這對較為複雜的程式開發是不利的。

 第二種方法

  實現Runnable 介面來構建一個執行緒。

public class Ticket implements Runnable{
    // 重寫run方法
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        // 1.建立執行緒
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        Thread thread1 = new Thread(t1, "買票1號");
        Thread thread2 = new Thread(t2, "買票2號");
        
        // 2.啟動執行緒
        thread1.start();
        thread2.start();
    }
}

  我們建立了一個Ticket類,實現了Runnable介面,在該類中實現了run方法。在啟動執行緒前,我們要建立一個執行緒物件,不同的是我們要將一個實現了Runnable介面的類的物件作為Thread類構造方法的引數傳入,以構建執行緒物件。構造方法Thread的第二個引數用來指定該執行緒的名字,通過Thread.currentThread().getName()可獲取當前執行緒的名字。

  在真實的專案開發中,推薦使用實現Runnable介面的方法進行多執行緒程式設計。因為這樣既可以實現一個執行緒的功能,又可以更好地複用其他類的屬性和方法。

第三種方法

  實現Callable 介面來構建一個執行緒。

public class TestThread {
    public static void main(String[] args) {    
        // 1.建立Callable的例項
        Callable<String> callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(7000);
                return "我結束了";
            }
        };
        
        // 2.通過FutureTask介面的例項包裝Callable的例項
        FutureTask<String> futureTask = new FutureTask<String>(callable);
        
        // 3.建立執行緒並啟動
        new Thread(futureTask).start();
        
        // 4.獲得結果並列印
        try {
            System.out.println(futureTask.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  首先我們用匿名內部類建立了一個實現Callable介面的類的物件,然後通過FutureTask 的例項包裝了Callable的例項,這樣我們就可以通過一個Thread 物件在新執行緒中執行call()方法,同時又可以通過get方法獲取到call()的返回值。然後建立執行緒並啟動它,最後線上程執行完執行完call()方法後得到返回值並列印。

  我們來看一下Callable的原始碼:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

  從Callable 的定義可以看出,Callable介面是一個泛型介面,它定義的call()方法類似於Runnable 的run()方法,是執行緒所關聯的執行程式碼。但是與run()方法不同的是,call()方法具有返回值,並且泛型介面的引數V指定了call()方法的返回值型別。同時,如果call()方法得不到返回值將會丟擲一個異常,而在Runnable的run()方法中不能丟擲異常。

如何獲得call()方法的返回值呢?

  通過Future介面來獲取。Future介面定義了一組對 Runnable 或者Callable 任務的執行結果進行取消、查詢、獲取、設定的操作。其中get方法用於獲取call()的返回值,它會發生阻塞,直到call()返回結果。

這樣的執行緒呼叫與直接同步呼叫函式有什麼差異呢?

  在上面的例子中,通過future.get()獲取 call()的返回值時,由於call方法中會 sleep 7s,所以在執行future.get()的時候主執行緒會被阻塞而什麼都不做,等待call()執行完並得到返回值。但是這與直接呼叫函式獲取返回值還是有本質區別的。

  因為call()方法是執行在其他執行緒裡的,在這個過程中主執行緒並沒有被阻塞,還是可以做其他事情的,除非執行future.get()去獲取 call()的返回值時主執行緒才會被阻塞。所以當呼叫了Thread.start()方法啟動 Callable 執行緒後主執行緒可以執行別的工作,當需要call()的返回值時再去呼叫future.get()獲取,此時call()方法可能早已執行完畢,這樣就可以既確保耗時操作在工作執行緒中完成而不阻擋主執行緒,又可以得到執行緒執行結果的返回值。而直接呼叫函式獲取返回值是一個同步操作,該函式本身就是執行在主執行緒中,所以一旦函式中有耗時操作,必然會阻擋主執行緒。

 

相關文章