FutureTask を呼び出す複数のスレッドにより、作成した Callable の call メソッドが 1 回しか実行されませんでした (理由: ソース コードの詳細な説明) (問題解決方法: 詳細な説明)

まず、FutureTask が RunnableFuture インターフェイスを実装し、RunnableFuture インターフェイスが Runnable インターフェイスと Future インターフェイスを継承していることがわかります。

public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V>

したがって、これは new Thread() で Callable を使用する責任のあるアダプテーション クラスです。アダプター設計パターンのアイデアを使用します

さて、次のコードを見てみましょう。

public class FutureTaskTest {
    public static void main(String[] args) throws Exception {
        FutureTask<String> task=new FutureTask<>(()->{
            System.out.println("run...");
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
            return "小贱哥哥";
        });
        new Thread(task).start();
        new Thread(task).start();
        System.out.println(task.get());
    }
}

操作結果:

run...
Thread-0
小贱哥哥

この時点で、明らかに 2 つのスレッドを使用して実行したのに、結果が 1 回しか出力されないという問題が見つかりました。

どうしたの?

実際、問題は非常に単純です。まず、作成したコードを見て、ソース コード比較して分析してみましょう。

FutureTask<String> task=new FutureTask<>(()->{
    System.out.println("run...");
    System.out.println(Thread.currentThread().getName());
    TimeUnit.SECONDS.sleep(1);
    return "小贱哥哥";
});

新しい FutureTask を作成し、対応する FutureTask が内部で実行されました。

public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
}

this.state = NEW; および this.callable = callable;に注意してください。

次に、実行を開始します。

new Thread(task).start();
new Thread(task).start();

まず質問について考えてみましょう. FutureTask は間接的に Runnable インターフェースを実装しているため, run メソッドを FutureTask 内で書き換える必要があります. したがって, スレッドが実行を開始するときは, FutureTaskrun メソッドを使用する方が良いです.その実行メソッド:

public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
    }
}

state == NEW かつ callable ! = null の場合にのみ、call メソッド (  result = c.call(); )を実行できることがわかります。

(このc.call();メソッドの内容は以前に書いたものです) (ここではラムダ式が使用されています):

FutureTask<String> task=new FutureTask<>(()->{
    System.out.println("run...");
    System.out.println(Thread.currentThread().getName());
    TimeUnit.SECONDS.sleep(1);
    return "小贱哥哥";
});

さて、それでは、プログラムは最後まで正常に実行されるので、引き続き set メソッド (上記の run メソッド)まで進みます。

public void run() {

        ......

        if (ran)
           set(result);

        ......
}

次に、set メソッドを見てみましょう。

protected void set(V v) {
        if (STATE.compareAndSet(this, NEW, COMPLETING)) {
            outcome = v;
            STATE.setRelease(this, NORMAL); // final state
            finishCompletion();
        }
}

STATE.setRelease(this, NORMAL)が見つかり、状態を NORMAL に変更しました。

引き続き set メソッドを下に見て、finishCompletion メソッドを確認し、それをクリックすると、次のものが見つかります。

private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = waiters) != null;) {
            if (WAITERS.weakCompareAndSet(this, 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
}

このメソッドは最終的に呼び出し可能オブジェクトを null に変更します。

さて、ここですべてが明らかです。

最初のスレッドが実行されるとFutureTask の状態は NEW ではなくなり、呼び出し可能オブジェクトは null になります。また、2 つのスレッドはFutureTask の同じインスタンスを使用するため、2 番目のスレッドが実行されると、ここに直接戻ります。

public void run() {
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return;
    
    .......

}

そのため、実行されるのは 1 回だけです。

これは望ましくありません。どうすればこの状況を修正できるでしょうか?

実際、state の値を NEW にし、callable の値を null にしない方法を見つける必要があるだけです。

方法 1: 新しい FutureTask を作成します。

((更新するときに、FutureTask の構築メソッドを使用して状態を NEW に変更し、callable を再割り当てするため)これについては前にも述べました: this.state = NEW; および this.callable = callable; )

public class FutureTaskTest {
    public static void main(String[] args) throws Exception{
        final int[] i={0};
        Callable callable=()->{
            System.out.println("run...");
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
            return "小贱哥哥"+(++i[0]);
        };
        FutureTask<String> task1=new FutureTask<>(callable);
        FutureTask<String> task2=new FutureTask<>(callable);
        new Thread(task1).start();
        new Thread(task2).start();
        System.out.println(task1.get());
        System.out.println(task2.get());
    }
}

今回の結果は以下の通りです。

run...
Thread-1
run...
Thread-0
小贱哥哥2
小贱哥哥1

方法 2:まず考えてください: FutureTask の run() メソッドは、FutureTask の状態が NEW ではなくFutureTask の呼び出し可能オブジェクトが null であるという事実によって引き起こされます。これら 2 つの値を変更できる方法はありますか? ソースコードを見てみましょう:

private volatile int state;
private Callable<V> callable;

これらはすべてプライベートであり、変更を直接呼び出すことができないことがわかりました。(答え:反射を使用します)

public class FutureTaskTest {
    public static void main(String[] args) throws Exception {
        final int[] i = {0};
        FutureTask<String> task=new FutureTask<>(()->{
            System.out.println("run...");
            System.out.println(Thread.currentThread().getName());
            TimeUnit.SECONDS.sleep(1);
            return "小贱哥哥"+(++i[0]);
        });
        new Thread(task).start();
        Field callable = task.getClass().getDeclaredField("callable");
        callable.setAccessible(true);
        Object call = callable.get(task);
        System.out.println(task.get());
        Field state = task.getClass().getDeclaredField("state");
        state.setAccessible(true);
        state.set(task,0);
        callable.set(task,call);
        new Thread(task).start();
        System.out.println(task.get());
    }
}

結果:

run...
Thread-0
小贱哥哥1
run...
Thread-1
小贱哥哥2

上記のstate.set(task,0);について混乱しているかもしれませんが、なぜ 0 に設定する必要があるのでしょうか?

したがって、引き続きソースコードを確認する必要があります。

private volatile int state;
private static final int NEW          = 0; //任务新建和执行中
private static final int COMPLETING   = 1; //任务将要执行完毕
private static final int NORMAL       = 2; //任务正常执行结束
private static final int EXCEPTIONAL  = 3; //任务异常
private static final int CANCELLED    = 4; //任务取消
private static final int INTERRUPTING = 5; //任务线程即将被中断
private static final int INTERRUPTED  = 6; //任务线程已中断

おそらく、今すぐ徹底的に行う必要があります。

最後にもう 1 つ、人々は意図的にこのように開発しました。その目的は効率を向上させることです。戻り値は常に同じであるため、常にそれをキャッシュすることを考慮し、次回外出するときに値を直接返すことを考慮する必要があります。計算を行う必要がなくなり、効率が大幅に向上します。

お役に立てば幸いです!

おすすめ

転載: blog.csdn.net/xiaomaomixj/article/details/126821063
おすすめ