Java併發學習之四種線程建立方式的實現與對比

線程建立的幾種方式

在併發編程中,最基本的就是建立線程了,那麼通常的建立姿式是怎樣的,又都有些什麼區別java

通常來說線程建立有四種方式:編程

  1. 繼承Thread
  2. 實現Runnable接口
  3. 實現Callable接口,結合 FutureTask使用
  4. 利用該線程池ExecutorService、Callable、Future來實現

因此本篇博文從佈局來說,分爲兩部分併發

  1. 實例演示四種使用方式
  2. 對比分析四種使用方式的異同,以及適合的應用場景

I. 實例演示

目標: 建立兩個線程併發實現從1-1000的累加框架

1. 繼承Thread實現線程建立

實現邏輯以下ide

public class AddThread extends Thread {

    private int start, end;

    private int sum = 0;

    public AddThread(String name, int start, int end) {
        super(name);
        this.start = start;
        this.end = end;
    }
    public void run() {
        System.out.println("Thread-" + getName() + " 開始執行!");
        for (int i = start; i <= end; i ++) {
            sum += i;
        }
        System.out.println("Thread-" + getName() + " 執行完畢! sum=" + sum);
    }

    public static void main(String[] args) throws InterruptedException {
        int start = 0, mid = 500, end = 1000;

        AddThread thread1 = new AddThread("線程1", start, mid);
        AddThread thread2 = new AddThread("線程2", mid + 1, end);

        thread1.start();
        thread2.start();

        // 確保兩個線程執行完畢
        thread1.join();
        thread2.join();

        int sum = thread1.sum + thread2.sum;
        System.out.println("ans: " + sum);
    }
}

輸出結果佈局

Thread-線程1 開始執行!
Thread-線程2 開始執行!
Thread-線程1 執行完畢! sum=125250
Thread-線程2 執行完畢! sum=375250
ans: 500500

通常實現步驟:學習

  • 繼承 Thread
  • 覆蓋 run() 方法
  • 直接調用 Thread#start() 執行

邏輯比較清晰,只須要注意覆蓋的是run方法,而不是start方法this

2. 實現Runnable接口方式建立線程

public class AddRun implements Runnable {

    private int start, end;
    private int sum = 0;

    public AddRun(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 開始執行!");
        for(int i = start; i <= end; i++) {
            sum += i;
        }
        System.out.println(Thread.currentThread().getName() + " 執行完畢! sum=" + sum);
    }


    public static void main(String[] args) throws InterruptedException {
        int start = 0, mid = 500, end = 1000;
        AddRun run1 = new AddRun(start, mid);
        AddRun run2 = new AddRun(mid + 1, end);
        Thread thread1 = new Thread(run1, "線程1");
        Thread thread2 = new Thread(run2, "線程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        int sum = run1.sum + run2.sum;
        System.out.println("ans: " + sum);
    }
}

輸出結果線程

線程2 開始執行!
線程1 開始執行!
線程2 執行完畢! sum=375250
線程1 執行完畢! sum=125250
ans: 500500

通常實現步驟:code

  • 實現Runnable接口
  • 獲取實現Runnable接口的實例,做爲參數,建立Thread實例
  • 執行 Thread#start() 啓動線程

說明

相比於繼承Thread,這裏是實現一個接口,最終依然是藉助 Thread#start()來啓動線程

而後就有個疑問:

二者是否有本質上的區別,在實際項目中如何抉擇?

3. 實現Callable接口,結合FutureTask建立線程

Callable接口相比於Runnable接口而言,會有個返回值,那麼如何利用這個返回值呢?

demo以下

public class AddCall implements Callable<Integer> {

    private int start, end;

    public AddCall(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        System.out.println(Thread.currentThread().getName() + " 開始執行!");
        for (int i = start; i <= end; i++) {
            sum += i;
        }
        System.out.println(Thread.currentThread().getName() + " 執行完畢! sum=" + sum);
        return sum;
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int start = 0, mid = 500, end = 1000;
        FutureTask<Integer> future1 = new FutureTask<>(new AddCall(start, mid));
        FutureTask<Integer> future2 = new FutureTask<>(new AddCall(mid + 1, end));

        Thread thread1 = new Thread(future1, "線程1");
        Thread thread2 = new Thread(future2, "線程2");

        thread1.start();
        thread2.start();

        int sum1 = future1.get();
        int sum2 = future2.get();
        System.out.println("ans: " + (sum1 + sum2));
    }
}

輸出結果

線程2 開始執行!
線程1 開始執行!
線程2 執行完畢! sum=375250
線程1 執行完畢! sum=125250
ans: 500500

通常實現步驟:

