1擎鸠、Callable 和 Runnable 的不同?
1.1 Runnable 接口
public interface Runnable {
public abstract void run();
}
Runnable 是一個 interface缘圈,并且里面只有一個方法劣光,叫作 public abstract void run()。這個方法已經(jīng)規(guī)定了 run() 方法的返回類型是 void糟把,而且這個方法沒有聲明拋出任何異常绢涡。所以,當(dāng)實現(xiàn)并重寫這個方法時遣疯,既不能改返回值類型雄可,也不能更改對于異常拋出的描述,因為在實現(xiàn)方法的時候缠犀,語法規(guī)定是不允許對這些內(nèi)容進(jìn)行修改的滞项。
所以Runnable 不能返回一個返回值,不能拋出 checked Exception夭坪。
為什么 Java 要把它設(shè)計成這個樣子呢文判?
假設(shè) run() 方法可以返回返回值,或者可以拋出異常室梅,也無濟(jì)于事戏仓,因為我們并沒有辦法在外層捕獲并處理,這是因為調(diào)用 run() 方法的類(比如 Thread 類和線程池)是 Java 直接提供的亡鼠,而不是我們編寫的赏殃,所以就算它能有一個返回值,我們也很難把這個返回值利用到间涵。
1.2 Callable 接口
public interface Callable<V> {
V call() throws Exception;
}
可以看出它也是一個 interface仁热,并且它的 call 方法中已經(jīng)聲明了 throws Exception,前面還有一個 V 泛型的返回值勾哩,這就和 Runnable 有很大的區(qū)別抗蠢。實現(xiàn) Callable 接口,就要實現(xiàn) call 方法思劳,這個方法的返回值是泛型 V迅矛,如果把 call 中計算得到的結(jié)果放到這個對象中,就可以利用 call 方法的返回值來獲得子線程的執(zhí)行結(jié)果了潜叛。
1.3 Callable 和 Runnable 的不同之處
- 方法名秽褒,Callable 規(guī)定的執(zhí)行方法是 call()壶硅,而 Runnable 規(guī)定的執(zhí)行方法是 run();
- 返回值销斟,Callable 的任務(wù)執(zhí)行后有返回值庐椒,而 Runnable 的任務(wù)執(zhí)行后是沒有返回值的;
- 拋出異常蚂踊,call() 方法可拋出異常约谈,而 run() 方法是不能拋出受檢查異常的;
- 和 Callable 配合的有一個 Future 類悴势,通過 Future 可以了解任務(wù)執(zhí)行情況窗宇,或者取消任務(wù)的執(zhí)行措伐,還可獲取任務(wù)執(zhí)行的結(jié)果特纤,這些功能都是 Runnable 做不到的,Callable 的功能要比 Runnable 強大侥加。
2捧存、Future 的主要功能是什么?
2.1 Future 的作用
Future 最主要的作用是担败,比如當(dāng)做一定運算的時候(運算過程可能比較耗時昔穴,有時會去查數(shù)據(jù)庫,或是繁重的計算提前,比如壓縮吗货、加密等),在這種情況下狈网,如果我們一直在原地等待方法返回宙搬,整體程序的運行效率會大大降低。我們可以把運算的過程放到子線程去執(zhí)行拓哺,再通過 Future 去控制子線程執(zhí)行的計算過程勇垛,最后獲取到計算結(jié)果。這樣一來就可以把整個程序的運行效率提高士鸥,是一種異步的思想闲孤。
2.2 Callable 和 Future 的關(guān)系
首先看一下 Future 接口的代碼,一共有 5 個方法烤礁,代碼如下所示:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
//第 5 個方法是對第 4 個方法的重載讼积,方法名一樣,但參數(shù)不一樣
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutExceptio
}
Future 相當(dāng)于一個存儲器脚仔,存儲了 Callable 的 call 方法的任務(wù)結(jié)果币砂。
Future 的 get 方法來獲取 Callable 接口任務(wù)執(zhí)行的結(jié)果;
Future 的 isDone 方法來判斷任務(wù)是否已經(jīng)執(zhí)行完畢了玻侥;
Future 的 cancel 方法取消任務(wù)的執(zhí)行决摧;
Future 的 isCancelled() 方法判斷任務(wù)是否被取消。
2.3 用 FutureTask 來創(chuàng)建 Future
除了用線程池的 submit 方法會返回一個 future 對象之外,同樣還可以用 FutureTask 來獲取 Future 類和任務(wù)的結(jié)果掌桩。
FutureTask 首先是一個任務(wù)(Task)边锁,然后具有 Future 接口的語義,因為它可以在將來(Future)得到執(zhí)行的結(jié)果波岛。
來看一下 FutureTask 的代碼實現(xiàn):
public class FutureTask<V> implements RunnableFuture<V>{
...
}
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
可以看到茅坛,F(xiàn)utureTask 實現(xiàn)了一個接口,這個接口叫作 RunnableFuture则拷,而 RunnableFuture 是 extends Runnable 和 Future 這兩個接口的贡蓖,它們的關(guān)系如下圖所示:
既然 RunnableFuture 繼承了 Runnable 接口和 Future 接口,而 FutureTask 又實現(xiàn)了 RunnableFuture 接口煌茬,所以 FutureTask 既可以作為 Runnable 被線程執(zhí)行斥铺,又可以作為 Future 得到 Callable 的返回值。
典型用法是坛善,把 Callable 實例當(dāng)作 FutureTask 構(gòu)造函數(shù)的參數(shù)晾蜘,生成 FutureTask 的對象,然后把這個對象當(dāng)作一個 Runnable 對象眠屎,放到線程池中或另起線程去執(zhí)行剔交,最后還可以通過 FutureTask 獲取任務(wù)執(zhí)行的結(jié)果。
3改衩、使用 Future 有哪些注意點岖常?Future 產(chǎn)生新的線程了嗎?
3.1 Future 的注意點
- 當(dāng) for 循環(huán)批量獲取 Future 的結(jié)果時容易 block葫督,get 方法調(diào)用時應(yīng)使用 timeout 限制
即調(diào)用 Future 的帶超時參數(shù)的 get(long timeout, TimeUnit unit)方法竭鞍。這個方法的作用是,如果在限定的時間內(nèi)沒能返回結(jié)果的話候衍,那么便會拋出一個 TimeoutException 異常笼蛛,隨后就可以把這個異常捕獲住,或者是再往上拋出去蛉鹿,這樣就不會一直卡著了滨砍。
- Future 的生命周期不能后退
Future 的生命周期不能后退,一旦完成了任務(wù)妖异,它就永久停在了“已完成”的狀態(tài)惋戏,不能從頭再來,也不能讓一個已經(jīng)完成計算的 Future 再次重新執(zhí)行任務(wù)他膳。
3.2 Future 產(chǎn)生新的線程了嗎响逢?
其實 Callable 和 Future 本身并不能產(chǎn)生新的線程,它們需要借助其他的比如 Thread 類或者線程池才能執(zhí)行任務(wù)棕孙。例如舔亭,在把 Callable 提交到線程池后些膨,真正執(zhí)行 Callable 的其實還是線程池中的線程,而線程池中的線程是由 ThreadFactory 產(chǎn)生的钦铺,這里產(chǎn)生的新線程與 Callable订雾、Future 都沒有關(guān)系,所以 Future 并沒有產(chǎn)生新的線程矛洞。
4洼哎、如何利用 CompletableFuture 實現(xiàn)“旅游平臺”問題?
4.1 什么是旅游平臺問題沼本?
如果想要搭建一個旅游平臺噩峦,經(jīng)常會有這樣的需求,那就是用戶想同時獲取多家航空公司的航班信息抽兆。所以應(yīng)該把所有航空公司的航班识补、票價等信息都獲取到,然后再聚合郊丛。由于每個航空公司都有自己的服務(wù)器李请,所以分別去請求它們的服務(wù)器就可以了瞧筛,比如請求國航厉熟、海航、東航等较幌,如下圖所示:
4.2 獲取旅游信息的方式
(1)串行獲取
比如想獲取價格揍瑟,要先去訪問國航,在這里叫作 website 1乍炉,然后再去訪問海航 website 2绢片,以此類推。當(dāng)每一個請求發(fā)出去之后岛琼,等它響應(yīng)回來以后底循,才能去請求下一個網(wǎng)站,這就是串行的方式槐瑞。
這樣做的效率非常低下熙涤,比如航空公司比較多,假設(shè)每個航空公司都需要 1 秒鐘的話困檩,那么用戶肯定等不及祠挫,所以這種方式是不可取的。
(2)并行獲取
可以并行地去獲取這些機票信息悼沿,然后再把機票信息給聚合起來等舔,這樣的話,效率會成倍的提高糟趾。
這種并行雖然提高了效率慌植,但也有一個缺點甚牲,那就是會“一直等到所有請求都返回”。如果有一個網(wǎng)站特別慢蝶柿,那么你不應(yīng)該被那個網(wǎng)站拖累鳖藕,比如說某個網(wǎng)站打開需要二十秒,那肯定是等不了這么長時間的只锭,所以需要一個功能著恩,那就是有超時的獲取。
(3)有超時的并行獲取
在這種情況下蜻展,就屬于有超時的并行獲取喉誊,同樣也在并行的去請求各個網(wǎng)站信息。但是規(guī)定了一個時間的超時纵顾,比如 3 秒鐘伍茄,那么到 3 秒鐘的時候如果都已經(jīng)返回了那當(dāng)然最好,把它們收集起來即可施逾;但是如果還有些網(wǎng)站沒能及時返回敷矫,就把這些請求給忽略掉,這樣一來用戶體驗就比較好了汉额,它最多只需要等固定的 3 秒鐘就能拿到信息曹仗,雖然拿到的可能不是最全的,但是總比一直等更好蠕搜。
4.3 實現(xiàn)“有超時的并行獲取”的三種方案
(1)線程池實現(xiàn)
public class ThreadPoolDemo {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
public static void main(String[] args) throws InterruptedException {
ThreadPoolDemo threadPoolDemo = new ThreadPoolDemo();
System.out.println(threadPoolDemo.getPrices());
}
private Set<Integer> getPrices() throws InterruptedException {
Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
threadPool.submit(new Task(123, prices));
threadPool.submit(new Task(456, prices));
threadPool.submit(new Task(789, prices));
Thread.sleep(3000);
return prices;
}
private class Task implements Runnable {
Integer productId;
Set<Integer> prices;
public Task(Integer productId, Set<Integer> prices) {
this.productId = productId;
this.prices = prices;
}
@Override
public void run() {
int price=0;
try {
Thread.sleep((long) (Math.random() * 4000));
price= (int) (Math.random() * 4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
prices.add(price);
}
}
}
在代碼中怎茫,新建了一個線程安全的 Set,它是用來存儲各個價格信息的妓灌,把它命名為 Prices轨蛤,然后往線程池中去放任務(wù)。線程池是在類的最開始時創(chuàng)建的虫埂,是一個固定 3 線程的線程池祥山。而這個任務(wù)在下方的 Task 類中進(jìn)行了描述,在這個 Task 中我們看到有 run 方法掉伏,在該方法里面缝呕,我們用一個隨機的時間去模擬各個航空網(wǎng)站的響應(yīng)時間,然后再去返回一個隨機的價格來表示票價岖免,最后把這個票價放到 Set 中岳颇。這就是我們 run 方法所做的事情。
再回到 getPrices 函數(shù)中颅湘,我們新建了三個任務(wù)话侧,productId 分別是 123、456闯参、789瞻鹏,這里的 productId 并不重要悲立,因為我們返回的價格是隨機的,為了實現(xiàn)超時等待的功能新博,在這里調(diào)用了 Thread 的 sleep 方法來休眠 3 秒鐘薪夕,這樣做的話,它就會在這里等待 3 秒赫悄,之后直接返回 prices原献。
此時,如果前面響應(yīng)速度快的話埂淮,prices 里面最多會有三個值姑隅,但是如果每一個響應(yīng)時間都很慢,那么可能 prices 里面一個值都沒有倔撞。不論你有多少個讲仰,它都會在休眠結(jié)束之后,也就是執(zhí)行完 Thread 的 sleep 之后直接把 prices 返回痪蝇,并且最終在 main 函數(shù)中把這個結(jié)果給打印出來鄙陡。
我們來看一下可能的執(zhí)行結(jié)果,一種可能性就是有 3 個值躏啰,即 [3815, 3609, 3819](數(shù)字是隨機的)趁矾;有可能是 1 個 [3496]、或 2 個 [1701, 2730]丙唧,如果每一個響應(yīng)速度都特別慢愈魏,可能一個值都沒有觅玻。
(2)CountDownLatch實現(xiàn)
在這里會有一個優(yōu)化的空間想际,比如說網(wǎng)絡(luò)特別好時,每個航空公司響應(yīng)速度都特別快溪厘,你根本不需要等三秒胡本,有的航空公司可能幾百毫秒就返回了,那么我們也不應(yīng)該讓用戶等 3 秒畸悬。所以需要進(jìn)行一下這樣的改進(jìn)侧甫,看下面這段代碼:
public class CountDownLatchDemo {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
public static void main(String[] args) throws InterruptedException {
CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
System.out.println(countDownLatchDemo.getPrices());
}
private Set<Integer> getPrices() throws InterruptedException {
Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
CountDownLatch countDownLatch = new CountDownLatch(3);
threadPool.submit(new Task(123, prices, countDownLatch));
threadPool.submit(new Task(456, prices, countDownLatch));
threadPool.submit(new Task(789, prices, countDownLatch));
countDownLatch.await(3, TimeUnit.SECONDS);
return prices;
}
private class Task implements Runnable {
Integer productId;
Set<Integer> prices;
CountDownLatch countDownLatch;
public Task(Integer productId, Set<Integer> prices,
CountDownLatch countDownLatch) {
this.productId = productId;
this.prices = prices;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
int price = 0;
try {
Thread.sleep((long) (Math.random() * 4000));
price = (int) (Math.random() * 4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
prices.add(price);
countDownLatch.countDown();
}
}
}
這段代碼使用 CountDownLatch 實現(xiàn)了這個功能,整體思路和之前是一致的蹋宦,不同點在于我們新增了一個 CountDownLatch披粟,并且把它傳入到了 Task 中。在 Task 中冷冗,獲取完機票信息并且把它添加到 Set 之后守屉,會調(diào)用 countDown 方法,相當(dāng)于把計數(shù)減 1蒿辙。
這樣一來拇泛,在執(zhí)行 countDownLatch.await(3,
TimeUnit.SECONDS) 這個函數(shù)進(jìn)行等待時滨巴,如果三個任務(wù)都非常快速地執(zhí)行完畢了俺叭,那么三個線程都已經(jīng)執(zhí)行了 countDown 方法恭取,那么這個 await 方法就會立刻返回,不需要傻等到 3 秒鐘熄守。
如果有一個請求特別慢蜈垮,相當(dāng)于有一個線程沒有執(zhí)行 countDown 方法,來不及在 3 秒鐘之內(nèi)執(zhí)行完畢裕照,那么這個帶超時參數(shù)的 await 方法也會在 3 秒鐘到了以后窃款,及時地放棄這一次等待,于是就把 prices 給返回了牍氛。所以這樣一來晨继,我們就利用 CountDownLatch 實現(xiàn)了這個需求,也就是說我們最多等 3 秒鐘搬俊,但如果在 3 秒之內(nèi)全都返回了紊扬,我們也可以快速地去返回,不會傻等唉擂,提高了效率餐屎。
(3)CompletableFuture實現(xiàn)
public class CompletableFutureDemo {
private final CompletableFuture<Void> completableFuture = new CompletableFuture<Void>();
public static void main(String[] args)
throws Exception {
CompletableFutureDemo completableFutureDemo = new CompletableFutureDemo();
System.out.println(completableFutureDemo.getPrices());
}
private Set<Integer> getPrices() {
Set<Integer> prices = Collections.synchronizedSet(new HashSet<Integer>());
CompletableFuture<Void> task1 = CompletableFuture.runAsync(new Task(123, prices));
CompletableFuture<Void> task2 = CompletableFuture.runAsync(new Task(456, prices));
CompletableFuture<Void> task3 = CompletableFuture.runAsync(new Task(789, prices));
CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2, task3);
try {
allTasks.get(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
} catch (ExecutionException e) {
} catch (TimeoutException e) {
}
return prices;
}
private class Task implements Runnable {
Integer productId;
Set<Integer> prices;
public Task(Integer productId, Set<Integer> prices) {
this.productId = productId;
this.prices = prices;
}
@Override
public void run() {
int price = 0;
try {
Thread.sleep((long) (Math.random() * 4000));
price = (int) (Math.random() * 4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
prices.add(price);
}
}
}
這里不再使用線程池了,看 getPrices 方法玩祟,在這個方法中腹缩,用了 CompletableFuture 的 runAsync 方法,這個方法會異步的去執(zhí)行任務(wù)空扎。
有三個任務(wù)藏鹊,并且在執(zhí)行這個代碼之后會分別返回一個 CompletableFuture 對象,把它們命名為 task 1转锈、task 2盘寡、task 3,然后執(zhí)行 CompletableFuture 的 allOf 方法撮慨,并且把 task 1竿痰、task 2、task 3 傳入砌溺。這個方法的作用是把多個 task 匯總影涉,然后可以根據(jù)需要去獲取到傳入?yún)?shù)的這些 task 的返回結(jié)果,或者等待它們都執(zhí)行完畢等规伐。我們就把這個返回值叫作 allTasks蟹倾,并且在下面調(diào)用它的帶超時時間的 get 方法,同時傳入 3 秒鐘的超時參數(shù)楷力。
這樣一來它的效果就是喊式,如果在 3 秒鐘之內(nèi)這 3 個任務(wù)都可以順利返回孵户,也就是這個任務(wù)包括的那三個任務(wù),每一個都執(zhí)行完畢的話岔留,則這個 get 方法就可以及時正常返回夏哭,并且往下執(zhí)行,相當(dāng)于執(zhí)行到 return prices献联。在下面的這個 Task 的 run 方法中竖配,該方法如果執(zhí)行完畢的話,對于 CompletableFuture 而言就意味著這個任務(wù)結(jié)束里逆,它是以這個作為標(biāo)記來判斷任務(wù)是不是執(zhí)行完畢的进胯。但是如果有某一個任務(wù)沒能來得及在 3 秒鐘之內(nèi)返回,那么這個帶超時參數(shù)的 get 方法便會拋出 TimeoutException 異常原押,同樣會被我們給 catch 住胁镐。這樣一來它就實現(xiàn)了這樣的效果:會嘗試等待所有的任務(wù)完成,但是最多只會等 3 秒鐘诸衔,在此之間盯漂,如及時完成則及時返回。那么所以利用 CompletableFuture笨农,同樣也可以解決旅游平臺的問題就缆。它的運行結(jié)果也和之前是一樣的,有多種可能性谒亦。