【异步】Futurn、FutureTask、CompletionService、CompletableFuture

1. 呼び出し可能

この記事【スレッド】スレッドの基本概念と作成方法(1)では、スレッドを作成するいくつかの方法がわかりました。そのうち 2 つはインターフェイスを通じて実装されます: RunnableCallableそれらの違いは次のとおりです。

  • Runnableインターフェイス内のメソッドには戻り値がありませんが、Callableインターフェイス内のメソッドには戻り値があります。
  • Callableインターフェース内のメソッドが例外をスローする可能性がある

Callableインターフェースの使い方

Callableインターフェースを使用するには 2 つの方法があります。

  1. パスしThread、再度結合しFutureFutureTask
  2. スレッドプールのsubmit()invokeAll()invokeAny()メソッドを介して

方法 1:

public static void main(String[] args) throws Exception {
    
    
    Callable<String> task = () -> "I am a callable Task";
    // 将 Callable 封装为 FutureTask
    FutureTask<String> futureTask = new FutureTask<>(task);
    new Thread(futureTask).start();
    String result = futureTask.get();
}

注:get()このメソッドはブロックメソッドです。結果が得られるまでブロックされます。

方法 2:

public static void main(String[] args) throws Exception {
    
    
    Callable<String> task = () -> "I am a callable Task";
    ExecutorService threadPool = Executors.newFixedThreadPool(2);
    Future<String> future = threadPool.submit(task);
    String result = future.get();
    threadPool.shutdown();
}

2. 未来、将来の課題

まず、次のような両者の関係図を見てください。

ここに画像の説明を挿入します

Futureインターフェース 将来は、特定の Runnable または Callable タスクの実行結果をキャンセルし、完了したかどうかをクエリし、結果を取得することです。必要に応じて、get()タスクが結果を返すまでブロックされるメソッドを通じて実行結果を取得できます。

public interface Future<V> {
    
    
    // 取消任务,如果任务正在运行的,mayInterruptIfRunning为true时,表明这个任务会被打断的,并返回true;
    // 为false时,会等待这个任务执行完,返回true;若任务还没执行,取消任务后返回true,如任务执行完,返回false
    boolean cancel(boolean mayInterruptIfRunning);
    // 判断任务是否被取消了,正常执行完不算被取消
    boolean isCancelled();
    // 判断任务是否已经执行完成,任务取消或发生异常也算是完成,返回true
    boolean isDone();
    // 获取任务返回结果,如果任务没有执行完成则等待完成将结果返回,如果获取的过程中发生异常就抛出异常,
    // 比如中断就会抛出InterruptedException异常等异常
    V get() throws InterruptedException, ExecutionException;
    // 在规定的时间如果没有返回结果就会抛出TimeoutException异常
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTaskInterface :インターフェイスFutureTaskを実装しRunnableFuture、このインターフェイスはRunnableインターフェイス、Futureインターフェイスを継承します。したがって、インターフェイス内のrun()メソッドとメソッドは必然的に再作成されます。Future

public class FutureTask<V> implements RunnableFuture<V> {
    
    

	private Callable<V> callable;
	// 存储任务结果
	private Object outcome;
	
	public void run() {
    
    
        if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
            return;
        try {
    
    
            Callable<V> c = callable;
            if (c != null && state == NEW) {
    
    
                V result = c.call();
                set(result);
            }
        } finally {
    
    
            //...
        }
    }
    
