前文回顧
在《用好組合索引,性能提升10倍不止!》一文中,我們主要使用了CountDownLatch這個類來優化程序的性能,在文末提出了一個思考題:其實,上面的代碼不是允許的,你有更好的優化方法嗎? 很多小伙伴的私信其實或多或少的說出了一些方案,但是沒說到真正的點子上。
這里,再向小伙伴們提出一個疑問:如果我們不使用CountDownLatch和CompletableFuture,讓你對前文的程序進行優化,你有思路嗎?
其實思路也很簡單:蕞直接的方式就是創建一個計數器,將計數器的初始值設置為2,當子線程1執行完hasNoOrders = getHasNoOrders(); 這行代碼時,將計數器的值減1,當子線程2執行完 hasNoStock = getHasNoStock(); 這行代碼時,將計數器的值減1。在主線程中,等待計數器的值減為0,然后執行后續的業務操作。
CountDownLatch類的總體思路也是這樣,小伙伴們可以根據這個思路自行實現程序性能的優化,我就不再這里絮叨啦。
能否進一步優化?
我們先來看看之前程序的優化效果圖。
通過仔細的分析,我們就會發現:雖然getHasNoOrders()和getHasNoStock()這兩個方法實現了并行操作,但是getHasNoOrders()方法和getHasNoStock()方法和checkData()方法與saveCheckResult()方法之間還是串行的,如果能夠讓他們之間的操作并行化,那么系統的性能就可以得到進一步提升了。如下圖所示。
如何實現上圖所示的優化呢?接下來,我們先說說進一步優化的總體思路。
進一步優化思路
查詢未校對的訂單方法getHasNoOrders()和查詢未校對的庫存方法getHasNoStock()能夠并行執行,校對數據的方法 checkData()還要依賴getHasNoOrders()方法和getHasNoStock()方法的結果,很明顯可以使用CompletableFuture來優化,那除了CompletableFuture還有其他的方式嗎?今天,我們先不講CompletableFuture,先來看看其他的優化方式。
大家認真思考下,上述的場景中,一個方法的執行需要等待另外兩個方法的執行結果,是不是有點生產者-消費者的意思呢?
有些小伙伴可能會說:這哪是生產者和消費者模型啊?我們仔細想一下:兩次查詢未校對的數據就是生產者,校對數據的操作是消費者。
我們可以使用隊列來保存生產者生產的數據,而消費者就從這個隊列中消費數據。
由于查詢未校對的訂單方法getHasNoOrders()和查詢未校對的庫存方法getHasNoStock()是在兩個不同的線程中執行的,這里,在具體實現時,我們可以使用兩個隊列分別保存未校對的訂單數據和未校對的庫存數據,校對數據的操作每次從隊列1中取出未校對的訂單數據,從隊列2中取出未校對的庫存數據,然后再執行數據的校對操作。
接下來,我們再思考一個問題:就是如何使用兩個隊列實現完全的并行化。
一個簡單的方案就是在線程1中執行查詢未校對訂單的數據,在線程2中執行查詢未校對庫存的數據,當線程1和線程2分別生產完一條數據時,通知線程3執行數據的校對操作。這里,有個關鍵的點就是線程1和線程2的執行步調要一致,不能一個線程執行的太快,一個線程執行的太慢。
很顯然,線程1和線程2之間會存在相互等待的現象,說到這里,小伙伴們是不是就有解決方案啦?
我們先來說說優化的總體思路吧: 首先,進一步優化存在兩個難點:一個是線程1和線程2執行的步調要一致,另外就是線程1和線程2中每次方法執行完畢后,要通知線程3執行數據校對操作。
我們也可以使用計數器的方式實現,計數器的初始值為2,線程1執行完getHasNoOrders()方法時,對計數器減1,線程2執行完getHasNoStock()方法時,對計數器減1。如果計數器的值大于0時,則線程1等待或者線程2等待。如果計數器的值等于0,則通知線程3執行數據校對操作,并重新喚醒等待中的線程1或者線程2。同時,需要我們將計數器的值重新設置為2,以此往復實現程序的優化效果。
有小伙伴可能會說:這也太麻煩了吧!哈哈,自己實現確實挺麻煩的,不過Java并發類庫中為我們準備好了一個實現上述場景的類——沒錯,可以使用Java并發類庫中的 CyclicBarrier 類實現。
使用CyclicBarrier進一步優化
使用CyclicBarrier進一步優化的具體方案就是:首先創建一個計數器初始值為2的CyclicBarrier對象,在構造方法中傳入一個回調函數,在回調函數中執行數據的校對操作,當計數器的值減為0時,就會執行這個回調函數。
在線程1中執行完getHasNoOrders()方法并將結果放入隊列1后,執行barrier.await()將計數器減1,同時等待計數器的值減為0。在線程2中執行完getHasNoStock()方法并將結果放入隊列2后,執行barrier.await()將計數器減1,同時等待計數器的值減為0。
當計數器的值減為0時,線程1和線程2繼續向下執行,同時會調用回調函數來執行數據的校對操作。
不僅如此,CyclicBarrier類還能夠自動重置計數器的值,當計數器的值減為0時,它又會被自動重置為初始值,這個功能使用起來也很方便。
接下來,我們看一下使用CyclicBarrier類優化后的核心偽代碼,如下所示。
`// 訂單隊列
Vector<Order> orderQueue;
// 庫存隊列
Vector<Stock> stockQueue;
//創建查詢未校對訂單和未校對庫存的線程池
Executor executor = Executors.newFixedThreadPool(2);
//執行數據校對的線程池
Executor checkExecutor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier =
new CyclicBarrier(2, ()->{
executor.execute(() -> checkDataAndSaveResult());
});
void checkDataAndSaveResult(){
Order o = orderQueue.remove(0);
Stock s = stockQueue.remove(0);
//校對數據并返回結果
checkResult = checkData(o, a);
//將結果信息保存到數據校對信息表中
saveCheckResult(checkResult);
}
void checkAllOrdersAndStock(){
//檢測是否存在未對賬訂單
checkOrders = checkOrders();
while(checkOrders != null){
executor.execute(()->{
//查詢未校對的訂單信息
hasNoOrders = getHasNoOrders();
orderQueue.add(hasNoOrders);
barrier.await();
});
executor.execute(()->{
//查詢未校對的庫存記錄
hasNoStock = getHasNoStock();
stockQueue.add(hasNoStock);
barrier.await();
});
}
}
`
至此,整個程序的優化操作就完成了。
總結
在整個程序的優化過程中,我們開始使用了CountDownLatch優化程序,后面又使用了CyclicBarrier優化程序。它兩個的區別就是:
好了,今天就到這兒吧,我們下期見~~