JAVA多線程——(三)多線程編程

JAVA多線程——(三)多線程編程

【一】Future

Future應用場景

  • 在併發編程中,我們經常用到非阻塞的模型,在之前的多線程的三種實現中,不管是繼承thread類還是實現runnable接口,都無法保證獲取到之前的執行結果。通過實現Callback接口,並用Future可以來接收多線程的執行結果。
  • Future表示一個可能還沒有完成的異步任務的結果,針對這個結果可以添加Callback以便在任務執行成功或失敗後作出相應的操作。

FutureTask的類圖結構

在這裏插入圖片描述

	public class Main {
    public static Integer consumerFunction(){
        System.out.println("這是消費者:");
        return 1;
    }

    public static Integer producerFunction(){
        System.out.println("這是生產者:");
        return 2;
    }


    public static void main(String args[]) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        //生產者
        FutureTask<Integer> consumer = new FutureTask<Integer>(()->consumerFunction());
        //消費者
        FutureTask<Integer> producer = new FutureTask<Integer>(()->producerFunction());

        //提交任務
        executorService.submit(consumer);
        executorService.submit(producer);

        int resultProducer = producer.get();
        int resultConsumer = consumer.get();

        System.out.println("resultConsumer:"+resultConsumer+"  resultProducer:"+resultProducer);

    }
}

【二】CompletableFuture

CompletableFuture 是一個 Future
CompletableFuture 提供了 join() 方法,它的功能和 get() 方法是一樣的,都是阻塞獲取值,它們的區別在於 join() 拋出的是 unchecked Exception。

上面的代碼確實沒什麼用,下面介紹幾個 static 方法,它們使用任務來實例化一個 CompletableFuture 實例。

CompletableFuture.runAsync(Runnable runnable);
CompletableFuture.runAsync(Runnable runnable, Executor executor);
 
CompletableFuture.supplyAsync(Supplier<U> supplier);
CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor)
  • runAsync 方法接收的是 Runnable 的實例,意味着它沒有返回值
  • supplyAsync 方法對應的是有返回值的情況

這兩個方法的帶 executor 的變種,表示讓任務在指定的線程池中執行,不指定的話,通常任務是在 ForkJoinPool.commonPool() 線程池中執行的。

2.1 任務之間執行順序(串行):

我們先來看執行兩個任務的情況,首先執行任務 A,然後將任務 A 的結果傳遞給任務 B。

其實這裏有很多種情況:
任務 A 是否有返回值
任務 B 是否需要任務 A 的返回值
任務 B 是否有返回值,等等。

有個明確的就是,肯定是任務 A 執行完後再執行任務 B。

我們用下面的 6 行代碼來說:

CompletableFuture.runAsync(() -> {}).thenRun(() -> {}); 
CompletableFuture.runAsync(() -> {}).thenAccept(resultA -> {}); 
CompletableFuture.runAsync(() -> {}).thenApply(resultA -> "resultB");
 
 //任務 A 執行完執行 B,並且 B 不需要 A 的結果
CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {});

//任務 A 執行完執行 B,B 需要 A 的結果,但是任務 B 不返回值
CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {});

//任務 A 執行完執行 B,B 需要 A 的結果,同時任務 B 有返回值
CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB");

前面 3 句代碼:演示的是,任務 A 無返回值。第 2 行和第 3 行代碼中,resultA 其實是 null。

第 4 句:用的是 thenRun(Runnable runnable),任務 A 執行完執行 B,並且 B 不需要 A 的結果。

第 5 句:用的是 thenAccept(Consumer action),任務 A 執行完執行 B,B 需要 A 的結果,但是任務 B 不返回值。

第 6 句:用的是 thenApply(Function fn),任務 A 執行完執行 B,B 需要 A 的結果,同時任務 B 有返回值。

這一小節說完了,如果任務 B 後面還有任務 C,往下繼續調用 .thenXxx() 即可。

2.2 取兩個任務的結果(並行)

