呼び出し可能、実行可能、Future および FutureTask

Callableインターフェイスと同様にFuture、Java の以降のバージョンで導入され、インターフェイスを実装するクラスとインターフェイスを実装するクラスの両方をスレッドによって実行されるタスクに使用できます。CallableRunnableCallbackRunnable

以下は、2 つのインターフェイスに関連するソース コードです。

// /xref/libcore/ojluni/src/main/java/java/util/concurrent/Callable.java
public interface Callable<V> {
    
    
    V call() throws Exception;
}

// /xref/libcore/ojluni/src/main/java/java/lang/Runnable.java
public interface Runnable {
    
    
    public abstract void run();
}

上記のソース コードからわかるように、Callable.call()メソッドには戻り値があり、例外をスローできますが、Runable.run()メソッドには戻り値がなく、例外はスローされません。

CallableインターフェイスはRunnableインターフェイスの補足として見ることができ、インターフェイスRunnableとの違いは、タスクの実行結果を返し、例外をスローできる点であり、通常はThreadPoolExecutor組み合わせて使用​​されます。

FutureタスクをキャンセルしRunnableたり、タスクがキャンセルされたかどうかを判断したり、タスクが完了したかどうかを問い合わせたり、タスクの結果を取得したりできるインターフェイスです。関連するソースコードはCallable次のとおりです。Future

public interface Future<V> {
    
    

  	// 取消任务的执行。任务已经完成或者已经被取消时调用此方法,会返回 false
    boolean cancel(boolean mayInterruptIfRunning);

  	// 判断任务是否被取消
    boolean isCancelled();

  	// 判断任务是否完成。正常完成、异常以及被取消,都将返回 true
    boolean isDone();

  	// 阻塞地获取任务的执行结果
    V get() throws InterruptedException, ExecutionException;

  	// 在一定时间内,阻塞地获取任务的执行结果
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

ただし、Futureは単なるインターフェイスであるため、その中の関数を使用するには、FutureTask非同期計算の結果を表す間接実装クラス を使用する必要があります。

FutureTaskRunnableFutureインターフェイスの実装クラスであり、RunnableFutureインターフェイスはRunnableおよびFutureインターフェイスを実装します。したがって、FutureTask引き渡しThreadまたはExecutor実行することができます。

FutureTaskの継承関係は次のとおりです。

FutureTaskの継承関係

FutureTask.run()メソッドが実行されるタイミングにより、FutureTask以下の 3 つの状態になります。

  • 未開始:FutureTask.run()メソッドが実行される前は、未開始状態です。
  • 開始済み:メソッドはFutureTask.run()実行中FutureTask開始済み状態です。
  • Completed:FutureTask.run()メソッドの実行が完了した後、正常に終了した後、または実行FutureTask.cacel(boolean)、キャンセル、または実行された後FutureTask.run() 方法时抛出异常而结束、完了状態になります。

以下は のFutureTask状態遷移図です。

FutureTask の状態

FutureTask.get()/FutureTask.get(long, TimeUnit)方法について:

  • FutureTaskが未開始または開始済みの状態にある場合、FutureTask.get()/FutureTask.get(long, TimeUnit)メソッドを実行するとスレッドがブロックされます。
  • FutureTaskが完了状態にあるときにFutureTask.get()/FutureTask.get(long, TimeUnit)メソッドを実行すると、呼び出し元のスレッドは結果をすぐに返すか、例外をスローします。

FutureTask.cancel(boolean)方法について:

  • が開始されていない場合FutureTaskFutureTask.cancel(boolean)メソッドを実行すると、このタスクは実行されません。
  • FutureTaskが開始状態にある場合、executeFutureTask.cancel(true)メソッドはタスクを実行しているスレッドを中断してタスクを停止しようとします。
  • FutureTask開始状態の場合、実行FutureTask.cancel(false)メソッドは実行中のタスクのスレッドに影響を与えません (実行中のタスクを完了まで実行させます)。
  • が完了した状態になるとFutureTask、executeFutureTask.cancel(boolean)メソッドは を返しますfalse

FutureTask の get および cancel 実行の概略図

実行のためにFutureTaskに引き渡すことも、メソッドを通じてオブジェクトを返すこともできますが、 はインターフェイスであるため、通常はオブジェクトがここで返されます。その後、メソッドまたはメソッドを呼び出すことができます。Executor<T> Future<T> ExecutorService.submit(Runnable, T)/Future<?> ExecutorService.submit(Runnable)FutureFutureFutureTaskFutureTask.get()/get(long, TimeUnit)FutureTask.cancel(boolean)

これは、スレッドが続行する前に、別のスレッドがタスクを完了するのを待つ必要がある場合に使用できますFutureTask複数のタスクを実行する複数のスレッドがあるとします。複数のスレッドが同じタスクを同時に実行しようとすると、1 つのスレッドだけがタスクの実行を許可され、他のスレッドは実行を続ける前にタスクが完了するまで待機する必要があります。

FutureTaskのコードは次のとおりです。

private final ConcurrentHashMap<Object, Future<String>> taskCache = new ConcurrentHashMap<>();

