併發編程的概念並不新鮮,每一種編程語言中都內置了相關的支持,而有些編程語言因爲對併發提供了更有友好的支持而得到了更多的關注。
擁抱併發
使用併發編程並不僅僅是爲了CPU多核從而使得程序能夠並行執行,其本質其實就是爲了消除延遲,例如訪問硬盤、網絡IO等慢速的設備相對單純的CPU計算會有很高的延遲,進而導致線程阻塞在這裏等待資源,這個時候CPU的資源就白白浪費了,因此我們會根據業務場景,選擇開啓多個線程,將這些比較耗時的IO任務丟到另外的線程中去處理,這樣就不會因爲某些慢請求而影響其他用戶,從而提高響應時間。因此這裏就涉及到了併發模型的選型,下面我選擇幾種並嘗試總結其優劣。
Java Future
Java Future 代表一種異步計算的結果,調用方可以檢查計算是否完成、等待計算完成、獲取計算結果等,使用起來非常的直觀,但當多個Future組合起來時,特別每個異步計算的耗時都不一樣,代碼通常會變的很複雜並且很容易出錯。各種Future#get和try/catch充斥在代碼中,導致可讀性變的非常差。
What about callback?
提起回調我們都會想起回調地獄,隨便Google一下都能找到很多類似的代碼,但回調也有它自己的優勢所在,異步計算的結果完成的那一刻我們就能夠收到通知,但和Future一樣當有多個不同的條件時,代碼就會變的很不可控,充滿了各種內嵌。
下面的示例只有兩層而已,試想一下如果有多個需要組合處理,那麼維護這段代碼將會變的非常蛋疼。
public static void main(String[] args) {
fetchBlogs(new Callback<List<String>>() {
public void onComplete(List<String> data) {
data.forEach(s -> fetchImage(s, new Callback<String>() {
@Override
public void onComplete(String data) {
}
@Override
public void error(Throwable throwable) {
throwable.printStackTrace();
}
}));
}
public void error(Throwable throwable) {
throwable.printStackTrace();
}
});
}
public static void fetchBlogs(Callback<List<String>> callback) {
callback.onComplete(Collections.<String>emptyList());
}
public static void fetchImage(String title, Callback<String> callback) {
callback.onComplete(title);
}
}
interface Callback<T> {
void onComplete(T data);
void error(Throwable throwable);
}
Guava的解決方案
熟悉Guava的同學都知道,它提供了很多工具類從而使得我們可以更友好的編寫併發代碼,但是在處理多個異步任務之間的組合依賴關係時也有類似的問題, 因爲本質上就是將Future的計算轉換成了回調的方式提供給用戶,同樣會產生很多的內嵌代碼。
private static final ListeningExecutorService executorService = MoreExecutors
.listeningDecorator(Executors.newCachedThreadPool());
public static void main(String[] args) {
Futures.addCallback(fetchBlogs(), new FutureCallback<List<String>>() {
@Override
public void onSuccess(List<String> result) {
result
.forEach(s -> Futures.addCallback(fetchImage(s), new FutureCallback<String>() {
@Override
public void onSuccess(String result) {
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
}, executorService));
}
@Override
public void onFailure(Throwable t) {
t.printStackTrace();
}
}, executorService);
}
public static ListenableFuture<List<String>> fetchBlogs() {
return executorService.submit(
(Callable<List<String>>) Collections::emptyList);
}
public static ListenableFuture<String> fetchImage(String title) {
return executorService.submit(() -> title);
}
And CompletableFuture?
CompletableFuture是在JDK1.8進入的,相當於Future的升級版,其實靈感就是來自Guava的 Listenable Futures,它提供了一堆操作符讓你可以將多個異步任務的處理組合成鏈式,從而讓你可以方便處理多個future之間的依賴關係。
public static void main(String[] args) {
fetchBlogs().thenAccept(blogs ->
blogs.forEach(s ->
fetchImage(s)
.thenAccept(s1 -> System.out.println())
.exceptionally(throwable -> {
throwable.printStackTrace();
return null;
})))
.exceptionally(throwable -> {
throwable.printStackTrace();
return null;
});
}
public static CompletableFuture<List<String>> fetchBlogs() {
CompletableFuture<List<String>> completableFuture = new CompletableFuture<>();
completableFuture.complete(Collections.emptyList());
return completableFuture;
}
public static CompletableFuture<String> fetchImage(String title) {
CompletableFuture<String> completableFuture = new CompletableFuture<>();
completableFuture.complete("");
return completableFuture;
}
RxJava
Rxjava最初是由Netflix開發的Reactive programming框架,庫本身非常的強大,提供了大量的操作符、更友好的錯誤處理、事件驅動的編程模式,屏蔽了線程安全、同步、併發數據結構等底層併發原語。
所有的方法都返回Observable,對於調用方都是一直的方法簽名,而方法內部,我們想基於同步/異步、從緩存讀取還是數據庫讀取、採用NIO還是阻塞式IO,對於調用者來說都是透明的,你不需要關心,假設我們有這個方法Data getData(),很明顯目前是同步的方式,如果我們想改成異步的方式,那麼就需要改動方法簽名,例如返回Future或者添加一個callback參數,這就涉及了很大的改造,那麼如果採用RxJava的方法,這些對於上層都是透明的。
但是RxJava的也有其問題,學習成本很高,observeOn/subscribeOn等各種新鮮的概念需要去理解,如果系統完全基於Rxjava開發,那麼會調試也是個很蛋疼的問題,特別是多線程的情況下,雖然說官網以及WIKI等都提供了相當豐富的資料,但問題也在於此,太豐富了,不利於初學者上手。當然Java 9也提供了Reactive programming的解決方案,Flow API 提供了一系列接口,具體的實現可以採用Rxjava、 Vertx等。
private static final ExecutorService executorService = Executors.newCachedThreadPool();
public static void main(String[] args) {
fetchBlogs()
.subscribe(blogs ->
blogs.forEach(s ->
fetchImage(s)
.subscribe(System.out::println,
Throwable::printStackTrace)),
Throwable::printStackTrace);
}
public static Observable<List<String>> fetchBlogs() {
return Observable.create(observable -> executorService.submit(() -> {
try {
observable.onNext(Collections.emptyList());
observable.onComplete();
} catch (Exception e) {
observable.onError(e);
}
}));
}
public static Observable<String> fetchImage(String title) {
return Observable.create(observable -> {
observable.onNext("");
observable.onComplete();
});
}
Coroutine
Kotlin目前已經支持了Coroutine,Java也有類似的解決方案,現在也已經有了提案給Java也增加Coroutine的機制,可能很多人有一個誤解,coroutine並不是說有更高的性能,而是說讓我們可以像調用其他方法一樣去調用併發/阻塞的方法,而調度器則向你屏蔽了底層的細節。例如一個GetXXX方法,其內部設計到了網絡調用,那麼我們可以中斷在這裏並讓出CPU資源,從而使得其他coroutine可以運行,具體實現機制可以參考另一篇博客.
總結
這裏簡單總結了幾種不同的併發編程模式,並不是說哪一種更好,而是要看看自己具體的場景,選擇最合適的解決方案,萬能藥是不存在的,每一種藥都有其用武之地、其擅長的領域。
歡迎加入Java高級架構學習交流羣:375989619
我們提供免費的架構資料 以及免費的解答
不懂得問題都可以來問我們老師,之後還會有職業生涯規劃,以及面試指導
我們每天晚上八點也有公開課免費學習:
10年架構師分享經驗,Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式、高併發等架構技術
加羣條件:
1、具有1-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的。
2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的。
3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的。
4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的。
5. 羣號:375989619高級架構羣備註好信息!