まず、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 内で書き換える必要があります. したがって, スレッドが実行を開始するときは, FutureTaskのrun メソッドを使用する方が良いです.その実行メソッド:
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 つ、人々は意図的にこのように開発しました。その目的は効率を向上させることです。戻り値は常に同じであるため、常にそれをキャッシュすることを考慮し、次回外出するときに値を直接返すことを考慮する必要があります。計算を行う必要がなくなり、効率が大幅に向上します。
お役に立てば幸いです!