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

1. 建立線程的三種方法及其區別

1.1 繼承Thread類

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

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()方法啓動線程:java

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());
    }
}

運行結果以下所示:git

主線程開始執行,當前線程名稱:maingithub

主線程執行結束,當前線程名稱:main面試

[MyFirstThread]輸出:0,當前線程名稱:Thread-0spring

[MyFirstThread]輸出:1,當前線程名稱:Thread-0springboot

[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,一塊兒交流和探討技術。

相關文章
相關標籤/搜索