Callable和Runnable有什么區(qū)別?
首先,Runnable是出自jdk1.0,Callable出自jdk1.5,那么,后出的類肯定對(duì)于前者有增強(qiáng)。
再看Runnable的run方法與Callable的call方法進(jìn)行對(duì)比
// Runnable
public abstract void run();
// Callable
V call() throws Exception;
可以看出,run方法沒有返回值,call方法有返回值并能夠拋出異常。
在對(duì)Callable原理進(jìn)行解析之前,首先先看一下Callable的基本用法
方式一:通過FutureTask,交給線程池執(zhí)行,阻塞等待結(jié)果返回
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
public String call() throws InterruptedException {
Thread.sleep(3000L);
System.out.println("當(dāng)前線程:" + Thread.currentThread().getName());
return "hello world";
}
});
ExecutorService executorService = Executors.newFixedThreadPool(1);
System.out.println("開始時(shí)間戳為:" + System.currentTimeMillis());
executorService.submit(futureTask);
String result = futureTask.get();
System.out.println("結(jié)束時(shí)間戳為:" + System.currentTimeMillis() + ",result = " + result);
}
結(jié)果打印:
開始時(shí)間戳為:1639275517192
當(dāng)前線程:pool-1-thread-1
結(jié)束時(shí)間戳為:1639275520202,result = hello world
可以看出,callable的邏輯是在異步線程中處理的,并且,主線程通過阻塞式接受異步線程返回的內(nèi)容。
那么,如果直接調(diào)用futureTask的run方法會(huì)怎樣?
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<String>(new Callable<String>() {
public String call() throws InterruptedException {
Thread.sleep(3000L);
System.out.println("當(dāng)前線程:" + Thread.currentThread().getName());
return "hello world";
}
});
System.out.println("開始時(shí)間戳為:" + System.currentTimeMillis());
futureTask.run();
String result = futureTask.get();
System.out.println("結(jié)束時(shí)間戳為:" + System.currentTimeMillis() + ",result = " + result);
}
結(jié)果打?。?開始時(shí)間戳為:1639275759794
當(dāng)前線程:main
結(jié)束時(shí)間戳為:1639275762796,result = hello world
結(jié)果和直接調(diào)用Runnable的方法是一樣的,最終是由主線程完成執(zhí)行
方式二:通過線程池執(zhí)行,返回Future對(duì)象
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(1);
System.out.println("開始時(shí)間戳為:" + System.currentTimeMillis());
Future<String> future = executorService.submit(new Callable<String>() {
public String call() throws InterruptedException {
Thread.sleep(3000L);
System.out.println("當(dāng)前線程:" + Thread.currentThread().getName());
return "hello world";
}
});
String result = future.get();
System.out.println("結(jié)束時(shí)間戳為:" + System.currentTimeMillis() + ",result = " + result);
}
結(jié)果打印:
開始時(shí)間戳為:1639276055569
當(dāng)前線程:pool-1-thread-1
結(jié)束時(shí)間戳為:1639276058583,result = hello world
實(shí)現(xiàn)效果與方式一相同,這里返回的Future,實(shí)際上也是FutureTask
原理分析
首先分析下線程池的submit方法
// FutureTask傳入方式調(diào)用的submit方法
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
// Callable傳入方式調(diào)用的submit方法
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
1.最終都會(huì)調(diào)用newTaskFor方法生成一個(gè)RunnableFuture方法
2.調(diào)用execute方法
3.返回RunnableFuture對(duì)象
線程池相關(guān)的原理不在本文章中具體展開,有興趣的小伙伴可以自行研究或者在之后的文章中推出詳解
再分析下future.get方法如何阻塞等待異步線程執(zhí)行結(jié)果
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
// 如果狀態(tài)還在進(jìn)行中,或者剛創(chuàng)建,就阻塞等待
s = awaitDone(false, 0L);
// 調(diào)用Report,返回結(jié)果或者拋出異常
return report(s);
}
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
// 狀態(tài)正常,返回結(jié)果
return (V)x;
if (s >= CANCELLED)
// 狀態(tài)取消,拋出取消異常
throw new CancellationException();
// 拋出程序執(zhí)行異常
throw new ExecutionException((Throwable)x);
}

FutureTask內(nèi)部維護(hù)了任務(wù)進(jìn)行的狀態(tài),當(dāng)異步任務(wù)完成時(shí),會(huì)將狀態(tài)碼設(shè)置為已完成,如果發(fā)生異常,會(huì)將狀態(tài)碼設(shè)置成對(duì)應(yīng)的異常狀態(tài)碼。
如何自己實(shí)現(xiàn)一個(gè)類似的能夠有返回值的Runnable
public static class MyRunnable implements Runnable {
private String result;
private AtomicBoolean finished = new AtomicBoolean();
public void run() {
try {
Thread.sleep(3000L);
System.out.println("當(dāng)前線程:" + Thread.currentThread().getName());
this.result = "hello world";
finished.compareAndSet(false, true);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String get() {
while (true) {
if (finished.get()) {
return result;
}
}
}
}
public static void main(String[] args) throws Exception {
System.out.println("開始時(shí)間戳為:" + System.currentTimeMillis());
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
String result = myRunnable.get();
System.out.println("結(jié)束時(shí)間戳為:" + System.currentTimeMillis() + ",result = " + result);
}
結(jié)果打?。?開始時(shí)間戳為:1639280212975
當(dāng)前線程:異步線程
結(jié)束時(shí)間戳為:1639280215984,result = hello world
本質(zhì)上就是調(diào)用線程持有了異步線程的對(duì)象引用,jdk給我們封裝好了對(duì)應(yīng)的邏輯,無須額外處理。