    protected void set(V v) {
    
    
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
    
    
        	// 将结果赋值给 outcome
            outcome = v;
            // 修改线程状态
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL);
            // 移除所有等待线程并发出信号
            finishCompletion();
        }
    }
    
    private void finishCompletion() {
    
    
        for (WaitNode q; (q = waiters) != null;) {
    
    
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
    
    
                for (;;) {
    
    
                    Thread t = q.thread;
                    if (t != null) {
    
    
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }
		// 留给子类重写
        done();
        callable = null;        // to reduce footprint
    }
}

FutureTask.run()メソッドのロジックは、Callable.call()メソッドを呼び出してタスクを実行し、返された結果をset()メソッドを通じて属性に割り当てることですoutcome

上記のコードブロックの処理は次のようにnew Thread(futureTask).start();なります。

FutureTask の run() メソッドは Thread の start() メソッドを通じて実行され、最後に Callable! の call() メソッドを通じて実行されます。

get()メソッドの実装: get()このメソッドは、まずstate変数を使用してタスクが完了したかどうかを判断します。完了した場合は、結果が直接返されます。そうでない場合は、待機ノードが構築され、待機キューに投入されてスピンし、メインスレッドがブロックされます。反対側のスレッドが結果を計算した後、待機キュー内のすべてのノードをキューから取り出し、スレッドをウェイクアップします。

public V get() throws InterruptedException, ExecutionException {
    
    
    int s = state;
    // 任务还未执行完,等待执行完
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // 任务执行完|已取消,返回结果或者抛出异常
    return report(s);
}

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    
    
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
    
    
    	// 发生线程中断,从等待队列中移除节点,并抛出中断异常
        if (Thread.interrupted()) {
    
    
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        // 任务已执行完|已取消
        if (s > COMPLETING) {
    
    
        	// 若已有等待节点,将线程置空,避免另外一边再次调用 unpark()
            if (q != null)
                q.thread = null;
            return s;
        }
        // 任务正在执行,让出时间片
        else if (s == COMPLETING)
            Thread.yield();
        // 如果还未构造等待队列节点,则构造
        else if (q == null)
            q = new WaitNode();
        // 如果为入队,则 CAS 尝试入队;如果入队失败,则下一次循环再次尝试
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        // 通过 LockSupport.park() 阻塞线程
        else if (timed) {
    
    
            nanos = deadline - System.nanoTime();
            // 如果限时,且不大于 0,则从等待队列中移除节点,并返回
            if (nanos <= 0L) {
    
    
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);
    }
}

private V report(int s) throws ExecutionException {
    
    
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    //...
}

get()メソッドの処理ロジックは次のとおりです。

  1. まずstateの値がcomleting(実行が完了)より大きいかどうかを判定し、そうであれば正常であると判定し、正常であれば結果結果を返し、cancelであればキャンセル例外を返します。異常な場合は、outcome に記録された例外がスローされます。
  2. state の値が実行が完了していないことを示している場合、無限ループであるスピン操作に入ります。
  3. 状態が完了している場合は、Thread.yield によるタイム スライスを放棄します。
  4. それ以外の場合、結果がすでに出ている場合は戻ります
  5. それ以外の場合、待機ノードがまだ構築されていない場合は、待機ノードが構築され、現在のスレッドがノードに格納されます。
  6. それ以外の場合、ノードがまだキューに追加されていない場合は、ノードをチームに追加します。
  7. それ以外の場合、LockSupport.park()メソッドの呼び出しによりスレッドがブロックされます

タスクを実行するスレッドがタスクの実行を終了した後(run()メソッドの実行を終了した後)、結果または例外を結果に保存し、状態の値を変更し、キュー内のノードを順番にデキューし、LockSupport.unpark()ノード内のスレッドをウェイクアップするメソッドを呼び出すだけです。(これらの操作はset()メソッド内にあります)

ケース: Future を使用して複数の非同期タスクをバッチで実行し、結果を取得する

ExecutorService executor = Executors.newFixedThreadPool(2); 
Future f1 = excutor.submit(c1);
f1.get();
Future f2 = excutor.submit(c2);
f2.get();

f1.get() 取得が成功する前にブロックされるため、c2 の実行がブロックされ、効率が大幅に低下します。

Futureインターフェースの制限:

基本的に、Future は非同期計算の結果を表します。計算が完了したかどうかを検出する isDone() を提供し、計算完了後は get() メソッドを通じて計算結果を取得できます。ただし、多くの制限があります。

  1. 複数のタスクを同時に実行します。Future は結果を取得するための get() メソッドのみを提供しており、ブロックされています。
  2. 呼び出しを複数のタスクにチェーンできない: 計算タスクの完了後に特定のアクション (電子メールの送信など) を実行したい場合、Future にはそのような機能がありません。
  3. 複数のタスクを組み合わせることができない: 10 個のタスクを実行し、それらがすべて実行された後に特定のアクションを実行することが期待されている場合、Future で実行できることは何もありません。
  4. 例外処理なし: Future インターフェースには例外処理のメソッドがありません。

3. 完了サービス

Callable + Future複数のタスクを並行して実行できますが、前のタスクの実行が遅い場合は、結果を取得する前に、前のタスクが次のタスクの実行を完了するまでブロックして待つ必要があります。の主な機能はCompletionService次のとおりです。タスク生成時にタスクの戻り値を取得します。2 つのことを別々に実行すると、タスクが相互にブロックされなくなり、最初に実行された結果が最初に取得され、タスクの順序に依存しなくなります。

ExecutorCompletionServiceクラスはCompletionServiceインターフェイスを実装します

CompletionService を使用して上記のケースを最適化します

public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
    ExecutorService executor = Executors.newFixedThreadPool(10);
    CompletionService<Integer> cs = new ExecutorCompletionService<>(executor);

    Future<Integer> f1 = cs.submit(() -> {
    
    
        Thread.sleep(2000);
        return 1;
    });
    Future<Integer> f2 = cs.submit(() -> {
    
    
        Thread.sleep(1000);
        return 2;
    });
    for (int i = 0; i < 2; i++) {
    
    
        Integer integer = cs.take().get();
        System.out.println(integer);
    }
}

