1. 程式人生 > >【併發那些事 】建立執行緒的三種方式

【併發那些事 】建立執行緒的三種方式

建立執行緒可以說是併發知識中最基礎的操作了,JDK 提供的建立執行緒的方式,如果不包括通過執行緒池的話,目前有三種形式,它們分別是通過繼承 Thread 類,通過實現 Runable 介面,通過 FutureTask。如下圖所示

下面整理了一下 3 種方法的具體使用與異同。

建立執行緒的 3 種方法

1. 繼承 Thread

  1. 建立一個類繼承 Thread 並覆蓋 run 方法
class MyThread extends Thread {
    @Override
    public void run() {
        String threadName = getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("執行緒[" + threadName + "]執行開始,i = " + i + " time = " + new Date());
        }
    }
}
  1. 建立並啟動執行緒
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();

        myThread1.start();
        myThread2.start();

        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("執行緒[" + threadName + "]執行開始,i = " + i + " time = " + new Date());
        }

整體流程如下:

這裡步驟比較簡單和清晰

  1. 建立一個類,這類要繼承 Thread
  2. 覆蓋 Thread 的 run 方法,並在此方法中實現你的多執行緒任務
  3. 建立這個類的例項
  4. 呼叫它的 start() 方法(這裡要注意,新手容易直接呼叫 run 方法,那樣只是普通呼叫,而不多執行緒呼叫)

2. 實現 Runable

  1. 建立一個類實現 Runable 介面,並覆蓋 run 方法
class MyRunable implements Runnable {
    
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        for (int i = 0; i < 20; i++) {
            System.out.println("執行緒[" + threadName + "]執行開始,i = " + i + " time = " + new Date());
        }
    }
}
  1. 建立類的實現,並將它傳給 Thread 的建構函式來建立 Thread 
MyRunable myRunable = new MyRunable();

new Thread(myRunable).start();
new Thread(myRunable).start();

String threadName = Thread.currentThread().getName();
for (int i = 0; i < 20; i++) {
    System.out.println("執行緒[" + threadName + "]執行開始,i = " + i + " time = " + new Date());
}

整體流程如下:

具體步驟如下:

  1. 建立一個實現了 Runable 介面的類
  2. 覆蓋 run 方法,並在此方法中實現你的多執行緒任務
  3. 建立 Runable 介面實現類的例項
  4. 通過把上步得到的 Runable 介面實現類的例項,傳給  Thread 的有參建構函式來建立 Thread 的例項
  5. 呼叫上步得來的 Thread 例項的 start() 方法(這裡要注意,新手容易直接呼叫 run 方法,那樣只是普通呼叫,而不多執行緒呼叫)

3. 通過 FutureTask

  1. 建立一個實現了 Callable 介面的類,並實現call 方法 (T 代表你想要的返回值型別)
class MyCallerTask implements Callable<String> {
    
    @Override
    public String call() throws Exception {
        System.out.println("執行任務開始");
        Thread.sleep(3000);
        System.out.println("執行任務結束");
        return "hello";
    }
}
  1. 建立並啟動執行緒
        // 建立非同步任務
        FutureTask<String> futureTask = new FutureTask<>(new MyCallerTask());
        // 啟動執行緒
        new Thread(futureTask).start();
        System.out.println("其它操作");
        try {
            // 等待任務執行完,並獲得任務執行完的結果
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

整體流程如下:

具體步驟如下:

  1. 建立一個實現了 Callable 介面的類,這裡 T 的型別就是你執行緒任務想要返回的型別
  2. 覆蓋 call 方法,並在此方法中實現你的多執行緒任務
  3. 建立 Callable 介面實現類的例項
  4. 通過把上步得到的 Callable 介面實現類的例項,傳給  FutureTask 的有參建構函式來建立 FutureTask 的例項
  5. 通過把上步得到的 FutureTask 例項,傳給  Thread 的有參建構函式來建立 Thread 的例項
  6. 呼叫上步得來的 Thread 例項的 start() 方法(這裡要注意,新手容易直接呼叫 run 方法,那樣只是普通呼叫,而不多執行緒呼叫)
  7. 如果你還想獲得返回值,需要再呼叫 FutureTask 例項的 get() 方法(這裡要注意,get()會阻塞執行緒)

3種方法的優缺點

通過上述的演示程式碼,可以看出這 3 種方法,其實各有優缺點

複雜程度

通過程式碼量與邏輯可以明顯感覺出來,第一種直接繼承 Thread 最方便,並且其它兩種到最後,還是要依賴建立 Thread 才能實現。所以從方便及難易程度來看,可以得到如下結論:

可擴充套件性

通過演示程式碼可以看出,只有第一種是通過繼承,其它兩種是通過實現介面的形式。我們都知道 JAVA 是不允許多繼承,但是可以多實現。所以如果使用了第一種方法,就無法再繼承別的類了。另外第一種把執行緒與執行緒任務冗餘在了一起,不利於後期的維護。所以可以得到如下結論:

是否有返回值

從程式碼中可以很容易看出,只有通過 FutureTask 的方式才有返回值,另外兩種均沒有,所以得出如下結論

該用哪種方式建立執行緒

如果要用到返回值,那不用想,肯定只能使用 FutureTask 的方法。如果對於返回值沒有要求,那Thread 與  Runable 均可,不過,考慮到可擴充套件性,最好使用 Runable 的形式。不過,話說回來,如果在真正專案中使用,綜合考慮,一般還是最推薦通過執行緒池去建立