1. 程式人生 > >《Java多執行緒面試題》系列-建立執行緒的三種方法及其區別

《Java多執行緒面試題》系列-建立執行緒的三種方法及其區別

1. 建立執行緒的三種方法及其區別

1.1 繼承Thread類

首先,定義Thread類的子類並重寫run()方法:

package com.zwwhnly.springbootaction.javabase.thread;

public class MyFirstThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.printf("[MyFirstThread]輸出:%d,當前執行緒名稱:%s\n",
                    i, getName());
        }
    }
}

然後,建立該子類的例項並呼叫start()方法啟動執行緒:

package com.zwwhnly.springbootaction.javabase.thread;

public class ThreadTest {
    public static void main(String[] args) {
        System.out.println("主執行緒開始執行,當前執行緒名稱:" +
                Thread.currentThread().getName());

        Thread firstThread = new MyFirstThread();
        firstThread.start();

        System.out.println("主執行緒執行結束,當前執行緒名稱:" +
                Thread.currentThread().getName());
    }
}

執行結果如下所示:

主執行緒開始執行,當前執行緒名稱:main

主執行緒執行結束,當前執行緒名稱:main

[MyFirstThread]輸出:0,當前執行緒名稱:Thread-0

[MyFirstThread]輸出:1,當前執行緒名稱:Thread-0

[MyFirstThread]輸出:2,當前執行緒名稱:Thread-0

[MyFirstThread]輸出:3,當前執行緒名稱:Thread-0

[MyFirstThread]輸出:4,當前執行緒名稱:Thread-0

從執行結果可以看出以下2個問題:

  1. 程式中存在2個執行緒,分別為主執行緒main和自定義的執行緒Thread-0。
  2. 呼叫firstThread.start();
    ,run()方法體中的程式碼並沒有立即執行,而是非同步執行的。

檢視Thread類的原始碼,可以發現Thread類實現了介面Runnable:

public class Thread implements Runnable {
    // 省略其它程式碼
}

這裡是重點,面試常問!

1.2 實現Runnable介面(推薦)

首先,定義Runnable介面的實現類並實現run()方法:

package com.zwwhnly.springbootaction.javabase.thread;

public class MySecondThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.printf("[MySecondThread]輸出:%d,當前執行緒名稱:%s\n",
                    i, Thread.currentThread().getName());
        }
    }
}

然後,呼叫Thread類的建構函式建立Thread例項並呼叫start()方法啟動執行緒:

package com.zwwhnly.springbootaction.javabase.thread;

public class ThreadTest {
    public static void main(String[] args) {
        Runnable target = new MySecondThread();
        Thread secondThread = new Thread(target);
        secondThread.start();
    }
}

執行結果如下所示:

主執行緒開始執行,當前執行緒名稱:main

主執行緒執行結束,當前執行緒名稱:main

[MySecondThread]輸出:0,當前執行緒名稱:Thread-0

[MySecondThread]輸出:1,當前執行緒名稱:Thread-0

[MySecondThread]輸出:2,當前執行緒名稱:Thread-0

[MySecondThread]輸出:3,當前執行緒名稱:Thread-0

[MySecondThread]輸出:4,當前執行緒名稱:Thread-0

可以看出,使用這種方式和繼承Thread類的執行結果是一樣的。

1.3 實現Callable介面

首先,定義Callable介面的實現類並實現call()方法:

package com.zwwhnly.springbootaction.javabase.thread;

import java.util.Random;
import java.util.concurrent.Callable;

public class MyThirdThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        Thread.sleep(6 * 1000);
        return new Random().nextInt();
    }
}

然後,呼叫FutureTask類的建構函式建立FutureTask例項:

Callable<Integer> callable = new MyThirdThread();
FutureTask<Integer> futureTask = new FutureTask<>(callable);

最後,呼叫Thread類的建構函式建立Thread例項並呼叫start()方法啟動執行緒:

package com.zwwhnly.springbootaction.javabase.thread;

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