我們來看怎麼讓任務 A 和任務 B 同時執行,然後取它們的結果進行後續操作,這裏強調的是任務之間的並行工作。


如果使用 Future 的話,我們通常是這麼寫的:

ExecutorService executorService = Executors.newCachedThreadPool();
 
Future<String> futureA = executorService.submit(() -> "resultA");
Future<String> futureB = executorService.submit(() -> "resultB");
 
String resultA = futureA.get();
String resultB = futureB.get();

接下來,我們看看 CompletableFuture 中是怎麼寫的:
CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture<String> futureB = CompletableFuture.supplyAsync(() -> "resultB");

//使用兩個任務的結果 resultA 和 resultB,thenAcceptBoth 表示後續的處理不需要返回值
futureA.thenAcceptBoth(futureB, (resultA, resultB) -> {});

//使用兩個任務的結果 resultA 和 resultB,而 thenCombine 表示需要返回值。
futureA.thenCombine(futureB, (resultA, resultB) -> "result A + B");

//如果你不需要 resultA 和 resultB,runAfterBoth 方法。
futureA.runAfterBoth(futureB, () -> {});

第 4句代碼:使用兩個任務的結果 resultA 和 resultB,thenAcceptBoth 表示後續的處理不需要返回值

第 5 句代碼:使用兩個任務的結果 resultA 和 resultB,而 thenCombine 表示需要返回值。

第 6句代碼:如果你不需要 resultA 和 resultB,runAfterBoth 方法。

2.3 取多個任務的結果(並行)

我們將介紹兩個非常簡單的靜態方法:allOf() 和 anyOf() 方法:

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs){...}
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {...}
  • allOf :聚合了多個 CompletableFuture 實例,所以它是沒有返回值的
CompletableFuture futureA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture futureB = CompletableFuture.supplyAsync(() -> 123);
CompletableFuture futureC = CompletableFuture.supplyAsync(() -> "resultC");

//並行執行三個任務,但是由於是多個任務,沒有返回值
CompletableFuture<Void> future = CompletableFuture.allOf(futureA, futureB, futureC);
        
// 所以這裏的 join() 將阻塞,直到所有的任務執行結束
future.join();
  • anyOf :就是隻要有任意一個 CompletableFuture 實例執行完成就可以了
CompletableFuture futureA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture futureB = CompletableFuture.supplyAsync(() -> 123);
CompletableFuture futureC = CompletableFuture.supplyAsync(() -> "resultC");

//三個任務只要有一個有返回值了就行
CompletableFuture<Object> future = CompletableFuture.anyOf(futureA, futureB, futureC);

//最後一行的 join() 方法會返回最先完成的任務的結果,
//所以它的泛型用的是 Object,因爲每個任務可能返回的類型不同
Object result = future.join();

2.4 異常處理

我們順便來說下 CompletableFuture 的異常處理。這裏我們要介紹兩個方法:

public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn);
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);

看下面的代碼:

CompletableFuture.supplyAsync(() -> "resultA")
    .thenApply(resultA -> resultA + " resultB")
    .thenApply(resultB -> resultB + " resultC")
    .thenApply(resultC -> resultC + " resultD");

上面的代碼中,任務 A、B、C、D 依次執行,如果任務 A 拋出異常(當然上面的代碼不會拋出異常),那麼後面的任務都得不到執行

方式一:
我們在任務 A 中拋出異常,並對其進行處理

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException();
        })
                .exceptionally(ex -> "errorResultA")
                .thenApply(resultA -> resultA + " resultB")
                .thenApply(resultB -> resultB + " resultC")
                .thenApply(resultC -> resultC + " resultD");

        System.out.println(future.join());

方式二:
使用 handle(BiFunction fn) 來處理異常

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "resultA")
        .thenApply(resultA -> resultA + " resultB")
        // 任務 C 拋出異常
        .thenApply(resultB -> {throw new RuntimeException();})
        // 處理任務 C 的返回值或異常
        .handle(new BiFunction<Object, Throwable, Object>() {
            @Override
            public Object apply(Object re, Throwable throwable) {
                if (throwable != null) {
                    return "errorResultC";
                }
                return re;
            }
        })
        .thenApply(resultC -> resultC + " resultD");
 
