@程式設計師,如何花式構建執行緒?

CSDN資訊發表於2020-02-07

@程式設計師,如何花式構建執行緒?

@程式設計師,如何花式構建執行緒?

作者 | 曾建

責編 | 郭芮

出品 | CSDN(ID:CSDNnews)

在專案和業務的開發中,我們難免要經常使用執行緒來進行業務處理,使用執行緒可以保證我們的業務在相互處理之間可以保證原子性,減少相互之間的耦合和影響。通常情況下,我們會使用建立一個繼承Thread的物件或實現Runnable介面的類來建立執行緒,我們很少會注意如何建立執行緒更簡潔,更方便,更能提高開發效率,其實建立執行緒的方式有很多種,下面就來感受一下建立執行緒這個操作所擁有的魅力。

Java中建立執行緒主要有三種方式,我們先來看下第一種方式,通過繼承Thread類建立執行緒類。該方式建立執行緒有3個步驟:

  • 定義Thread類的子類,並重寫該類的run方法,該run方法的方法體就代表了執行緒要完成的任務。因此把run()方法稱為執行體。

  • 建立Thread子類的例項,即建立執行緒物件。

  • 呼叫執行緒物件的start()方法來啟動該執行緒。

package com.test.thread;
public class ThreadTest extends Thread{
    /**
     * 需要手動重寫run方法,run方法內為執行緒執行體
     */
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+" "+i);
        }
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        for (int i = 0; i < 100; i++) {
            if(i==20){
                new ThreadTest().start();
                new ThreadTest().start();
            }
        }
    }
}

這種方式建立執行緒也是最基本和最常用的方式,上述程式碼中Thread.currentThread()方法返回當前正在執行的執行緒物件,getName()方法返回撥用該方法的執行緒的名字。

在實際應用過程中,我們經常會使用匿名內部類的方式在方法或程式碼塊中建立執行緒,其示例如下:

public class ThreadTest2 {  
  public static void main(String[] args) {  
            new Thread() {  
                public void run() {  
                    while (true) {  
                        try {  
                            System.out.println("執行緒輸出");  
                            //休眠兩秒  
                            Thread.sleep(2 * 1000);  
                        } catch (InterruptedException e) {  
                            e.printStackTrace();  
                        }  
                    }  
                };  
            }.start();  
        }  
    } 

這種方式使用了匿名內部類的方式建立執行緒,在程式碼塊中就可以直接建立執行緒,從而進行業務處理。這種方式也減少了建立執行緒類的步驟,直接就可以使用,提高了建立執行緒的靈活性。

第二種建立執行緒的主要方式為通過實現Runnable介面建立執行緒類,該方式建立執行緒主要有以下3個步驟:

  • 定義runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。

  • 建立Runnable實現類的例項,並依此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。

  • 呼叫執行緒物件的start()方法來啟動該執行緒。

package com.test.thread;
public class RunnableThreadTest implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        for (int i = 0; i < 100; i++) {
            if(i==20){
                RunnableThreadTest test = new RunnableThreadTest();
                Thread thread1 = new Thread(test);
                Thread thread2 = new Thread(test);
                thread1.start();
                thread2.start();
            }
        }
    }
}

通過實現Runnable介面建立執行緒類時,我們需要將該執行緒類的物件作為Thread類的傳參,然後才可以建立物件,因為Thread類的底層封裝了一個Runnable介面為引數的建構函式,然後呼叫Thread類的start()方法開始執行執行緒內的方法體,Runnable介面為引數的建構函式其程式碼如下,可以感受一番:

  public Thread(Runnable target) {
          // 該方法為Thread類中初始化載入執行緒的方法
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

第三種建立執行緒的方式主要是通過Callable和Future建立執行緒。

從繼承Thread類和實現Runnable介面可以看出,上述兩種方法都不能有返回值,且不能宣告丟擲異常。而Callable介面則實現了此兩點,Callable介面如同Runnable介面的升級版,其提供的call()方法將作為執行緒的執行體,同時允許有返回值。

但是Callable物件不能直接作為Thread物件的target,不是Runnable介面的子介面。對於這個問題的解決方案,就引入了Future介面,此介面可以接受call()的返回值,RubbaleFuture介面是Future介面和Runnable介面的子介面,可以作為Thread物件的target。並且,Future介面提供了一個實現類:FutureTask。FutureTask實現了RunnableFuture介面,可以作為Thread物件的target。

通過Callable和Future建立執行緒主要有以下四個步驟:

  • 建立Callable介面的實現類,並實現call()方法,該call()方法將作為執行緒執行體,並且有返回值。

  • 建立Callable實現類的例項,使用FutureTask類來包裝Callable物件,該FutureTask物件封裝了該Callable物件的call()方法的返回值。

  • 使用FutureTask物件作為Thread物件的target建立並啟動新執行緒。

  • 呼叫FutureTask物件的get()方法來獲得子執行緒執行結束後的返回值。

package com.test.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableThreadTest implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        int i=0;
        for (; i < 10; i++) {
            System.out.println(Thread.currentThread().getName());
        }
        return i;
    }

    public static void main(String[] args) {
        CallableThreadTest thredClass = new CallableThreadTest();
        FutureTask<Integer> future = new FutureTask<>(thredClass);
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName());
            if(i==2){
                new Thread(future).start();
            }
        }
        try {
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    }

