文章目錄
併發:功能強大而簡單的抽象,讓編寫正確的併發代碼更加容易。
ListenableFuture:完成後回調的Future
Service:啓動和關閉的服務,爲你處理複雜的狀態邏輯。
1.ListenableFuture
併發是一個困難的問題,但是通過使用功能強大且簡單的抽象可以大大簡化併發。爲了簡化問題,Guava使用ListenableFuture
擴展了JDK的Future
接口。
我們強烈建議你在所有代碼中始終使用ListenableFuture
而不是Future
,因爲:
- 大多數
Futures
方法都需要它。 - 比以後更改爲
ListenableFuture
更容易。 - 工具方法的提供者無需提供其方法的
Future
和ListenableFuture
變體。
1.1接口
傳統的Future
表示異步計算的結果:可能已經或可能尚未完成產生結果的計算。Future
可以作爲正在進行的計算的句柄,是服務向我們提供結果的承諾。
ListenableFuture
允許你在計算完成後或在計算已經完成時立即註冊要執行的回調。這個簡單的附加功能使它可以有效地支持基本Future
接口無法支持的許多操作。
ListenableFuture
添加的基本操作是addListener(Runnable, Executor)
,它指定當此Future
表示的計算完成時,指定的Runnable
將在指定的Executor
上運行。
1.2添加回調
大多數用戶更喜歡使用Futures.addCallback(ListenableFuture, FutureCallback, Executor)
。FutureCallback
實現兩種方法:
onSuccess(V)
,如果Future成功,則根據其結果執行的操作onFailure(Throwable)
,如果Future失敗,則根據失敗執行的操作
1.3創建
對應於JDK的ExecutorService.submit(Callable)
方法來啓動異步計算,Guava提供了ListeningExecutorService
接口,該接口在ExecutorService
返回正常Future
的任何地方都返回ListenableFuture
。要將ExecutorService
轉換爲ListeningExecutorService
,只需使用MoreExecutors.listeningDecorator(ExecutorService)
。
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
ListenableFuture<Explosion> explosion = service.submit(
new Callable<Explosion>() {
public Explosion call() {
return pushBigRedButton();
}
});
Futures.addCallback(
explosion,
new FutureCallback<Explosion>() {
// we want this handler to run immediately after we push the big red button!
public void onSuccess(Explosion explosion) {
walkAwayFrom(explosion);
}
public void onFailure(Throwable thrown) {
battleArchNemesis(); // escaped the explosion!
}
},
service);
另外,如果你要從基於FutureTask
的API進行轉換,則Guava提供了ListenableFutureTask.create(Callable)
和ListenableFutureTask.create(Runnable, V)
。與JDK不同,ListenableFutureTask
不能直接擴展。
如果你更喜歡抽象的方式設置future值,而不是實現一種計算該值的方法,請考慮擴展AbstractFuture
或直接使用SettableFuture
。
如果必須將另一個API提供的Future
轉換爲ListenableFuture
,則別無選擇,只能使用重量級的JdkFutureAdapters.listenInPoolThread(Future)
將Future
轉換爲ListenableFuture
。只要有可能,最好修改原始代碼以返回ListenableFuture
。
1.4應用
使用ListenableFuture
的最重要原因是可以擁有複雜的異步操作鏈。
ListenableFuture<RowKey> rowKeyFuture = indexService.lookUp(query);
AsyncFunction<RowKey, QueryResult> queryFunction =
new AsyncFunction<RowKey, QueryResult>() {
public ListenableFuture<QueryResult> apply(RowKey rowKey) {
return dataService.read(rowKey);
}
};
ListenableFuture<QueryResult> queryFuture =
Futures.transformAsync(rowKeyFuture, queryFunction, queryExecutor);
ListenableFuture
可以有效地支持許多其他操作,而單獨的Future
不能支持。不同的執行者可以執行不同的操作,並且單個ListenableFuture
可以有多個操作在等待它。
當多個操作應該在另一個操作啓動時立即開始時——“扇出”——ListenableFuture
只起作用:它觸發所有請求的回調。稍微多做一些工作,我們可以“扇入”或觸發一個ListenableFuture
,以便在其他幾個future都完成後立即進行計算:有關示例,請參見Futures.allAsList
的實現。
方法 | 描述 | 參見 |
---|---|---|
transformAsync(ListenableFuture, AsyncFunction, Executor) * |
返回一個新的ListenableFuture ,其結果是將給定的AsyncFunction 應用於給定ListenableFuture 的結果的產物。 |
transformAsync(ListenableFuture, AsyncFunction) |
transform(ListenableFuture, Function, Executor) |
返回一個新的ListenableFuture ,其結果是將給定的Function 應用於給定ListenableFuture 的結果的產物。 |
transform(ListenableFuture, Function) |
allAsList(Iterable>) |
返回一個ListenableFuture ,其值是按順序包含每個輸入future的值的列表。如果任何一個輸入future失敗或被取消,則該future失敗或被取消。 |
allAsList(ListenableFuture...) |
successfulAsList(Iterable>) |
返回一個ListenableFuture ,其值是按順序包含每個成功輸入future的值的列表。與失敗或取消的future相對應的值將替換爲null 。 |
successfulAsList(ListenableFuture...) |
*
AsyncFunction<A, B>
提供一個方法ListenableFuture<B> apply(A input)
。它可以用於異步轉換值。
List<ListenableFuture<QueryResult>> queries;
// The queries go to all different data centers, but we want to wait until they're all done or failed.
ListenableFuture<List<QueryResult>> successfulQueries = Futures.successfulAsList(queries);
Futures.addCallback(successfulQueries, callbackOnSuccessfulQueries);
1.5避免嵌套Future
在代碼調用通用接口並返回Future
的情況下,可能會以嵌套的Future
s結尾。例如:
executorService.submit(new Callable<ListenableFuture<Foo>() {
@Override
public ListenableFuture<Foo> call() {
return otherExecutorService.submit(otherCallable);
}
});
將返回一個ListenableFuture<ListenableFuture<Foo>>
。這段代碼是不正確的,因爲如果外部future的取消與外部future的完成進行競爭,則該取消將不會傳播到內部future。使用get()
或監聽器檢查另一個future是否失敗也是常見的錯誤,但是除非特別小心,否則從otherCallable
拋出的異常將被抑制。爲了避免這種情況,Guava的所有future處理方法(以及JDK中的某些方法)都具有*Async
版本,可以安全地解開此嵌套——transform(ListenableFuture, Function, Executor)
、transformAsync(ListenableFuture, AsyncFunction, Executor)
、ExecutorService.submit(Callable)
和submitAsync(AsyncCallable, Executor)
等等。
2.Service
Guava Service
接口表示一個具有操作狀態的對象,並帶有啓動和停止的方法。例如,Web服務器,RPC服務器和計時器可以實現Service
接口。管理這些服務的狀態(需要適當的啓動和關閉管理)並非易事,特別是在涉及多線程或日程調度schedule的情況下。Guava提供了一些框架來爲你管理狀態邏輯和同步細節。
2.1使用Service
服務Service
的正常生命週期是
Service.State.NEW
到Service.State.STARTING
到Service.State.RUNNING
到Service.State.STOPPING
到Service.State.TERMINATED
已停止的服務無法重新啓動。如果服務在啓動、運行或停止的地方失敗,它將進入Service.State.FAILED
狀態。
如果服務是NEW
,則可以使用startAsync()
異步啓動服務。因此,你應該將應用程序結構化爲在每個服務啓動時都有唯一的位置(統一)。
使用異步stopAsync()
方法來停止服務也是類似的。但是與startAsync()
不同,多次調用此方法是安全的。這使得處理關閉服務時可能發生的競爭成爲可能。
服務還提供了幾種方法來等待服務轉換完成。
- 異步使用
addListener()
。addListener()
允許你添加一個Service.Listener
,它將在服務的每個狀態轉換時調用。注意:如果在添加監聽器時服務不是NEW
新建的,那麼任何已經發生的狀態轉換都不會在監聽器上重新觸發。 - 同步使用
awaitRunning()
。這是不中斷的,不會拋出已檢查的異常,並在服務啓動完成後返回。如果服務啓動失敗,則會拋出IllegalStateException
。同樣,awaitTerminated()
等待服務達到終端狀態(TERMINATED
或FAILED
)。兩種方法都具有重載的允許指定超時時間。
Service
接口是微妙而複雜的。我們不建議直接實現它。相反,請使用guava中的抽象基類之一作爲實現的基礎。每個基類都支持特定的線程模型。
2.2實現
2.2.1AbstractIdleService
AbstractIdleService
框架實現了Service
,該服務在處於“運行”狀態時不需要執行任何操作——因此在運行時不需要線程——但具有要執行的啓動和關閉操作。實現這樣的服務與擴展AbstractIdleService
以及實現startUp()
和shutDown()
方法一樣容易。
protected void startUp() {
servlets.add(new GcStatsServlet());
}
protected void shutDown() {}
請注意,對GcStatsServlet
的任何查詢都已經有一個在運行的線程。在服務運行時,我們不需要該服務自行執行任何操作。
2.2.2AbstractExecutionThreadService
AbstractExecutionThreadService
在單個線程中執行啓動、運行和關閉操作。你必須重寫run()
方法,並且它必須響應停止請求。例如,你可以在工作循環中執行操作:
public void run() {
while (isRunning()) {
// perform a unit of work
}
}
或者,你可以以任何方式重寫,從而使run()
返回。
重寫startUp()
和shutDown()
是可選的,但是將爲你管理服務狀態。
protected void startUp() {
dispatcher.listenForConnections(port, queue);
}
protected void run() {
Connection connection;
while ((connection = queue.take() != POISON)) {
process(connection);
}
}
protected void triggerShutdown() {
dispatcher.stopListeningForConnections(queue);
queue.put(POISON);
}
請注意,start()
調用你的startUp()
方法,爲你創建一個線程,並在該線程中調用run()
。stop()
調用triggerShutdown()
方法並等待線程死亡。
2.2.3AbstractScheduledService
AbstractScheduledService
在運行時執行一些週期性任務。子類實現runOneIteration()
來指定任務的一次迭代,以及熟悉的startUp()
和shutDown()
方法。
要描述執行日程調度schedule,你必須實現scheduler()
方法。通常,你將使用AbstractScheduledService.Scheduler
提供的日程schedule之一,newFixedRateSchedule(initialDelay, delay, TimeUnit)
或newFixedDelaySchedule(initialDelay, delay, TimeUnit)
,與ScheduledExecutorService
中熟悉的方法相對應。可以使用CustomScheduler
來實現自定義日程調度schedule;有關詳細信息,請參見Javadoc。
2.2.4AbstractService
當你需要執行自己的手動線程管理時,請直接重寫AbstractService
。通常,上述實現之一應該可以爲你提供良好的服務,但是當你在建模某種提供自己的線程語義作爲Service
時,建議你實現AbstractService
,因爲你有自己特定的線程需求。
要實現AbstractService
,必須實現2個方法。
doStart()
:doStart()
是第一次調用startAsync()
直接調用的,你的doStart()
方法應執行所有的初始化,如果啓動成功,則最終調用notifyStarted()
,如果啓動失敗,則最終調用notifyFailed()
。doStop()
:doStop()
是由第一次調用stopAsync()
直接調用的,你的doStop()
方法應關閉服務,如果關閉成功,則最終調用notifyStopped()
,如果關閉失敗,則最終調用notifyFailed()
。
你的doStart
和doStop
方法應該是快速的。如果你需要進行昂貴的初始化,例如讀取文件、打開網絡連接或任何可能阻塞的操作,則應考慮將該工作移至另一個線程。
2.3使用ServiceManager
除了Service
框架實現之外,Guava還提供了ServiceManager
類,它使涉及多個服務實現的某些操作更加容易。使用Services
集合創建一個新的ServiceManager
。然後,你可以管理它們:
startAsync()
將啓動管理下的所有服務。與Service#startAsync()
類似,如果所有服務都是NEW
,則只能調用此方法一次。stopAsync()
將停止管理下的所有服務。addListener
將添加一個ServiceManager.Listener
,它將在主要狀態轉換時調用。awaitHealthy()
將等待所有服務達到RUNNING
狀態。awaitStopped()
將等待所有服務達到終端狀態。
或檢查它們:
- 如果所有服務都是
RUNNING
,則isHealthy()
返回true。 servicesByState()
返回按狀態索引的所有服務的一致快照。startupTimes()
返回管理下的Service
到該服務啓動所需的時間(以毫秒爲單位)的映射。返回的映射保證按啓動時間排序。
雖然建議通過ServiceManager
管理服務生命週期,但是通過其他機制啓動的狀態轉換不會影響其方法的正確性。例如,如果服務是由startAsync()
之外的某種機制啓動的,則監聽器將在適當的時候被調用,而awaitHealthy()
仍將按預期工作。ServiceManager
強制執行的唯一要求是,在構造ServiceManager
時,所有Service
都必須是NEW
。
本文參考:
ListenableFutureExplained
ServiceExplained
guava-tests-concurrent