ava使用的是系統級線程,也就是說,每次調用new Thread(....).run(),都會在系統層面建立一個新的線程,然鵝新建線程的開銷是很大的(每個線程默認情況下會占用1MB的內存空間,當然你願意的話可以用-Xss來調小點),更不要說線程切換帶來的開銷了
為了節省開銷,程序員玩出了很多花樣。
最常用的是線程池(線程復用,但是完全無法處理阻塞調用的問題)
以及事件驅動框架(NIO或者Netty,用少量的工作線程來服務大量的慢速IO連接,但是EventLoop中也不能有阻塞調用,耗時的邏輯必須放在額外的線程池里處理)
但是NIO的代碼難寫也難懂,像我這種懶惰的程序猴子,最喜歡的還是一個線程對應一個連接這種簡單粗暴的編程手法。
纖程(Coroutine)是我們的救星
所謂的纖程,或者協程,可以理解為是一種輕量級的線程,它與線程的主要區別在於
a. 線程切換的過程是由系統內核完成,切換的過程中會進入到內核態。而纖程則完全工作在用戶態。
b. 線程是否發生切換是由操作系統決定的(搶占式調度),工作線程本身沒有決定權。而纖程的切換是需要工作纖程主動放棄CPU,這樣調度器才能讓另外一個纖程繼續運行。
很多語言已經內置了纖程,最著名的應該就是Go了,用go關鍵字,就能直接創建一個纖程並在其中為所欲為,其他的Scheduler會自動幫你搞定。所以Go能相對容易的寫出正確的高並發程序。
可惜的是,Java沒有官方的纖程支持,好在有個叫做Quasar的庫可堪一用
使用這個lib,你就能在Java程序中創建纖程了,代碼大概長這個樣子:
public static void main(String[] args) throws ExecutionException, InterruptedException, SuspendExecution { int FiberNumber = 1_000_000; CountDownLatch latch = new CountDownLatch(1); AtomicInteger counter = new AtomicInteger(0); for (int i = 0; i < FiberNumber; i++) { new Fiber(() -> { counter.incrementAndGet(); if (counter.get() == FiberNumber) { System.out.println("done"); } Strand.sleep(1000000); }).start(); } latch.await(); }
在上面這段代碼中,我們直接創建了一百萬個纖程,如果是一般的Thread,不考慮OS能否負擔得起,單單占用的內存就要1T起步。
但是這段程序實際占用的內存只在1G出頭,也就是說每個纖程的內存占用只在1K左右。
這是如何做到的?
Quasar在編譯時會對代碼進行掃描,如果方法帶有Suspendable注解,或者拋出SuspendExecution,或者在配置文件中被指定,Quasar會直接修改生成的字節碼,在park方法的前后,插入一些字節碼。
這些字節碼會記錄此時纖程的執行狀態(相關的局部變量與操作數棧),然后通過拋出異常的方式將CPU的控制權從當前協程交回到控制器
此時控制器可以再次調度另外一個纖程運行,並通過之前插入的那些字節碼恢復當前纖程的執行狀態,使程序能繼續正常執行。
並且,這些操作是非常輕量的,所以內存消耗極小,也不會對CPU帶來太多的額外開銷(據說在3%-5%)