@程式設計師,如何花式構建線程?

csdn 發佈 2020-01-22T08:39:57+00:00

packagecom.test.thread;public class ThreadTest extends Thread{/*** 需要手動重寫run方法,run方法內為線程執行體*/public void run {for {System.out.println;}}pub

作者 | 曾建

責編 | 郭芮

在項目和業務的開發中,我們難免要經常使用線程來進行業務處理,使用線程可以保證我們的業務在相互處理之間可以保證原子性,減少相互之間的耦合和影響。通常情況下,我們會使用創建一個繼承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(, 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 PointerException if the callable is
*/
public FutureTask(Callable<V> callable) {
if (callable == )
throw new PointerException;
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, )}
* @throws PointerException if the runnable is
*/
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相關係統開發。

關鍵字: