みなさん、こんにちは。私はコーダーです。前回の記事では、未来を支配する未来の愚かな息子であるランナブルの欠陥について話しました。スレッドの開始Thread.start()は基本的に、start()がネイティブメソッドstart0()を呼び出し、次にシステムスレッドを呼び出し、システムスレッドのRunnableでrun()メソッドを呼び出します。また、プロセス全体が非同期であるため、Runnableに2つの致命的な欠陥が生じます。1つは結果を返すことができないこと、もう1つは例外をスローできないことです。そこで、Javaのお父さんは、これら2つの欠陥を解決するために設計されたCallableインターフェースを後で導入しました。
Callableの使い方は、もうご存知だと思いますが、シニアプログラマーとしては、使い方を知るだけでは不十分です。その理由も知っておく必要がありますか?それでは、次の質問について考えてみましょう。
- call()メソッドもrun()メソッドのようにシステムスレッドを介して直接呼び出されますか?
- Callableはどのようにして結果をメインスレッドに返しますか?
- Callableはどのようにしてメインスレッドに例外をスローしますか?
次の数分で、Callableについての新しい理解が得られると思います。
実際、これらの関数の実現は、Callableインターフェースだけでは実現できません。また、この関数を完了するには、FutureクラスとFutureTaskクラスを使用する必要があります。
三ばか大将Future、FutureTask、Callable Introduction
紹介する前に考えてみましょう。スレッドが結果を返す関数を実装するように求められたら、どうすればよいですか?
以下に示すように:
メインスレッドに示されているように、スレッドは非同期で開始されますThread.start()
。実際、スレッドが実行された後、メインスレッドはすでに終了しています。では、結果を返すスレッドの機能を実現したいのであれば、曲線を通して国を救う方法でそれを実現する必要がありますが、それはどういう意味ですか?
スレッドは非同期であるため、結果を取得するには、メインスレッドをブロックし、スレッドが終了するのを待った後に結果をメインスレッドにコールバックする必要がありますか?Java 1.5以降、JavaDadはインターフェイスを提供しています。Future
、簡単に言えば、Futureクラスは、非同期スレッドの将来の結果を表します。この結果は、処理が完了した後、最終的にFutureに表示されます。Future.get()
このメソッドはブロッキング機能を実装しています。詳細については、この画像を参照してください。
図に示すように、メインスレッドはタスクFutureTaskをThreadに渡し、Thread.start()
それを開始してからrun()
、メソッド内のメソッドを呼び出し、メソッドを介してCallable.call()
戻り値Future.get()
をメインスレッドに返します。
全体的なクラス図を見てください。
クラス図からわかるように、FutureTaskはFutureとRunnableの実装クラスであり、ThreadインスタンスとCallableインスタンスの両方を保持します。FutureTaskはFutureの関数を実装します。つまり、FutureTaskはスレッドのブロックを管理して結果を取得するだけではありません(get ())、スレッドキャンセル割り込み(cancel())およびその他の関数。
結果を格納するためにFutureTaskで
Object outcome
変数が定義されていることに注意してください。リンクリストは
waiters
、結果を取得するのを待っているすべてのスレッドを格納するために定義されています
実際、FutureTaskは、スレッドと呼び出し可能の機能を統合するタスク管理センターと見なすことができます(これはアダプターモードです)。次に、コードを組み合わせて特定のロジックを分析します。
コード分析を実装する
上記の関係図とフローチャートでは、次のコードを表示するにはまだ問題があります
まず、簡単な使用方法を見てみましょう。ここでは詳しく説明しません。
- FutureTaskインスタンスを作成します。
- インスタンスをスレッドに渡して開始します
- 結果の取得をブロックする
public class FutureTest {
public static void main(String[] args) {
//1. 创建FutureTask并传入callable实例
Future<String> stringFutureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(3000);
return "FutureTask";
}
});
//2. 把实例传递给Thread并启动
new Thread(stringFutureTask).start();
String s = "";
try {
//3. 阻塞获取结果
s = stringFutureTask.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.printf(s);
}
}
次に、これらの3つのステップをソースコードと組み合わせて分析します。
1. FutureTaskを作成し、呼び出し可能なインスタンスを渡します
public FutureTask(Callable<V> callable) {
...
this.callable = callable;
this.state = NEW;
}
callableをFutureTask.callable変数に渡します。
2. 把实例传递给Thread并启动并调用run()方法
执行new Thread(stringFutureTask).start()
,这里启动后实际上调用的是Runnable.run()
方法,具体为啥是调用run(),可参照线程的实现方式, 我们看一下FutureTask.run()
源码
@Override
public void run() {
//把当前线程赋值给FutureTask.runner 实例
runner = Thread.currentThread();
try {
// 赋值变量
Callable<V> c = callable;
//启动状态为NEW
if (c != null && state == NEW) {
// 定义结果变量
V result;
boolean ran;
try {
//1. 这里调用 callable.call() 方法,也就是第一步传入的callable. 并返回结果
result = c.call();
ran = true;
} catch (Throwable ex) {
//如果抛出异常,
result = null;
ran = false;
//改变线程状态为 COMPLETING
if (STATE.compareAndSet(this, NEW, COMPLETING)) {
//2. 把异常赋给 outcome 变量
outcome = ex;
// 改变线程状态为 EXCEPTIONAL
STATE.setRelease(this, EXCEPTIONAL);
}
}
if (ran) {
//设置结果并通知所有等待的线程
set(result);
}
}
} finally {
...
}
}
protected void set(V v) {
// 改变线程状态为 EXCEPTIONAL
if (STATE.compareAndSet(this, NEW, COMPLETING)) {
//3. 把结果赋给 outcome 变量
outcome = v;
// 改变线程状态为 NORMAL
STATE.setRelease(this, NORMAL);
// 4. 遍历阻塞等待的获取锁的线程,通知他们锁已释放
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);
}
FutureTask.WaitNode next = q.next;
if (next == null) {
break;
}
q.next = null;
q = next;
}
break;
}
}
callable = null;
}
}
上面的就是线程运行的源码,核心点有4个,
- 就是在这里调用
call()
方法。 - 如果抛出异常把异常存到
Object outcome
变量里面 - 如果正常返回结果,把结果存到
Object outcome
中。至此线程运行完毕。 - 遍历阻塞等待的获取锁的线程,通知他们锁已释放
其实就是线程运行完后 把正常结果或者异常结果存到 Object outcome 对像中,释放锁并通知所有等待的线程。
到这里就可以回答开篇的第一个问题
1. call()方法是否也是和run()方法一样通过系统线程直接来调用的?
,调用流程是:Thread.start() --> native start0() --> run() -> call()
可以看出,call() 方法是通过 run() 来调用的,当然这也是在线程中。
3. 阻塞获取结果
这一步是调用 Future.get()
方法阻塞线程,等待结果,我们看一下源码:
public V get() throws Exception {
int s = state;
if (s <= COMPLETING) {
// 1. 如果线程还在执行,就就到waiters 链表里面阻塞等待结果。
s = awaitDone();
}
// 获取结果
if (s == NORMAL) {
// 2. 如果正常就返回正常的结果 outcome
return (V)outcome;
}
// 3. 如果异常就直接抛出 outcome
throw new Exception((Throwable)outcome);
}
// 这个方法的意思就是,如果线程还在执行,就到waiters 链表里面等待,
// 一直到被 LockSupport.unpark() 唤醒
private int awaitDone() {
WaitNode q = null;
boolean queued = false;
for (;;) {
int s = state;
if (s > COMPLETING) {
return s;
} else if (s == COMPLETING) {
Thread.yield();
} else if (q == null) {
q = new WaitNode();
} if (!queued) {
queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
} else {
LockSupport.park(this);
}
}
}
上面的就是阻塞获取结果的源码,核心点有3个,
- 如果线程还在执行,就就到waiters 链表里面阻塞等待结果。
- 如果线程执行完并正常,就返回正常的结果 outcome
- 如果异常就直接抛出 outcome。
看到这里,我们再来回顾一下开篇的几个问题,你是不是有了答案了。
最后
到这里,Callable,Future 相关的都分析完了,源码解析都比较枯燥,写这么多也不容易,感谢大家看到这里,有什么意见或者建议可以留言一起讨论,看到后第一时间回复,也希望大家能给个赞,你的赞就是我写文章的动力,再次感谢。
GZH:TodoCoder、私は時々バックエンド関連のソリューションを共有します。Java、Go、Python、DevSecOpsに関連する技術記事、注目を集めることを歓迎します、ありがとう。
ナゲッツテクノロジーコミュニティのクリエイター署名プログラムの募集に参加しています。リンクをクリックして登録し、送信してください。