1. 程式人生 > >Java併發03:多執行緒實現三方式:繼承Thread類、實現Runnable介面、實現Callable介面

Java併發03:多執行緒實現三方式:繼承Thread類、實現Runnable介面、實現Callable介面

本章主要對Java多執行緒實現的三種方式進行學習。

1.序言

JDK5版本之前,提供了兩種多執行緒的實現方式:

  • 繼承Thread類,重寫run()方法
  • 實現Runnable介面,實現run()方法

兩種種方式本質都是一個:實現Runnable介面

JDK5版本時,提供了一種新的多執行緒實現方式:

  • Future介面+Callable介面+Executor介面

下面分別對這三種實現方式進行學習。

2.實現Runnable介面

2.1.Runnable介面定義

我們先來看以下Runnable介面的定義:

package java.lang;

/**
 * The <code>Runnable</code> interface should be implemented by any
 * class whose instances are intended to be executed by a thread. The
 * class must define a method of no arguments called <code>run</code>.
 * <p>
 * This interface is designed to provide a common protocol for objects that
 * wish to execute code while they are active. For example,
 * <code>Runnable</code> is implemented by class <code>Thread</code>.
 * Being active simply means that a thread has been started and has not
 * yet been stopped.
 * <p>
 * ...
 */
@FunctionalInterface public interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> */
public abstract void run(); }

總結:

  • 如果希望一個類的例項被當做一個執行緒執行,那麼這個類必須實現Runnable介面
  • Runnable介面被設計成執行緒設計的一種公共協議。
  • Thread類就是一種Runnable介面的實現
  • 當一個實現了Runnable介面的執行緒類開始執行,就會自動呼叫run()方法。
  • 實現Runnable介面,必須重寫run()方法。
  • 建議在run()方法中存放所有的業務程式碼,做到執行緒控制與業務流程的分離。
  • run()方法返回型別為Void,引數為空
  • run()方法不能丟擲異常

2.2.通過實現Runnable介面實現執行緒

實現Runnable實現執行緒的語法:

//定義
public class MyRunnableImpl implements Runnable {
    @Override
    public void run(){
        //..
    }
}

//使用
new Thread(new MyRunnableImpl()).start();

說明:

  • Runnable介面中只定義了run()方法,其他屬性和方法,如name等,需要自己去定義。
  • Runnable實現類本身並不能啟動,需要Thread()類的協助。

例項場景:

建立5個執行緒,每個執行緒隨機執行一段時間後結束。

例項程式碼:

/**
 * <p>自定義執行緒02:實現Runnable介面</p>
 * @author hanchao 2018/3/8 23:04
 **/
public class MyRunnableImpl implements Runnable{
    private static final Logger LOGGER = Logger.getLogger(MyRunnableImpl.class);

    /** 執行緒名(需要手動指定) */
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public MyRunnableImpl(String name) {
        this.name = name;
    }

    /**
     * <p>義務程式碼在run()方法中,此方法無返回值</p>
     * @author hanchao 2018/3/8 23:07
     **/
    @Override
    public void run() {
        Integer interval = RandomUtils.nextInt(1000,5000);
        LOGGER.info("執行緒[" + this.getName() + "]正在執行,預計執行" + interval + "...");
        try {
            Thread.sleep(interval);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            LOGGER.info("...執行緒[" + this.getName() + "]執行結束");
        }
    }

    /**
     * <p>自定義執行緒實現類測試</p>
     * @author hanchao 2018/3/8 23:07
     **/
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            //通過new建立一個執行緒
            Runnable runnable = new MyRunnableImpl("MyRunnableImpl-" + i);
            //通過new Thread.start()啟動執行緒
            new Thread(runnable).start();
        }
    }
}

執行結果:

2018-03-12 10:11:19 INFO  MyRunnableImpl:40 - 執行緒[MyRunnableImpl-0]正在執行,預計執行1442...
2018-03-12 10:11:19 INFO  MyRunnableImpl:40 - 執行緒[MyRunnableImpl-1]正在執行,預計執行1250...
2018-03-12 10:11:19 INFO  MyRunnableImpl:40 - 執行緒[MyRunnableImpl-2]正在執行,預計執行3603...
2018-03-12 10:11:19 INFO  MyRunnableImpl:40 - 執行緒[MyRunnableImpl-3]正在執行,預計執行3290...
2018-03-12 10:11:19 INFO  MyRunnableImpl:40 - 執行緒[MyRunnableImpl-4]正在執行,預計執行3758...
2018-03-12 10:11:20 INFO  MyRunnableImpl:46 - ...執行緒[MyRunnableImpl-1]執行結束
2018-03-12 10:11:21 INFO  MyRunnableImpl:46 - ...執行緒[MyRunnableImpl-0]執行結束
2018-03-12 10:11:22 INFO  MyRunnableImpl:46 - ...執行緒[MyRunnableImpl-3]執行結束
2018-03-12 10:11:23 INFO  MyRunnableImpl:46 - ...執行緒[MyRunnableImpl-2]執行結束
2018-03-12 10:11:23 INFO  MyRunnableImpl:46 - ...執行緒[MyRunnableImpl-4]執行結束

