项目织机
為什么為什么?
Java 8流背后的驅動程序之一是并發編程。 在流管道中,指定要完成的工作,然后任務將自動分發到可用處理器上:
var result = myData.parallelStream().map(someBusyOperation).reduce(someAssociativeBinOp).orElse(someDefault);當數據結構便宜且可拆分為多個部分且操作使處理器繁忙時,并行流將發揮出色的作用。 這就是它的設計目的。
但是,如果您的工作負載包含大部分阻塞的任務,那么這對您沒有幫助。 那是您的典型Web應用程序,可以處理許多請求,每個請求都花費大量時間等待REST服務,數據庫查詢等結果。
1998年,令人驚奇的是,Sun Java Web Server(Tomcat的前身)在單獨的線程而不是OS進程中運行了每個請求。 這樣就可以滿足數千個并發請求! 如今,這并不令人驚訝。 每個線程占用大量內存,典型服務器上不能擁有數百萬個線程。
這就是為什么服務器端編程的現代口號是:“永不阻塞!” 相反,您指定一旦數據可用就應該發生什么。
這種異步編程風格非常適合服務器,使它們可以輕松支持數百萬個并發請求。 對于程序員來說不是那么好。
這是使用HttpClient API的異步請求:
HttpClient.newBuilder().build().sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenAccept(response -> . . .);.thenApply(. . .);.exceptionally(. . .);我們通常用語句實現的功能現在被編碼為方法調用。 如果我們喜歡這種編程風格,就不會在Lisp中使用我們的編程語言來編寫語句和編寫快樂的代碼。
諸如JavaScript和Kotlin之類的語言為我們提供了“異步”方法,在這些方法中,我們編寫語句,然后將這些語句轉換為您剛剛看到的方法調用。 很好,只不過它意味著現在有兩種方法-常規方法和轉換方法。 而且您不能混合使用它們(“紅色藥丸/藍色藥丸”的分界)。
Project Loom從Erlang和Go等語言中獲得指導,在這些語言中,阻塞并不是什么大問題。 您可以在“光纖”或“輕型線程”或“虛擬線程”中運行任務。 該名稱尚待討論,但我更喜歡“光纖”,因為它很好地表示了多個光纖在一個載波線程中執行的事實。 當發生阻塞操作(例如等待鎖定或I / O)時,光纖將停放。 停車比較便宜。 如果很多時候都停放了一根承載線,則可以支撐一千根光纖。
請記住,Project Loom不能解決所有并發問題。 如果您有大量計算任務并且想讓所有處理器內核都忙,它對您無濟于事。 它對于使用單個線程的用戶界面沒有幫助(用于序列化對不是線程安全的數據結構的訪問)。 在該用例中繼續使用AsyncTask / SwingWorker / JavaFX Task 。 當您有很多任務花費大量時間阻塞時,Project Loom很有用。
注意 如果您已經存在很長時間了,您可能還記得早期的Java版本具有映射到OS線程的“綠色線程”。 但是,有一個關鍵的區別。 當綠色線程被阻塞時,其承載線程也被阻塞,從而阻止了同一承載線程上的所有其他綠色線程取得進展。
踢輪胎
在這一點上,Project Loom仍處于探索階段。 API會不斷變化,因此在假期過后嘗試使用該代碼時,請準備好適應最新的API版本。
您可以從http://jdk.java.net/loom/下載Project Loom的二進制文件,但是它們很少更新。 但是,在Linux機器或VM上,自己構建最新版本很容易:
git clone https://github.com/openjdk/loom cd loom git checkout fibers sh configure make images根據您已經安裝的內容, configure可能會失敗一些,但是消息會告訴您需要安裝哪些軟件包才能繼續進行。
在API的當前版本中,光纖或現在稱為虛擬線程的虛擬線程表示為Thread類的對象。 這是三種生產纖維的方法。 首先,有一個新的工廠方法可以構造OS線程或虛擬線程:
Thread thread = Thread.newThread(taskname, Thread.VIRTUAL, runnable);如果您需要更多自定義,則有一個構建器API:
Thread thread = Thread.builder().name(taskname).virtual().priority(Thread.MAX_PRIORITY).task(runnable).build();但是,一段時間以來,手動創建線程一直被認為是較差的做法,因此您可能不應該執行任何一種操作。 而是將執行程序與線程工廠一起使用:
ThreadFactory factory = Thread.builder().virtual().factory(); ExecutorService exec = Executors.newFixedThreadPool(NTASKS, factory);現在,熟悉的固定線程池將以與以往相同的方式從工廠調度虛擬線程。 當然,還將有OS級別的載體線程來運行這些虛擬線程,但這是虛擬線程實現的內部。
固定線程池將限制并發虛擬線程的總數。 默認情況下,從虛擬線程到載體線程的映射是通過使用系統屬性jdk.defaultScheduler.parallelism或默認情況下Runtime.getRuntime().availableProcessors()所給定數量的內核的jdk.defaultScheduler.parallelism池完成的。 您可以在線程工廠中提供自己的調度程序:
factory = Thread.builder().virtual().scheduler(myExecutor).factory();我不知道這是否是人們想要做的。 為什么載具線程多于核心?
返回我們的執行人服務。 您可以在虛擬線程上執行任務,就像在OS級線程上執行任務時一樣:
for (int i = 1; i <= NTASKS; i++) {String taskname = "task-" + i;exec.submit(() -> run(taskname)); } exec.shutdown(); exec.awaitTermination(delay, TimeUnit.MILLISECONDS);作為一個簡單的測試,我們可以在每個任務中入睡。
public static int DELAY = 10_000;public static void run(Object obj) {try {Thread.sleep((int) (DELAY * Math.random()));} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println(obj);}如果現在將NTASKS設置為1_000_000并在工廠生成器中.virtual() ,則該程序將失敗,并顯示內存不足錯誤。 一百萬個OS級線程占用大量內存。 但是使用虛擬線程,它可以工作。
至少,它應該可以工作,并且對我之前的Loom版本確實有效。 不幸的是,在12月5日下載的構建中,我得到了一個核心轉儲。 當我嘗試使用Loom時,這時有發生。 希望它會在您嘗試時解決。
現在,您可以嘗試更復雜的事情了。 亨氏·卡布茲(Heinz Kabutz)最近為益智游戲提供了一個程序,該程序可加載數千個Dilbert卡通圖像。 對于每個日歷日,都有一個頁面,例如https://dilbert.com/strip/2011-06-05 。 程序讀取這些頁面,在每個頁面中找到卡通圖像的URL,然后加載每個圖像。 這是一堆亂七八糟的期貨 ,有點像:
CompletableFuture.completedFuture(getUrlForDate(date)).thenComposeAsync(this::readPage, executor).thenApply(this::getImageUrl).thenComposeAsync(this::readPage).thenAccept(this::process);使用光纖,代碼更加清晰:
exec.submit(() -> { String page = new String(readPage(getUrlForDate(date)));byte[] image = readPage(getImageUrl(page));process(image); });當然,每個對readPage的調用readPage塊,但是對于纖維,我們不在乎。
嘗試一下您關心的事情。 閱讀大量網頁,進行處理,進行更多的阻塞讀取,并享受光纖阻塞便宜的事實。
結構化的一致性
Project Loom的最初動機是實現光纖,但今年早些時候,該項目開始了針對結構化并發的實驗性API。 在這篇強烈推薦的文章 (從中拍攝以下圖像)中,Nathaniel Smith提出了結構化的并發形式。 這是他的中心論點。 在新線程中啟動任務實際上并不比使用GOTO編程好,即有害:
new Thread(runnable).start();當多個線程在沒有協調的情況下運行時,這將是意大利面條代碼。 在1960年代,結構化編程將goto替換為分支,循環和函數:
現在,結構化并發的時機已經到來。 啟動并發任務時,通過閱讀程序文本,我們應該知道它們何時全部完成。
這樣,我們可以控制任務使用的資源。
到2019年夏季,Project Loom有了一個用于表達結構化并發的API。 不幸的是,由于最近進行了統一線程和光纖API的實驗,該API目前處于混亂狀態,但是您可以通過http://jdk.java.net/loom/上的原型進行嘗試。
在這里,我們安排了許多任務:
FiberScope scope = FiberScope.open(); for (int i = 0; i < NTASKS; i++) {scope.schedule(() -> run(i)); } scope.close();調用scope.close()阻塞,直到所有光纖完成。 請記住,光纖阻塞不是問題。 一旦關閉示波器,您就可以確定光纖已經完成。
FiberScope是可FiberScope的,因此您可以使用try -with-resources語句:
try (var scope = FiberScope.open()) {... }但是,如果其中一項任務永遠無法完成怎么辦?
您可以使用截止日期( Instant )或超時( Duration )創建范圍:
try (var scope = FiberScope.open(Instant.now().plusSeconds(30))) {for (...)scope.schedule(...); }截止期限/超時之前尚未完成的所有光纖都將被取消。 怎么樣? 繼續閱讀。
消除
取消一直是Java的痛苦。 按照慣例,您可以通過中斷線程來取消線程。 如果線程正在阻塞,則阻塞操作以InterruptedException終止。 否則,設置中斷狀態標志。 正確地進行檢查是乏味的。 可以重置中斷狀態,或者InterruptedException是已檢查的異常,這沒有幫助。
java.util.concurrent中取消的處理一直不一致。 考慮ExecutorService.invokeAny 。 如果有任務產生結果,則其他任務將被取消。 但是CompletableFuture.anyOf允許所有任務運行完成,即使其結果將被忽略。
2019年夏季的Project Loom API解決了取消問題。 在該版本中,光纖具有cancel操作,類似于interrupt ,但是取消是不可撤銷的。 如果當前光纖已被取消,則靜態Fiber.cancelled方法將返回true 。
當示波器超時時,其光纖將被取消。
取消可以由FiberScope構造函數中的以下選項控制。
- CANCEL_AT_CLOSE :關閉范圍取消所有計劃的光纖而不是阻塞
- PROPAGATE_CANCEL :如果取消擁有光纖,則任何新調度的光纖都會自動取消
- IGNORE_CANCEL :無法取消預定的光纖
所有這些選項都未在頂層設置。 PROPAGATE_CANCEL和IGNORE_CANCEL選項是從父范圍繼承的。
如您所見,有相當多的可調整性。 我們必須看看重新考慮此問題后會發生什么。 對于結構化并發,當示波器超時或被強制關閉時,必須自動取消示波器中的所有光纖。
螺紋局部
讓我感到驚訝的是,Project Loom實現者的痛苦之一是ThreadLocal變量,以及更深奧的東西-上下文類加載器AccessControlContext 。 我不知道有那么多東西騎在線程上。
如果您的數據結構不適合并發訪問,則有時可以在每個線程中使用一個實例。 經典示例是SimpleDateFormat 。 當然,您可以繼續構造新的格式化程序對象,但這并不高效。 所以你想分享一個。 但是全球
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");將無法正常工作。 如果兩個線程同時訪問它,則格式可能會混亂。
因此,每個線程中有一個是有意義的:
public static final ThreadLocal<SimpleDateFormat> dateFormat= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));要訪問實際的格式化程序,請致電
String dateStamp = dateFormat.get().format(new Date());首次調用get時,將調用構造函數中的lambda。 從那時起,get方法返回屬于當前線程的實例。
對于線程,這是公認的做法。 但是,如果真的有一百萬個光纖,您是否真的想擁有一百萬個實例?
這對我來說不是問題,因為使用線程安全的東西(如java.time格式化程序)似乎更容易。 但是Project Loom一直在考慮“范圍本地”對象-那些FiberScope被重新激活了。
在線程與處理器數量一樣多的情況下,線程局部變量也已被用作處理器局部性的近似值。 可以實際模擬用戶意圖的API可以支持此功能。
項目狀況
想要使用Project Loom的開發人員自然會沉迷于API,如您所見,該API尚未解決。 但是,許多實施工作都處于幕后。
一個關鍵部分是在操作阻塞時使光纖停放。 已經完成了網絡連接,因此您可以在光纖內連接到網站,數據庫等。 當前不支持本地文件操作塊時的停車。
實際上,在JDK 11、12和13中已經重新實現了這些庫,這是對頻繁發布實用程序的致敬。
目前尚不支持在監視器上進行阻塞( synchronized塊和方法),但最終需要這樣做。 ReentrantLock現在可以了。
如果光纖以本機方法阻塞,則將“固定”線程,并且所有光纖都不會前進。 Project Loom對此無能為力。
Method.invoke需要更多工作才能得到支持。
有關調試和監視支持的工作正在進行中。
如前所述,穩定性仍然是一個問題。
最重要的是,性能還有一段路要走。 停放光纖不是免費的午餐。 每次都需要替換運行時堆棧的一部分。
在所有這些方面都取得了很大的進展,所以讓我們回顧一下開發人員關心的API。 現在是查看Project Loom并考慮如何使用它的好時機。
同一類代表線和纖維對您有價值嗎? 還是您希望將某些Thread行李丟掉? 您是否認同結構化并發的承諾?
試一下Project Loom,看看它如何與您的應用程序和框架一起工作,并為無畏的開發團隊提供反饋!
翻譯自: https://www.javacodegeeks.com/2019/12/project-loom.html
總結
- 上一篇: 安保备案登记表怎么填(安保备案登记)
- 下一篇: 摇篮配置