CompletionServiceインターフェース:

public class ExecutorCompletionService<V> implements CompletionService<V> {
    
    
	private final Executor executor;
    private final AbstractExecutorService aes;
    // 已完成的任务队列
    private final BlockingQueue<Future<V>> completionQueue;
	
	public Future<V> submit(Callable<V> task) {
    
    
		// 将 Callable 转化为 FutureTask
        RunnableFuture<V> f = newTaskFor(task);
        // 会调用 FutureTask 类中的 run() 方法
        executor.execute(new QueueingFuture(f));
        return f;
    }
	
	// 将 Callable 转化为 FutureTask
	private RunnableFuture<V> newTaskFor(Callable<V> task) {
    
    
        if (aes == null)
            return new FutureTask<V>(task);
        else
            return aes.newTaskFor(task);
    }
	
	// 内部类:任务完成后,将已完成的任务添加到队列中
	private class QueueingFuture extends FutureTask<Void> {
    
    
        QueueingFuture(RunnableFuture<V> task) {
    
    
            super(task, null);
            this.task = task;
        }
        // done() 方法:FutureTask 中的 finishCompletion() 方法调用。这里,被子类重写
        protected void done() {
    
     completionQueue.add(task); }
        private final Future<V> task;
    }
}

もう一度見てみましょうtake()、またはpoll(): すべての操作ですcompletionQueue

public Future<V> take() throws InterruptedException {
    
    
    return completionQueue.take();
}

public Future<V> poll() {
    
    
    return completionQueue.poll();
}

public Future<V> poll(long timeout, TimeUnit unit)
        throws InterruptedException {
    
    
    return completionQueue.poll(timeout, unit);
}

4. 完成段階

CompletionStageこれは Java 8 の新しいインターフェースであり、次の用途に使用されます。非同期実行でのステージ処理

シナリオ: 大きなタスクを複数のサブタスクに分割でき、サブタスク間に明らかな順序があるか、または 1 つのサブタスクが別のサブタスクの完了結果に依存する場合、CompletionStage が適切な選択です。

CompletionStage は、大きなタスクをサブタスクに分割することです。これらのサブタスクは、特定の並列および直列の組み合わせに基づいて、タスクのさまざまなステージを形成します。

そのインターフェイスをチェックしてください。

public interface CompletionStage<T> {
    
    

	public <U> CompletionStage<U> thenApply(Function<? super T,? extends U> fn);
	//...
	public CompletionStage<Void> runAfterBoth(CompletionStage<?> other, Runnable action);
	//...
	public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn);
	//...
}

