簡介
在我的心中,JDK有兩個經典版本,第一個就是現在大部分公司都在使用的JDK8,這個版本引入了Stream、lambda表示式和泛型,讓JAVA程式的編寫變得更加流暢,減少了大量的冗餘程式碼。
另外一個版本要早點,還是JAVA 1.X的時代,我們稱之為JDK1.5,這個版本引入了java.util.concurrent併發包,從此在JAVA中可以愉快的使用非同步程式設計。
雖然先JDK已經發展到了17版本,但是併發這一塊的變動並不是很大。受限於JDK要保持穩定的需求,所以concurrent併發包提供的功能並不能完全滿足某些業務場景。所以依賴於JDK的包自行研發了屬於自己的併發包。
當然,netty也不例外,一起來看看netty併發包都有那些優勢吧。
JDK非同步緣起
怎麼在java中建立一個非同步任務,或者開啟一個非同步的執行緒,每個人可能都有屬於自己的回答。
大家第一時間可能想到的是建立一個實現Runnable介面的類,然後將其封裝到Thread中執行,如下所示:
new Thread(new(RunnableTask())).start()
每次都需要new一個Thread是JDK大神們不可接受的,於是他們產生了一個將thread呼叫進行封裝的想法,而這個封裝類就叫做Executor.
Executor是一個interface,首先看一下這個interface的定義:
public interface Executor {
void execute(Runnable command);
}
介面很簡單,就是定義了一個execute方法來執行傳入的Runnable命令。
於是我們可以這樣來非同步開啟任務:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
看到這裡,聰明的小夥伴可能就要問了,好像不對呀,Executor自定義了execute介面,好像跟非同步和多執行緒並沒有太大的關係呀?
別急,因為Executor是一個介面,所以我們可以有很多實現。比如下面的直接執行Runnable,讓Runnable在當前執行緒中執行:
class DirectExecutor implements Executor {
public void execute(Runnable r) {
r.run();
}
}
又比如下面的在一個新的執行緒中執行Runnable:
class ThreadPerTaskExecutor implements Executor {
public void execute(Runnable r) {
new Thread(r).start();
}
}
又比如下面的將多個任務存放在一個Queue中,執行完一個任務再執行下一個任務的序列執行:
class SerialExecutor implements Executor {
final Queue<Runnable> tasks = new ArrayDeque<Runnable>();
final Executor executor;
Runnable active;
SerialExecutor(Executor executor) {
this.executor = executor;
}
public synchronized void execute(final Runnable r) {
tasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (active == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((active = tasks.poll()) != null) {
executor.execute(active);
}
}
}
這些Executor都非常完美。但是他們都只能提交任務,提交任務之後就什麼都不知道了。這對於好奇的寶寶們是不可忍受的,因為我們需要知道執行的結果,或者對執行任務進行管控。
於是就有了ExecutorService。ExecutorService也是一個介面,不過他提供了shutdown方法來停止接受新的任務,和isShutdown來判斷關閉的狀態。
除此之外,它還提供了單獨呼叫任務的submit方法和批量呼叫任務的invokeAll和invokeAny方法。
既然有了execute方法,submit雖然和execute方法基本上執行了相同的操作,但是在方法引數和返回值上有稍許區別。
首先是返回值,submit返回的是Future,Future表示非同步計算的結果。 它提供了檢查計算是否完成、等待其完成以及檢索計算結果的方法。 Future提供了get方法,用來獲取計算結果。但是如果呼叫get方法的同時,計算結果並沒有準備好,則會發生阻塞。
其次是submit的引數,一般來說只有Callable才會有返回值,所以我們常用的呼叫方式是這樣的:
<T> Future<T> submit(Callable<T> task);
如果我們傳入Runnable,那麼雖然也返回一個Future,但是返回的值是null:
Future<?> submit(Runnable task);
如果我又想傳入Runnable,又想Future有返回值怎麼辦呢?
古人告訴我們,魚和熊掌不可兼得!但是現在是2021年了,有些事情是可以發生改變了:
<T> Future<T> submit(Runnable task, T result);
上面我們可以傳入一個result,當Future中的任務執行完畢之後直接將result返回。
既然ExecutorService這麼強大,如何建立ExecutorService呢?
最簡單的辦法就是用new去建立對應的例項。但是這樣不夠優雅,於是JDK提供了一個Executors工具類,他提供了多種建立不同ExecutorService的靜態方法,非常好用。
netty中的Executor
為了相容JDK的併發框架,雖然netty中也有Executor,但是netty中的Executor都是從JDK的併發包中衍生出來的。
具體而言,netty中的Executor叫做EventExecutor,他繼承自EventExecutorGroup:
public interface EventExecutor extends EventExecutorGroup
而EventExecutorGroup又繼承自JDK的ScheduledExecutorService:
public interface EventExecutorGroup extends ScheduledExecutorService, Iterable<EventExecutor>
為什麼叫做Group呢?這個Group的意思是它裡面包含了一個EventExecutor的集合。這些結合中的EventExecutor通過Iterable的next方法來進行遍歷的。
這也就是為什麼EventExecutorGroup同時繼承了Iterable類。
然後netty中的其他具體Executor的實現再在EventExecutor的基礎之上進行擴充套件。從而得到了netty自己的EventExecutor實現。
Future的困境和netty的實現
那麼JDK中的Future會有什麼問題呢?前面我們也提到了JDK中的Future雖然儲存了計算結果,但是我們要獲取的時候還是需要通過呼叫get方法來獲取。
但是如果當前計算結果還沒出來的話,get方法會造成當前執行緒的阻塞。
別怕,這個問題在netty中被解決了。
先看下netty中Future的定義:
public interface Future<V> extends java.util.concurrent.Future<V>
可以看到netty中的Future是繼承自JDK的Future。同時新增了addListener和removeListener,以及sync和await方法。
先講一下sync和await方法,兩者都是等待Future執行結束。不同之處在於,如果在執行過程中,如果future失敗了,則會丟擲異常。而await方法不會。
那麼如果不想同步呼叫Future的get方法來獲得計算結果。則可以給Future新增listener。
這樣當Future執行結束之後,會自動通知listener中的方法,從而實現非同步通知的效果,其使用程式碼如下:
EventExecutorGroup group = new DefaultEventExecutorGroup(4); // 4 threads
Future<?> f = group.submit(new Runnable() { ... });
f.addListener(new FutureListener<?> {
public void operationComplete(Future<?> f) {
..
}
});
還有一個問題,每次我們提交任務的時候,都需要建立一個EventExecutorGroup,有沒有不需要建立就可以提交任務的方法呢?
有的!
netty為那些沒有時間建立新的EventExecutorGroup的同志們,特意建立一個全域性的GlobalEventExecutor,這是可以直接使用的:
GlobalEventExecutor.INSTANCE.execute(new Runnable() { ... });
GlobalEventExecutor是一個單執行緒的任務執行器,每隔一秒鐘回去檢測有沒有新的任務,有的話就提交到executor執行。
總結
netty為JDK的併發包提供了非常有用的擴充套件。大家可以直接使用。
本文已收錄於 http://www.flydean.com/46-netty-future-executor/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!