3.繼承Thread類

3.1.Thread類定義

先來看Thread類的定義:

/**
 * A <i>thread</i> is a thread of execution in a program. The Java
 * Virtual Machine allows an application to have multiple threads of
 * execution running concurrently.
 * <p>
 * ...
 */
public
class Thread implements Runnable {
     //...
        /**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * ...
     */
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

說明:

  • Thread執行緒類實際上是實現的Runnable介面。

3.2.通過繼承Thread類實現執行緒

語法:

//定義
public class MyThread extends Thread {
    @Override
    public void run(){
        //..
    }
}

//使用
new MyThread().start();

說明:

  • 相較於Runnable類,Thread類提供了一系列方法(後續章節中會學習),都可以在自定義執行緒中通過super來呼叫。
  • 雖然Thread類提供了一些方法,簡化了執行緒開發。但是通過繼承的方式實現執行緒,會增加程式的耦合性,不利於維護。

例項場景:

建立5個執行緒,每個執行緒隨機執行一段時間後結束。

例項程式碼:

/**
 * <p>自定義執行緒01:繼承自Thread</p>
 * @author hanchao 2018/3/8 22:54
 **/
public class MyThread extends Thread {
    private static final Logger LOGGER = Logger.getLogger(MyThread.class);

    /**
     * <p>重寫Thread類的構造器,用以給執行緒命名</br>
     * 此種方式無需定義name變數以指定執行緒名,因為父類Thread中已有。</p>
     * @author hanchao 2018/3/8 22:59
     **/
    public MyThread(String name) {
        super(name);
    }

    /**
     * <p>業務程式碼寫在run()方法中,此方法無返回值</p>
     * @author hanchao 2018/3/8 22:55
     **/
    @Override
    public void run(){
        //run()方法無法丟擲異常
//    public void run() throws Exception{
        Integer interval = RandomUtils.nextInt(1000,9000);
        LOGGER.info("執行緒[" + super.getName() + "]正在執行,預計執行" + interval + "...");
        try {
            Thread.sleep(interval);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            LOGGER.info("...執行緒[" + super.getName() + "]執行結束");
        }
    }

    /**
     * <p>測試自定義執行緒</p>
     * @author hanchao 2018/3/8 22:57
     **/
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            //通過new建立一個執行緒
            Thread thread = new MyThread("MyThread-" + i);
            //通過start()啟動執行緒
            thread.start();
        }
    }
}

執行結果:

2018-03-12 10:18:35 INFO  MyThread:31 - 執行緒[MyThread-3]正在執行,預計執行6561...
2018-03-12 10:18:35 INFO  MyThread:31 - 執行緒[MyThread-4]正在執行,預計執行7464...
2018-03-12 10:18:35 INFO  MyThread:31 - 執行緒[MyThread-1]正在執行,預計執行4806...
2018-03-12 10:18:35 INFO  MyThread:31 - 執行緒[MyThread-2]正在執行,預計執行5214...
2018-03-12 10:18:35 INFO  MyThread:31 - 執行緒[MyThread-0]正在執行,預計執行8557...
2018-03-12 10:18:40 INFO  MyThread:37 - ...執行緒[MyThread-1]執行結束
2018-03-12 10:18:40 INFO  MyThread:37 - ...執行緒[MyThread-2]執行結束
2018-03-12 10:18:42 INFO  MyThread:37 - ...執行緒[MyThread-3]執行結束
2018-03-12 10:18:43 INFO  MyThread:37 - ...執行緒[MyThread-4]執行結束
2018-03-12 10:18:44 INFO  MyThread:37 - ...執行緒[MyThread-0]執行結束

4.實現Callable介面

4.1.JDK5之前執行緒實現的弊端

先來分析之前兩種實現方式的弊端。
通過分析Runnable介面的定義,很容易總結出來:

  • 沒有返回值:如果想要獲取某個執行結果,需要通過共享變數等方式,需要做更多的處理。
  • 無法丟擲異常:不能宣告式的丟擲異常,增加了某些情況下的程式開發複雜度。
  • 無法手動取消執行緒:只能等待執行緒執行完畢或達到某種結束條件,無法直接取消執行緒任務。

為了解決以上的問題,在JDK5版本的java.util.concurretn包中,引入了新的執行緒實現機制:Callable介面

4.2.Callable介面定義

我們先來看一下Callable介面的語法:

package java.util.concurrent;