public class ThreadTest {
    public static void main(String[] args) {
        System.out.println("主執行緒開始執行,當前執行緒名稱:" +
                Thread.currentThread().getName());

        Callable<Integer> callable = new MyThirdThread();
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();

        try {
            System.out.println("futureTask.isDone() return:" + futureTask.isDone());

            System.out.println(futureTask.get());

            System.out.println("futureTask.isDone() return:" + futureTask.isDone());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println("主執行緒執行結束,當前執行緒名稱:" +
                Thread.currentThread().getName());
    }
}

執行結果如下所示:

主執行緒開始執行,當前執行緒名稱:main

futureTask.isDone() return:false

-1193053528

futureTask.isDone() return:true

主執行緒執行結束,當前執行緒名稱:main

可以發現,使用Callable介面這種方式,我們可以通過futureTask.get()獲取到執行緒的執行結果,而之前的2種方式,都是沒有返回值的。

注意事項:呼叫futureTask.get()獲取執行緒的執行結果時,主執行緒會阻塞直到獲取到結果。

阻塞效果如下圖所示:

1.4 區別

以下是重點,面試常問!

  1. Java中,類僅支援單繼承,如果一個類繼承了Thread類,就無法再繼承其它類,因此,如果一個類既要繼承其它的類,又必須建立為一個執行緒,就可以使用實現Runable介面的方式。
  2. 使用實現Runable介面的方式建立的執行緒可以處理同一資源,實現資源的共享。
  3. 使用實現Callable介面的方式建立的執行緒,可以獲取到執行緒執行的返回值、是否執行完成等資訊。

關於第2點,可以通過如下示例來理解。

假如我們總共有10張票(共享的資源),為了提升售票的效率,開了3個執行緒來售賣,程式碼如下所示:

package com.zwwhnly.springbootaction.javabase.thread;

public class SaleTicketThread implements Runnable {
    private int quantity = 10;

    @Override
    public void run() {
        while (quantity > 0) {
            System.out.println(quantity-- + " is saled by " +
                    Thread.currentThread().getName());
        }
    }
}
public static void main(String[] args) {
    Runnable runnable = new SaleTicketThread();
    Thread saleTicketThread1 = new Thread(runnable);
    Thread saleTicketThread2 = new Thread(runnable);
    Thread saleTicketThread3 = new Thread(runnable);

    saleTicketThread1.start();
    saleTicketThread2.start();
    saleTicketThread3.start();
}

因為3個執行緒都是非同步執行的,因此每次的執行結果可能是不一樣,以下列舉2次不同的執行結果。

第1次執行結果:

10 is saled by Thread-0

8 is saled by Thread-0

7 is saled by Thread-0

5 is saled by Thread-0

9 is saled by Thread-1

3 is saled by Thread-1

2 is saled by Thread-1

1 is saled by Thread-1

4 is saled by Thread-0

6 is saled by Thread-2

第2次執行結果:

10 is saled by Thread-0

9 is saled by Thread-0

8 is saled by Thread-0

7 is saled by Thread-0

6 is saled by Thread-0

5 is saled by Thread-0

3 is saled by Thread-0

2 is saled by Thread-0

4 is saled by Thread-2

1 is saled by Thread-1

如果將上面的SaleTicketThread修改成繼承Thread類的方式,就變成了3個執行緒各自擁有10張票,即變成了30張票,而不是3個執行緒共享10張票。

2. Thread類start()和run()的區別

2.1 示例

因為實現Runnable介面的優勢,基本上實現多執行緒都使用的是該種方式,所以我們將之前定義的MyFirstThread也修改為實現Runnable介面的方式:

package com.zwwhnly.springbootaction.javabase.thread;

public class MyFirstThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.printf("[MyFirstThread]輸出:%d,當前執行緒名稱:%s\n",
                    i, Thread.currentThread().getName());
        }
    }
}

然後仍然沿用之前定義的MyFirstThread、MySecondThread,我們先看下呼叫start()的效果:

package com.zwwhnly.springbootaction.javabase.thread;

public class ThreadTest {
    public static void main(String[] args) {

        System.out.println("主執行緒開始執行,當前執行緒名稱:" +
                Thread.currentThread().getName());

        Thread firstThread = new Thread(new MyFirstThread());

        Runnable target = new MySecondThread();
        Thread secondThread = new Thread(target);

        firstThread.start();
        secondThread.start();

        System.out.println("主執行緒執行結束,當前執行緒名稱:" +
                Thread.currentThread().getName());
    }
}