System.out.println(future.join());

上面的代碼使用了 handle 方法來處理任務 C 的執行結果,上面的代碼中,re 和 throwable 必然有一個是 null,它們分別代表正常的執行結果和異常的情況。

當然,它們也可以都爲 null,因爲如果它作用的那個 CompletableFuture 實例沒有返回值的時候,re 就是 null。

【三】Fock_Join

fork/join框架是ExecutorService接口的一個實現,可以幫助開發人員充分利用多核處理器的優勢,編寫出並行執行的程序,提高應用程序的性能;設計的目的是爲了處理那些可以被遞歸拆分的任務。

fork/join框架與其它ExecutorService的實現類相似,會給線程池中的線程分發任務,不同之處在於它使用了工作竊取算法,所謂工作竊取,指的是對那些處理完自身任務的線程,會從其它線程竊取任務執行。

Fork/Join框架是Java 7提供的一個用於並行執行任務的框架,是一個把大任務分割成若干個小任務,最終彙總每個小任務結果後得到大任務結果的框架。簡單來說,Fork就是把一個大任務切分爲若干子任務並行的執行,Join就是合併這些子任務的執行結果,最後得到這個大任務的結果。比如計算1+2+…+10000,可以分割成10個子任務,每個子任務分別對1000個數進行求和,最終彙總這10個子任務的結果。

3.1 Fork/Join框架的工作原理

從上面Fork/Join框架的介紹中瞭解到,Fork/Join框架主要分爲幾個步驟實現。

1、分割任務。首先我們需要有一個fork類來把大任務分割成子任務,有可能子任務還是很大,所以還需要不停地分割,直到分割出的子任務足夠小。

2、執行任務併合並結果。分割的子任務分別放在雙端隊列裏,然後幾個啓動線程分別從雙端隊列裏獲取任務執行。子任務執行完的結果都統一放在一個隊列裏,啓動一個線程從隊列裏拿數據,然後合併這些數據。

  • RecursiveAction:用於沒有返回結果的任務,即執行任務後不會返回結果;
  • RecursiveTask:用於有返回結果的任務,即執行任務後會返回結果。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class CountTask extends RecursiveTask<Integer> {
    // 閾值,表示拆分爲幾個子任務
	private static final int THRESHOLD = 2;
	private int start;
	private int end;
	public CountTask(int start, int end) {
		this.start = start;
		this.end = end;
	}
	@Override
	protected Integer compute() {
		int sum = 0;
		// 如果任務小於等於閾值則計算任務
		Boolean canCompute = (end - start) <= THRESHOLD;
		if (canCompute) {
			for (int i = start; i <= end; i++) {
				sum += i;
			}
		} else {
			// 如果任務大於閾值,就分裂成兩個子任務計算
			int middle = (start + end) / 2;
			CountTask leftTask = new CountTask(start, middle);
			CountTask rightTask = new CountTask(middle + 1, end);
			// 執行子任務
			leftTask.fork();
			rightTask.fork();
			// 等待子任務執行完,並得到其結果
			int leftResult=leftTask.join();
			int rightResult=rightTask.join();
			// 合併子任務
			sum = leftResult + rightResult;
		}
		r
		eturn sum;
	}
	public static void main(String[] args) {
		ForkJoinPool forkJoinPool = new ForkJoinPool();
		// 生成一個計算任務,負責計算1+2+3+4
		CountTask task = new CountTask(1, 4);
		// 執行一個任務
		Future<Integer> result = forkJoinPool.submit(task);
		try {
			System.out.println(result.get());
		}
		catch (InterruptedException e) {
		}
		catch (ExecutionException e) {
		}
	}
}

【四】鏈接

https://blog.csdn.net/striveb/article/details/84140407
https://blog.csdn.net/w605283073/article/details/92418504

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章