CompletionStageインターフェイスには多くのメソッドがありますが、従うべきパターンを見つけるのは難しくありません。それらは基本的に、then、apply、async、accept、run、combine、both、other、after、compose、when、およびなどのキーワードで構成されます。ハンドル。これらのキーワードは次のように理解できます。

  • then: ステージのシーケンスを示します。つまり、あるステージが別のステージの完了を待ちます。
  • apply: Function と同じ、パラメータを消費して結果を提供することを意味します。
  • async: 非同期フラグ、つまり、ステージタスクの実行が現在のスレッドに対して同期であるか非同期であるか。
  • Accept: Consumer と同じで、パラメータの消費を示します。
  • run: 消費も削除もせず、Runnable インターフェースと同じ意味
  • 結合: 2 つのステージの結果を結合し、新しいステージを返します。
  • Both: 他の処理を実行する前に両方の条件が満たされていることを示します。
  • いずれか: 他のことを行う前に 2 つの条件のいずれかが満たされていることを示します。両方に対応します。
  • after: テーブル シーケンス。then と同様に、1 つのタスクが別のタスクの後に発生します。
  • compose: Function と同様に既存の結果を基に新しい結果を生成することを意味します。よく見ると、compose と thenApply のパラメーターに違いがあります。具体的な違いは以下のとおりです。
  • when: whenComplete と同等で、現在のフェーズが正常または異常に完了したときに BiConsumer アクションが実行されます。
  • handle: 現在のステージが正常または異常に完了した後に BiFunction アクションをトリガーします。

5. 完成可能な未来

CompletableFutureFutureインターフェイスを実装し、これに基づいて豊富な拡張機能を作成し、インターフェイスのFuture制限をCompletableFuture完全に補いました。CompletionStage

public class CompletableFuture<T> implements Future<T>, CompletionStage<T> {
    
    
	// 存储结果
	volatile Object result;
}

5.1 一般的な方法


依存関係

  • thenApply(): 前のタスクの実行結果を次の Function に渡します
CompletableFuture.supplyAsync(() -> 100).thenApply(x -> x * 100);
  • thenCompose(): 2 つの依存タスクを接続するために使用され、結果は 2 番目のタスクによって返されます。
CompletableFuture.supplyAsync(() -> 100).thenCompose(x -> CompletableFuture.supplyAsync(() -> x * 100));

そして関係を設定します

  • thenCombine(): 戻り値付きでタスクをマージします
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 100);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 200);
Integer result = future1.thenCombine(future2, Integer::sum).get();
  • thenAccepetBoth(): 2 つのタスクの実行が完了すると、結果は戻り値なしで、処理のために thenAccepet Both に渡されます。
future1.thenAcceptBoth(future2, (x, y) -> System.out.println(x + y));
  • runAfterBoth(): 両方のタスクが完了したら、次の操作を実行します (Runnable タイプのタスク)。すべて同期的に実行されます
future1.runAfterBoth(future2, () -> System.out.println(Thread.currentThread().getName()));

または集計関係

  • applyToEither(): 2 つのタスクのうち、より速く実行される方が結果を使用し、戻り値を持ちます。
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
    
    
    Thread.sleep(1000);
    return 100;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
    
    
    Thread.sleep(500);
    return 200;
});
System.out.println(future1.applyToEither(future2, x -> x * 100).get());
  • acceptEither(): 2 つのタスクのうちどちらか速く実行された方が結果を消費します。戻り値はありません。
  • runAfterEither(): いずれかのタスクが完了し、次のステップに進みます (Runnable 型タスク)

並列実行

  • allOf(): 指定されたすべての CompletableFuture が完了したら、新しい CompletableFuture を返します。
  • anyOf(): 特定の CompletableFuture が完了すると、新しい CompletableFuture を返します。

非同期操作

  • runAsync()
  • supplyAsync()

Executor を指定せずにメソッドを使用する場合、ForkJoinPool.commonPool()使用されるスレッド プールが内部的に非同期コードの実行に使用されます。スレッド プールが指定されている場合は、指定されたスレッド プールを使用して実行されます。
デフォルトでは、CompletableFuture はパブリック ForkJoinPool スレッド プールを使用します。デフォルトでこのスレッド プールによって作成されるスレッドの数は、CPU コアの数です ( を使用してJVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelismForkJoinPool スレッド プールのスレッド数を設定することもできます)。すべての CompletableFuture がスレッド プールを共有する場合、タスクが非常に遅い I/O 操作を実行すると、スレッド プール内のすべてのスレッドが I/O 操作でブロックされ、スレッドの枯渇が発生し、システム全体のパフォーマンスに影響します。したがって、相互干渉を避けるために、異なるビジネス タイプに応じて異なるスレッド プールを作成することを強くお勧めします。