  • 實現Callable接口
  • Callable的實現類爲參數,建立FutureTask實例
  • FutureTask做爲Thread的參數,建立Thread實例
  • 經過 Thread#start 啓動線程
  • 經過 FutreTask#get() 阻塞獲取線程的返回值

說明

Callable接口相比Runnable而言,會有結果返回,所以會由FutrueTask進行封裝,以期待獲取線程執行後的結果;

最終線程的啓動都是依賴Thread#start

4. 線程池方式建立

demo以下,建立固定大小的線程池,提交Callable任務,利用Future獲取返回的值

public class AddPool implements Callable<Integer> {
    private int start, end;

    public AddPool(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        System.out.println(Thread.currentThread().getName() + " 開始執行!");
        for (int i = start; i <= end; i++) {
            sum += i;
        }
        System.out.println(Thread.currentThread().getName() + " 執行完畢! sum=" + sum);
        return sum;
    }

    public static void main(String[] arg) throws ExecutionException, InterruptedException {
        int start=0, mid=500, end=1000;
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<Integer> future1 = executorService.submit(new AddPool(start, mid));
        Future<Integer> future2 = executorService.submit(new AddPool(mid+1, end));

        int sum = future1.get() + future2.get();
        System.out.println("sum: " + sum);
    }
}

輸出

pool-1-thread-1 開始執行!
pool-1-thread-2 開始執行!
pool-1-thread-1 執行完畢! sum=125250
pool-1-thread-2 執行完畢! sum=375250
sum: 500500

通常實現邏輯:

  • 建立線程池(能夠利用JDK的Executors,也可本身實現)
  • 建立Callable 或 Runnable任務,提交到線程池
  • 經過返回的 Future#get 獲取返回的結果

II. 對比分析

1. 分類

上面雖說是有四種方式,但實際而言,主要劃分爲兩類