上述示例是通過實現Callable介面的普通類來建立執行緒,通過上述示例我們不僅可以進行異常丟擲,還可以手動獲取執行緒的返回值。在實際的應用過程中,我們會經常通過匿名內部類在方法或程式碼塊中建立執行緒,示例如下:

package com.test;
import java.util.concurrent.FutureTask;
import java.util.concurrent.Callable;

public class FutureThread {
    public static void main(String[] args) {
        FutureTask<Integer> future = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println(Thread.currentThread().getName());
                return 2+3;
            }
        });
        Thread futurerThread = new Thread(future);
        futurerThread.start();      
    }   
}

當我們通過FutureTask類來建立例項物件時,我們會發現FutureTask的泛型引數是一個必填引數,我們可以開啟FutureTask的底層會發現,FutureTask類有兩個建構函式,其底層構造程式碼如下:

  /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

第一個構造是通過傳入一個Callable的物件建立執行緒,Callable物件會自動執行call()方法,第二個構造是通過傳入一個實現Runnable的物件建立執行緒,後面有一個result引數,其用來返回執行緒執行的成功失敗的狀態。所以我們可以通過以上兩種構造方式來建立FutureTask物件,然後將其作為Thread物件的target建立並啟動新執行緒。

當我們瞭解Java8的時候,你會發現上面建立執行緒的方式其實是很複雜的。Java8提供了函式式介面程式設計,函式式介面程式設計極簡化了執行緒的建立方式,增強了程式碼的可讀性。什麼是函式式介面程式設計呢?jdk8引入的lambda表示式和Stream為Java平臺提供了函數語言程式設計的支援,極大的提高了開發效率。

函數語言程式設計針對為函式式介面而言,函式式介面是有且只有一個抽象方法的介面。Lambda表示式允許你直接以內聯的形式為函式式介面的抽象方法提供實現,並把整個表示式作為函式式介面的例項。當我們把一個Lambda表示式賦給一個函式式介面時,這個表示式對應的必定是介面中唯一的抽象方法。因此就不需要以匿名類那麼繁瑣的形式去實現這個介面。可以說在語法簡化上,Lambda表示式完成了方法層面的簡化,函式式介面完成了類層面的簡化。

函數語言程式設計介面都只有一個抽象方法,編譯器會將函式編譯後當做該抽象方法的實現。如果介面有多個抽象方法,編譯器就不知道這段函式應該是實現哪個方法了。例如:

以Callable介面作為示例,它是一個函式式介面,包含一個抽象方法call()。現在要定義一個Callable物件,傳統方式是這樣的:

    Callable c = new Callable() {
        @Override
        public Object call() throws Exception {
            int resultCode = 200;
            return resultCode;
        }
    };

而使用函數語言程式設計,可以這樣定義:

    Consumer c = (o) -> {
        System.out.println(o);
    }; 

通過了解函數語言程式設計介面之後我們發現通過函式式介面可以極大簡化程式碼和開發效率。當我們在建立執行緒的時候也可以使用函數語言程式設計介面的方法來建立執行緒。

示例如下:

package com.test.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
 * 
 * 〈java8 流式建立執行緒〉<br> 
 * 〈功能詳細描述〉
 *
 * @author zengjian
 */
public class ThreadStreamTest {
    public static void main(String[] args) {
         try {
              FutureTask<Integer> task = new FutureTask<Integer>(()->threadMethod());
              Thread taskThread = new Thread(task);
              taskThread.start();
              Integer result = task.get();
              System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    public static int threadMethod(){
        return 2+4;
    }
}

通過示例我們會發現,通過函數語言程式設計的方式,極大的簡化了建立執行緒的程式碼,可以有效的提高程式碼的可讀性和維護性,所以在開發的過程中可以經常使用這種方式進行建立執行緒,提高我們的開發效率。

FutureTask是為了彌補Thread的不足而設計的,他可以讓程式設計師準確地知道執行緒什麼時候執行完成並獲得到執行緒執行完成後返回的結果。FutureTask是一種可以取消的非同步的計算任務。它的計算是通過Callable實現的,它等價於可以攜帶結果的Runnable,並且有三個狀態:等待、執行和完成。完成包括所有計算以任意的方式結束,包括正常結束、取消和異常。Executor框架利用FutureTask來完成非同步任務,並可以用來進行任何潛在的耗時的計算。一般FutureTask多用於耗時的計算,主執行緒可以在完成自己的任務後,再去獲取結果。在多執行緒高併發的場景下用途比較廣泛,可以處理多工非同步處理等。

執行緒知識豐富且複雜,通了解建立執行緒的這一操作,可以幫助我們理解執行緒相關的知識和細節,也可以幫忙我們理解在應用過程中多執行緒和高併發相關的知識。通過這篇文章,希望對你有所幫助哦。

作者:曾建,目前就職於蘇寧易購,專注於CDN相關係統開發。

@程式設計師,如何花式構建執行緒?

你點的每個“在看”,我都認真當成了喜歡

相關文章