/**
 * A task that returns a result and may throw an exception.
 * Implementors define a single method with no arguments called
 * {@code call}.
 *
 * <p>The {@code Callable} interface is similar to {@link
 * java.lang.Runnable}, in that both are designed for classes whose
 * instances are potentially executed by another thread.  A
 * {@code Runnable}, however, does not return a result and cannot
 * throw a checked exception.
 * ...
 */
@FunctionalInterface
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介面實現的任務必須返回一個結果並且丟擲一個異常
  • Callable介面的實現類需要重寫一個無參的方法Call()
  • Callable介面Runnable介面類似,都是為了實現多執行緒而設計的。
  • Runnable介面沒有返回值,也無法丟擲異常。
  • Callable介面是一個泛型介面。

4.3.通過實現Callable介面實現執行緒

實現Callable介面語法:

//定義
public class MyCallableImpl implements Callable<T> {
    @Override
    public T call() throws Exception {
        //...
    }
}

//使用
//一般配置Executor使用,Executor提供執行緒池服務
ExecutorService executor = new ....
//一般配置Future介面使用,Future用於儲存返回結果
//向執行緒池提交一個任務,並把此任務的執行情況儲存在future中
Futrue future = executor.submit(new MyCallableImple());
//獲取返回結果
future.get();
//關閉執行緒池和任務
executor.shutdwon();

說明:

  • Future、Callable一般與Executor結合使用。
  • Callable介面用於定義任務類,並在Call()方法中定義業務程式碼。
  • Executor介面負責執行緒池的管理(計劃在後續章節進行學習)。
  • Future介面負責保持在Executor執行緒池中執行的Callable任務的執行狀態。
  • Callable介面實現類,通過executor.submit()向執行緒池提交任務。
  • Future介面通過get()方法獲取執行結果(一直等待知道結果產生)。
  • 一定要記得通過executor.shutdwon()關閉執行緒池。推薦在finally中進行這個操作。

例項場景:

定義一個最大執行緒數為5的執行緒池,執行5個任務,獲取並打印出每個執行緒的執行時間。

例項程式碼:

/**
 * <p>自定義執行緒03:實現Callable介面</p>
 *
 * @author hanchao 2018/3/12 8:56
 **/
//注意,Callable是一個泛型介面
public class MyCallableImpl implements Callable<Integer> {
    private static final Logger LOGGER = Logger.getLogger(MyCallableImpl.class);

    /**
     * <p>實現Callable需要重寫call方法,此方法有返回值</p>
     *
     * @author hanchao 2018/3/12 8:59
     **/
    @Override
    public Integer call() throws Exception {
        Integer interval = RandomUtils.nextInt(1000, 5000);
        Thread.sleep(interval);
        return interval;
    }

    /**
     * <p>實現Callable示例</p>
     *
     * @author hanchao 2018/3/12 9:00
     **/
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        //Future、Callable一般與Executor結合使用
        //Executor負責建立執行緒池服務
        //實現Callable介面形成的執行緒類,負責處理業務邏輯,並將處理結果返回
        //Future介面負責接收Callable介面返回的值
        ExecutorService executor = Executors.newFixedThreadPool(5);
        try {
            //定義一組返回值
            Future<Integer>[] futures = new Future[5];
            //向執行緒池提交任務
            for (int i = 0; i < 5; i++) {
                //注意Future的引數化型別要與Callable的引數化型別一致
                futures[i] = executor.submit(new MyCallableImpl());
            }
            //輸出執行結果
            for (int i = 0; i < 5; i++) {
                LOGGER.info(futures[i].get());
            }
        }finally {//將關閉執行緒池放在finally中,最大限度保證執行緒安全
            //記得關閉這個執行緒池
            executor.shutdown();
        }
    }
}

執行結果:

2018-03-12 10:44:28 INFO  MyCallableImpl:50 - 1564
2018-03-12 10:44:30 INFO  MyCallableImpl:50 - 2992
2018-03-12 10:44:30 INFO  MyCallableImpl:50 - 1629
2018-03-12 10:44:30 INFO  MyCallableImpl:50 - 1454
2018-03-12 10:44:30 INFO  MyCallableImpl:50 - 1941

4.4.更多的Future方法

前面只展示了Future類的get()方法。
為了更詳細的體現[實現Callable介面]這種方式的優點,對Future介面的其他方法進行簡單學習。

Future介面的主要方法如下:

  • isDone():判斷任務是否完成。
  • isCancelled():判斷任務是否取消。
  • get():獲取計算結果(一致等待,直至得到結果)。
  • cancel(true):取消任務。
  • get(long,TimeUnit):規定時間內獲取計算結果(在long時間內等待結果,如果得到則返回;如果未得到,則結束,並丟擲TimeoutException異常)。

說明:

  • get(long,TimeUnit)的第一個引數是最大超時時間,第二個是時間單位,可以通過enum TimeUnit獲取。