public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
    
    
    ExecutorService threadPool = Executors.newFixedThreadPool(3);
    Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
    CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(runnable, threadPool).exceptionally(e -> {
    
    
        System.out.println("当前线程执行失败");
        return null;
    });
    completableFuture.get(30, TimeUnit.SECONDS);
}

結果を得る

  • get()
  • join()

方法の概要:

ここに画像の説明を挿入します

5.2 シナリオ

有名な数学者華洛庚氏は、「全体法」という記事で、水を沸騰させてお茶を作る例を紹介し、最適なプロセスは次のとおりであると述べています
ここに画像の説明を挿入します
。解決策: 2 つのスレッド T1 と T2 を使用して、お湯を沸かし、お茶を淹れるプロセスを完了します。T1 は、やかんを洗う、お湯を沸かし、お茶を淹れる 3 つのプロセスを担当します。T2 は、急須を洗う 3 つのプロセスを担当します。 、ティーカップの洗浄、茶葉の採取 T1 が実行中 お茶を入れるときは、T2 による茶葉の採取プロセスが完了するまで待つ必要があります。

将来を見据えた実装

public class T2 implements Callable<String> {
    
    

    @Override
    public String call() throws Exception {
    
    
        System.out.println("T2:洗茶壶...");
        TimeUnit.SECONDS.sleep(1);

        System.out.println("T2:洗茶杯...");
        TimeUnit.SECONDS.sleep(2);

        System.out.println("T2:拿茶叶...");
        TimeUnit.SECONDS.sleep(1);
        return "龙井";
    }
    
}
public class T1 implements Callable<String> {
    
    

    private FutureTask<String> t2;

    public T1(FutureTask<String> t2) {
    
    
        this.t2 = t2;
    }

    @Override
    public String call() throws Exception {
    
    
        System.out.println("T1:洗水壶...");
        TimeUnit.SECONDS.sleep(1);

        System.out.println("T1:烧开水...");
        TimeUnit.SECONDS.sleep(15);

        // 获取 T2 的茶叶
        String chaYe = t2.get();
        System.out.println("T1:拿到茶叶:" + chaYe);
        System.out.println("T1:泡茶...");
        return "上茶:" + chaYe;
    }

}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    FutureTask<String> task2 = new FutureTask<>(new T2());
    FutureTask<String> task1 = new FutureTask<>(new T1(task2));

    new Thread(task1).start();
    new Thread(task2).start();
    System.out.println(task1.get());
}

CompletableFuture に基づく実装

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    //任务1:洗水壶->烧开水
    CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
    
    
        System.out.println("T1:洗水壶...");
        sleep(1, TimeUnit.SECONDS);

        System.out.println("T1:烧开水...");
        sleep(15, TimeUnit.SECONDS);
    });
    //任务2:洗茶壶->洗茶杯->拿茶叶
    CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
    
    
        System.out.println("T2:洗茶壶...");
        sleep(1, TimeUnit.SECONDS);

        System.out.println("T2:洗茶杯...");
        sleep(2, TimeUnit.SECONDS);

        System.out.println("T2:拿茶叶...");
        sleep(1, TimeUnit.SECONDS);
        return "龙井";
    });
    //任务3:任务1和任务2完成后执行:泡茶
    CompletableFuture<String> f3 = task1.thenCombine(task2, (x, y) -> {
    
    
        System.out.println("T1:拿到茶叶:" + y);
        System.out.println("T1:泡茶...");
        return "上茶:" + y;
    });
    //等待任务3执行结果
    System.out.println(f3.join());
}

static void sleep(int t, TimeUnit u) {
    
    
    try {
    
    
        u.sleep(t);
    } catch (InterruptedException e) {
    
    
    }
}

おすすめ

転載: blog.csdn.net/sco5282/article/details/131124596