    private String executionTask(final String taskName) {
    
    
        while (true) {
    
    
            Future<String> future = taskCache.get(taskName); // 1.1 2.1
            if (future == null) {
    
    
                Callable<String> task = new Callable<String>() {
    
    
                    @Override
                    public String call() throws Exception {
    
    
                        return taskName;
                    }
                };
                FutureTask<String> futureTask = new FutureTask<>(task);
                future = taskCache.putIfAbsent(taskName, futureTask); // 1.3
                if (future == null) {
    
    
                    future = futureTask;
                    futureTask.run(); // 1.4 执行任务
                }
            }
            try {
    
    
                return future.get(); // 1.5 2.2 线程在此等待任务执行完成
            } catch (ExecutionException e) {
    
    
                throw new RuntimeException(e);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }
        }
    }

以下はコードの概略図です。

コード実行プロセス

2 つのスレッドが同時に同じタスクを実行しようとした場合、スレッド 1 が 1.3 を実行し、スレッド 2 が 2.1 を実行すると、スレッド 2 は 2.2 で待機し、スレッド 1 が 1.4 を実行するまで待機してから、スレッド 2 は 2.2 から戻ります。

FutureTask使用方法は次のとおりです。

public class Test {
    
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        Thread.sleep(100);
        futureTask.cancel(true); // 1
        System.out.println("future is cancel: " + futureTask.isCancelled());
        if (!futureTask.isCancelled()) {
    
    
            System.out.println("future is cancelled");
        }
        System.out.println("future is done: " + futureTask.isDone());
        if (!futureTask.isDone()) {
    
    
            System.out.println("future get = " + futureTask.get()); // 2
        } else {
    
    
            System.out.println("task is done");
        }
    }
}


class MyCallable implements Callable<String> {
    
    

    @Override
    public String call() throws Exception {
    
    
        System.out.println(Thread.currentThread().getName() + "-call is running");
        Thread.sleep(5000);
        return "Hello World";
    }
}

実行結果は以下の通りです。

の結果

注 1 でタスクの実行をキャンセルしてみます。タスクが完了またはキャンセルされたときにメソッドが return する場合、false完了していないタスクをキャンセルできる場合は return しますtrue。なぜなら、上記のコードではタスクがまだ休止状態にあるため、タスクをキャンセルできます。注 2 では、FutureTask.get()タスクの実行が完了して結果が返されるまで、メソッドは現在のスレッドをブロックします。

CallableおよびとともにFuture使用する場合:

public class Test {
    
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Callable<String> call = new MyCallable();
        Future<String> future = executorService.submit(call);
        executorService.shutdown();
        Thread.sleep(5000);
        System.out.println("主线程休眠 5 秒,当前时间: " + System.currentTimeMillis());
        String str = future.get();
        System.out.println("Future 已拿到数据,str = " + str + "; 当前时间为: " + System.currentTimeMillis());
    }
}

class MyCallable implements Callable<String> {
    
    

    @Override
    public String call() throws Exception {
    
    
        System.out.println("进入 call 方法,开始休眠,休眠的时间为: " + System.currentTimeMillis());
        Thread.sleep(10000);
        return "Hello World";
    }
}

結果:

の結果

上記のコードでは、future実行のためにスレッド プールに直接入れられます。実行結果から判断すると、メインスレッドは5秒スリープしますが、call()タスクの結果はメソッド実行から得られるため、両者の時間差は10秒あります。FutureTask.get() 方法会阻塞当前线程直到任务完成。

ExecutorService.shutdow()SHUTDOWNこの時点では、スレッド プールは新しいタスクを受け取ることができず、すべてのタスクが実行されるまで待機しますこのメソッドが呼び出された場合ExecutorService.shutdownNow()、スレッドはSTOPその状態にあり、スレッド プールは現時点では新しいタスクを受け入れることができず、実行中の結果を終了しようとします。

FutureTask同じ効果は次の方法でも実現できます。

public class Test {
    
    

    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Callable<String> call = new MyCallable();
        FutureTask<String> task = new FutureTask<>(call);
        executorService.submit(task);
        executorService.shutdown();
        Thread.sleep(5000);
        System.out.println("主线程休眠 5 秒,当前时间: " + System.currentTimeMillis());
        String str = task.get();
        System.out.println("Future 已拿到数据,str = " + str + "; 当前时间为: " + System.currentTimeMillis());
    }
}

class MyCallable implements Callable<String> {
    
    

    @Override
    public String call() throws Exception {
    
    
        System.out.println("进入 call 方法,开始休眠,休眠的时间为: " + System.currentTimeMillis());
        Thread.sleep(10000);
        return "Hello World";
    }
}

結果:

の結果

上記の組み合わせにより次のような変化が生じます。シナリオがある場合、メソッド A がデータを返すのに 10 秒かかり、メソッド A の背後にあるコードの実行には 20 秒かかりますが、20 秒の実行結果では、後半の 10 秒だけがかかります。メソッド A の実行結果に依存します。従来と同じ方法だとどうしても数十秒の時間が無駄になってしまいますが、最初の2つの組み合わせを使えば効率が上がります。

  1. まず、メソッド A の内容をCallable実装クラスのメソッドに入れますcall()
  2. メインスレッドのスレッドプールを通じてタスク A を実行します。
  3. メソッドAの実行結果に依存しない以下のメソッドのコードを10秒間実行します。
  4. メソッド A の実行結果を取得し、次のメソッドでメソッド A の実行結果に依存するコードを 10 秒間実行します。

これにより、コードの効率が向上し、プログラムが A メソッドに固執する必要がなくなります。

参考

この記事は、Runnable、Future、Callable、FutureTask の関係を理解するのに十分です。

おすすめ

転載: blog.csdn.net/xingyu19911016/article/details/130193410