執行結果(注意:多次執行,結果可能不一樣):

主執行緒開始執行,當前執行緒名稱:main

[MyFirstThread]輸出:0,當前執行緒名稱:Thread-0

[MyFirstThread]輸出:1,當前執行緒名稱:Thread-0

[MySecondThread]輸出:0,當前執行緒名稱:Thread-1

主執行緒執行結束,當前執行緒名稱:main

[MySecondThread]輸出:1,當前執行緒名稱:Thread-1

[MySecondThread]輸出:2,當前執行緒名稱:Thread-1

[MySecondThread]輸出:3,當前執行緒名稱:Thread-1

[MySecondThread]輸出:4,當前執行緒名稱:Thread-1

[MyFirstThread]輸出:2,當前執行緒名稱:Thread-0

[MyFirstThread]輸出:3,當前執行緒名稱:Thread-0

[MyFirstThread]輸出:4,當前執行緒名稱:Thread-0

可以看出,呼叫start()方法後,程式中有3個執行緒,分別為主執行緒main、Thread-0、Thread-1,而且執行順序不是按順序執行的,存在不確定性。

然後將start()方法修改為run()方法,如下所示:

firstThread.run();
secondThread.run();

此時的執行結果如下所示(多次執行,結果是一樣的):

主執行緒開始執行,當前執行緒名稱:main

[MyFirstThread]輸出:0,當前執行緒名稱:main

[MyFirstThread]輸出:1,當前執行緒名稱:main

[MyFirstThread]輸出:2,當前執行緒名稱:main

[MyFirstThread]輸出:3,當前執行緒名稱:main

[MyFirstThread]輸出:4,當前執行緒名稱:main

[MySecondThread]輸出:0,當前執行緒名稱:main

[MySecondThread]輸出:1,當前執行緒名稱:main

[MySecondThread]輸出:2,當前執行緒名稱:main

[MySecondThread]輸出:3,當前執行緒名稱:main

[MySecondThread]輸出:4,當前執行緒名稱:main

主執行緒執行結束,當前執行緒名稱:main

可以看出,呼叫run()方法後,程式中只有一個主執行緒,自定義的2個執行緒並沒有啟動,而且執行順序也是按順序執行的。

1.2 總結

以下是重點,面試常問!

  • run()方法只是一個普通方法,呼叫之後程式會等待run()方法執行完畢,所以是序列執行,而不是並行執行。
  • start()方法會啟動一個執行緒,當執行緒得到CPU資源後會自動執行run()方法體中的內容,實現真正的併發執行。

3. Runnable和Callable的區別

在文章前面的章節中(1.2 實現Runnable介面 和1.3 實現Callable介面),我們瞭解瞭如何使用Runnable、Callable介面來建立執行緒,現在我們分別看下Runable和Callable介面的定義,其中,Runable介面的定義如下所示:

public interface Runnable {
    public abstract void run();
}

Callable介面的定義如下所示:

public interface Callable<V> {
    V call() throws Exception;
}

由此可以看出,Runnable和Callable的區別主要有以下幾點:

  1. Runable的執行方法是run(),Callable的執行方法是call()
  2. call()方法可以丟擲異常,run()方法如果有異常只能在內部消化
  3. 實現Runnable介面的執行緒沒有返回值,實現Callable介面的執行緒能返回執行結果
  4. 實現Callable介面的執行緒,可以和FutureTask一起使用,獲取到執行緒是否完成、執行緒是否取消、執行緒執行結果,也可以取消執行緒的執行。

4. 原始碼及參考

原始碼地址:https://github.com/zwwhnly/springboot-action.git,歡迎下載。

Java中實現多執行緒的兩種方式之間的區別

Java Thread 的 run() 與 start() 的區別

Java Runnable與Callable區別

Callable,Runnable比較及用法

Runnable和Callable的區別和用法

如果覺得文章寫的不錯,歡迎關注我的微信公眾號:「申城異鄉人」,所有部落格會同步更新。

如果有興趣,也可以新增我的微信:zwwhnly_002,一起交流和探討技術。