java協程框架quasar和kotlin中的協程對比分析
前言
早就聽說Go語言開發的服務不用任何架構優化,就可以輕松實現百萬級別的qps。這得益於Go語言級別的協程的處理效率。協程不同於線程,線程是操作系統級別的資源,創建線程,調度線程,銷毀線程都是重量級別的操作。而且線程的資源有限,在java中大量的不加限制的創建線程非常容易將系統搞垮。接下來要分享的這個開源項目,正是解決瞭在java中隻能使用多線程模型開發高並發應用的窘境,使得java也能像Go語言那樣使用協程的語義開發瞭。
- quasar項目地址:https://github.com/puniverse/quasar
- quasar周邊項目地址:https://github.com/puniverse/comsat
快速體驗
添加依賴
<dependency> <groupId>co.paralleluniverse</groupId> <artifactId>quasar-core</artifactId> <version>0.7.10</version> </dependency>
註意:目前quasar最高的版本是0.8.0,但是最高版本的隻支持jdk11以上
添加java agent
quasar的實現原理是在java加載class前,通過jdk的instrument機制使用asm來修改目標class的字節碼來實現的,他標記瞭協程代碼的起始和結束的位置,以及方法需要暫停的位置,每個協程任務統一由FiberScheduler去調度,內部維護瞭一個或多個ForkJoinPool實例。所以,在運行應用前,需要配置好quasar-core的java agent地址,在vm參數上加上如下腳本即可:
-javaagent:D:\.m2\repository\co\paralleluniverse\quasar-core\0.7.10\quasar-core-0.7.10.jar
線程VS協程
下面模擬調用某個遠程的服務,假設遠程服務處理耗時需要1S,這裡使用執行阻塞1S來模擬,分別看多線程模型和協程模型調用這個服務10000次所需的耗時
協程代碼
public static void main(String[] args) throws Exception{ CountDownLatch count = new CountDownLatch(10000); StopWatch stopWatch = new StopWatch();stopWatch.start(); IntStream.range(0,10000).forEach(i-> new Fiber() { @Override protected String run() throws SuspendExecution, InterruptedException { Strand.sleep(1000 ); count.countDown(); return "aa"; } }.start()); count.await();stopWatch.stop(); System.out.println("結束瞭: " + stopWatch.prettyPrint()); }
耗時情況:
多線程代碼
public static void main(String[] args) throws Exception{ CountDownLatch count = new CountDownLatch(10000); StopWatch stopWatch = new StopWatch();stopWatch.start(); ExecutorService executorService = Executors.newCachedThreadPool(); IntStream.range(0,10000).forEach(i-> executorService.submit(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException ex) { } count.countDown(); })); count.await();stopWatch.stop(); System.out.println("結束瞭: " + stopWatch.prettyPrint()); }
耗時情況
協程完勝
可以看到上面的結果,在對比訪問一個耗時1s的服務10000次時,協程隻需要2秒多,而多線程模型需要4秒多,時效相差瞭一倍。而且上面多線程編程時,並沒有指定線程池的大小,在實際開發中是絕不允許的。一般我們會設置一個固定大小的線程池,因為線程資源是寶貴,線程多瞭費內存還會帶來線程切換的開銷。上面的場景在設置200個固定大小線程池時。結果也是可預見的達到瞭50多秒。這個結果足以證明協程編程ko線程編程瞭。而且在qps越大時,線程處理的效率和協程的差距就約明顯,縮小差距的唯一方式就是增加線程數,而這帶來的影響就是內存消耗激增。而反觀協程,基於固定的幾個線程調度,可以輕松實現百萬級的協程處理,而且內存穩穩的。
後記
最後,博主以為Quasar隻是一個框架層面的東西,所以就又去看瞭下同樣是jvm語言的kotlin的協程。他的語言更簡潔,可以直接和java混合使用。跑上面這種實例隻需要1秒多。
fun main() { val count = CountDownLatch(10000) val stopWatch = StopWatch() stopWatch.start() IntStream.range(0,10000).forEach { GlobalScope.launch { delay(1000L) println(Thread.currentThread().name + "->"+ it) count.countDown() } } count.await() stopWatch.stop() println("結束瞭: " + stopWatch.prettyPrint()) }
當博主看到這個結果的時候,有種震驚的趕腳,kotlin的同步模型牛逼呀,瞬時感覺到發現瞭java裡的騷操作瞭,可以使用kotlin的協程來代替java中的多線程操作。因為他們兩個混合開發毫無壓力。如果行的通,那就太爽瞭。所以就有下面這個kotlin協程實現的代碼:
@Service class KotlinAsyncService(private val weatherService: GetWeatherService,private val demoApplication: DemoApplication){ val weatherUrl = "http://localhost:8080/demo/mockWeatherApi?city=" fun getHuNanWeather(): JSONObject{ val result = JSONObject() val count = CountDownLatch(demoApplication.weatherContext.size) for (city in demoApplication.weatherContext){ val url = weatherUrl + city.key GlobalScope.launch { result[city.key.toString()] = weatherService.get(url) count.countDown() } } count.await() return result } }
現實是,當我使用協程替換掉我java多線程寫的一個多線程匯聚多個http接口的結果的接口時,通過ab壓測他們兩個的性能並沒有很大的變化,最後瞭解到主要原因是這個時候,在協程裡發起一個http的請求時,涉及到操作系統層面的socket io操作,io操作是阻塞的,協程的並發也就變成瞭調度協程的幾個線程的並發瞭。而且當我把同樣的代碼放到Quasar中的時候,Quasar直接拋io異常瞭,說明Quasar還並不能輕松支持這個場景。那為什麼上面的測試結果差距這麼大呢,是因為我錯誤的把協程實現裡的阻塞等同於線程的阻塞。協程裡的delay掛起函數,會立馬釋放線程到線程池,但是當真正的io阻塞的時候也就和真正的線程sleep一樣瞭,並沒有釋放當前的線程。所以這些對比都沒有太大的意義
以上就是java協程框架quasar和kotlin中的協程對比分析的詳細內容,更多關於java框架quasar和kotlin協程對比的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Java多線程同步工具類CountDownLatch詳解
- Java並發包工具類CountDownLatch的應用詳解
- Java並發編程之常用的輔助類詳解
- java並發包中CountDownLatch和線程池的使用詳解
- java多線程CyclicBarrier的使用案例,讓線程起步走