下面通過兩段程式碼進行例項學習。

程式碼段1:學習了isDone、isCancelled、get()方法

System.out.println();
//Future介面方法簡單展示: isDone/isCancelled/get()
//建立單執行緒池
ExecutorService executor1 = Executors.newSingleThreadExecutor();
//向執行緒池提交任務
Future<Integer> future = executor1.submit(new MyCallableImpl());
try {
    //計算執行時間
    Long begin = System.currentTimeMillis();
    LOGGER.info("future開始執行任務...當前時間:" + begin);
    LOGGER.info("通過future.isDone()判斷任務是否計算完成:" + future.isDone());
    LOGGER.info("通過future.isCancelled()判斷任務是否取消:" + future.isCancelled());
    LOGGER.info("通過future.get()獲取任務的計算結果(從任務開始就一直等待,直至有返回值):" + future.get());
    LOGGER.info("future結束執行任務...共計用時:" + (System.currentTimeMillis() - begin) + "ms..\n");
}finally {//將關閉執行緒池放在finally中,最大限度保證執行緒安全
    LOGGER.info("通過future.isDone()判斷任務是否計算完成:" + future.isDone());
    LOGGER.info("通過future.isCancelled()判斷任務是否取消:" + future.isCancelled());
    //記得關閉這個執行緒池
    executor1.shutdown();
}

程式碼段1執行結果:

2018-03-12 11:02:28 INFO  MyCallableImpl:66 - future開始執行任務...當前時間:1520823748695
2018-03-12 11:02:28 INFO  MyCallableImpl:67 - 通過future.isDone()判斷任務是否計算完成:false
2018-03-12 11:02:28 INFO  MyCallableImpl:68 - 通過future.isCancelled()判斷任務是否取消:false
2018-03-12 11:02:31 INFO  MyCallableImpl:69 - 通過future.get()獲取任務的計算結果(從任務開始就一直等待,直至有返回值):2830
2018-03-12 11:02:31 INFO  MyCallableImpl:70 - future結束執行任務...共計用時:2843ms..

2018-03-12 11:02:31 INFO  MyCallableImpl:72 - 通過future.isDone()判斷任務是否計算完成:true
2018-03-12 11:02:31 INFO  MyCallableImpl:73 - 通過future.isCancelled()判斷任務是否取消:false

程式碼段2:學習了isDone、isCancelled、cancel()、get(long,TimeUnit)方法

        //get(long,TimeUnit):最多等待多長時間就不再等待
        //建立單執行緒池
        ExecutorService executor2 = Executors.newSingleThreadExecutor();
        //向執行緒池提交任務
        Future<Integer> future2 = executor2.submit(new MyCallableImpl());
        Long begin2 = System.currentTimeMillis();
        try {
            LOGGER.info("future開始執行任務...當前時間:" + begin2);
            LOGGER.info("通過future.get(long,TimeUnit)獲取任務的計算結果(5秒鐘之後再獲取結果):" + future2.get(500,TimeUnit.MILLISECONDS));
            LOGGER.info("future結束執行任務...共計用時:" + (System.currentTimeMillis() - begin2) + "ms..\n");
        }catch (TimeoutException e){
            LOGGER.info("在限定時間內沒有等到查詢結果,不再等待..");
            //關閉任務
            LOGGER.info("當前任務狀態:future2.isDone() = " + future2.isDone());
            LOGGER.info("當前任務狀態:future2.isCancelled() = " + future2.isCancelled());
            LOGGER.info("通過future.cancel()取消這個任務:");
            future2.cancel(true);
            LOGGER.info("當前任務狀態:future2.isDone() = " + future2.isDone());
            LOGGER.info("當前任務狀態:future2.isCancelled() = " + future2.isCancelled());

            //關閉執行緒池
            executor2.shutdown();

            //意料之中的結果,無需列印日誌
            //e.printStackTrace();
        }

程式碼段2執行結果:

2018-03-12 11:03:15 INFO  MyCallableImpl:85 - future開始執行任務...當前時間:1520823795611
2018-03-12 11:03:16 INFO  MyCallableImpl:89 - 在限定時間內沒有等到查詢結果,不再等待..
2018-03-12 11:03:16 INFO  MyCallableImpl:91 - 當前任務狀態:future2.isDone() = false
2018-03-12 11:03:16 INFO  MyCallableImpl:92 - 當前任務狀態:future2.isCancelled() = false
2018-03-12 11:03:16 INFO  MyCallableImpl:93 - 通過future.cancel()取消這個任務:
2018-03-12 11:03:16 INFO  MyCallableImpl:95 - 當前任務狀態:future2.isDone() = true
2018-03-12 11:03:16 INFO  MyCallableImpl:96 - 當前任務狀態:future2.isCancelled() = true