  • 繼承Thread類,覆蓋run方法填寫業務邏輯
  • 實現Callable或Runnable接口,而後經過Thread或線程池來啓動線程

此外,還有一種利用Fork/Join框架來實現併發的方式,後續專門說明,此處先略過

2. 區分說明

繼承和實現接口的區別

先把線程池的方式拎出來單獨說,這裏主要對比Thread, Callable, Runnable三中方式的區別

我的理解,線程的這兩種方式的區別也就只有繼承和實現接口的本質區別:

一個是繼承Thread類,能夠直接調用實例的 start()方法來啓動線程;另外一個是實現接口,須要藉助 Thread#start()來啓動線程

繼承由於java語言的限制,當你的任務須要繼承一個自定義的類時,會有缺陷;而實現接口卻沒有這個限制


至於網上不少地方說的實現Runnable接口更利於資源共享什麼的,好比下面這種做爲對比的

public class ShareTest {
    private static class MyRun implements Runnable {
        private volatile AtomicInteger ato = new AtomicInteger(5);
        @Override
        public void run() {
            while (true) {
                int tmp = ato.decrementAndGet();
                System.out.println(Thread.currentThread() + " : " + tmp);
                if (tmp <= 0) {
                    break;
                }
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        MyRun run = new MyRun();
        Thread thread1 = new Thread(run, "線程1");
        Thread thread2 = new Thread(run, "線程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println("over");
    }
}

輸出:

Thread[線程1,5,main] : 4
Thread[線程2,5,main] : 3
Thread[線程1,5,main] : 2
Thread[線程2,5,main] : 1
Thread[線程1,5,main] : 0
Thread[線程2,5,main] : -1
over

MyRun實現Runnable接口,而後建立一個實例,將這個實例做爲多個Thread的參數構造Thread類,而後啓動線程,發現這幾個線程共享了 MyRun#ato 變量

然而上面這個實現接口改爲繼承Thread,其餘都不變,也沒啥兩樣

public class ShareTest {

    private static class MyRun extends Thread {
        private volatile AtomicInteger ato = new AtomicInteger(5);
        @Override
        public void run() {
            while (true) {
                int tmp = ato.decrementAndGet();
                System.out.println(Thread.currentThread() + " : " + tmp);
                if (tmp <= 0) {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRun run = new MyRun();
        Thread thread1 = new Thread(run, "線程1");
        Thread thread2 = new Thread(run, "線程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println("over");
    }
}

輸出以下

Thread[線程1,5,main] : 4
Thread[線程2,5,main] : 3
Thread[線程1,5,main] : 2
Thread[線程1,5,main] : 0
Thread[線程2,5,main] : 1
Thread[線程2,5,main] : -1
over

上面除了說明使用Runnable更利於資源共享神馬的,其實並無以外,還有一個比較有意思的,爲何會輸出-1?

若是我這個任務是售票的話,妥妥的就超賣了,這個問題留待後續詳解


Runnable, Callable兩種區別

這兩個就比較明顯了,最根本的就是

  • Runnable 無返回結果
  • Callable 有返回結果

從根源出發,就直接致使使用姿式上的區別

舉個形象的例子說明兩種方式的區別:

小明家今兒沒米了,小明要吃飯怎麼辦?

小明他媽對小明說,去你大爺家吃飯吧,至於小明到底吃沒吃着,小媽他媽就無論了,這就是Runnable方式;

小明他媽一想,這一家子都要吃飯,我先炒個菜,讓小明去大爺家借點米來,因此就等着小明拿米回來開鍋,這就是Callable方式

1.Runnable

Runnable不關心返回,因此任務本身默默的執行就能夠了,也不用告訴我完成沒有,我不care,您本身隨便玩,因此通常使用就是

new Thread(new Runnable() { public void run() {...} }).start()

換成JDK8的 lambda表達式就更簡單了 new Thread(() -> {}).start();

2.Callable

相比而言,callbale就悲催一點,無法這麼隨意了,由於要等待返回的結果,可是這個線程的狀態我又控制不了,怎麼辦?藉助FutrueTask來玩,因此通常能夠看到使用方式以下:

FutureTask<Object> future = new FutureTask<>(() -> null);
new Thread(future).start();
Object obj = future.get(); // 這裏會阻塞,直到線程返回值

Thread啓動和線程池啓動方式

這個就高端了,線程池一聽就感受厲害了,前面的四中方式有三種都是Thread#start()來啓動線程,這也是咱們最經常使用的方式,這裏單獨說一下線程池的使用姿式

  • 首先是建立一個線程池
  • 利用 ExecutorService#submit()提交線程
  • Future<Object> 接收返回
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> future1 = executorService.submit(()-> 10);
int ans = future1.get();

說明,這裏提交線程以後,並不表示線程立馬就要執行,也不表示必定能夠執行(這個留待後續線程池的學習中探討)


III. 小結

四種建立方式

  1. 繼承Thread類,覆蓋run方法,調用 Thread#start啓動

  2. 實現Runnable接口,建立實例,做爲Thread構造參數傳入,調用 Thread#start啓動

    new Thread(() -> {}).start()
  3. 實現Callable接口,建立實例,做爲FutureTask<>構造參數建立FutureTask對象,將FutureTask對象做爲Thread構造參數傳入,調用 Thread#start啓動

    FutureTask<Object> future = new FutureTask<>(() -> null);
    new Thread(future).start();
    Object obj = future.get(); // 這裏會阻塞,直到線程返回值
  4. 建立一個線程池,利用 ExecutorService#submit()提交線程,Future<Object> 接收返回

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    Future<Integer> future1 = executorService.submit(()-> 10);
    int ans = future1.get();

區別與應用場景

  • 繼承和實現接口的方式惟一區別就是繼承和實現的區別,不存在共享變量的問題
  • 須要獲取返回結果時,結合 FutureTask和Callable來實現
  • Thread和Runnable的兩種方式,原則上想怎麼用均可以,我的也沒啥好推薦的,隨意使用
  • 線程池須要注意線程複用時,對ThreadLocal中變量的串用問題(本篇沒有涉及,等待後續補上)

注意

  • 利用線程池建立線程,實際上依然是藉助的Runnable或者Callable,可否算一種新的方式純看我的理解
  • 採用Timer方式實現定時任務的方式,也是一種新的建立線程的方式,這裏也沒有多說,後續將有一篇專門說明定時任務的博文介紹其用法

IV. 其餘

聲明

盡信書則不如,已上內容,純屬一家之言,因本人能力通常,見識有限,若有問題,請不吝指正,感激

掃描關注,java分享

QrCode

相關文章
相關標籤/搜索