OCHaCafe Season 6 #6 で使用した資料です。
Javaで最強!コンカレントプログラミングOracle Cloud Hangout Café – Season 6 #6Shuhei KawamuraCloud Solution EngineerSolutions Architect, Oracle Corporation JapanFebruary 8, 2022
View Slide
Copyright © 2023, Oracle and/or its affiliates2• 所属• 日本オラクル株式会社• ソリューション・アーキテクト本部• 担当領域• Cloud Native, App Dev• Data Platform(Spark 周り)• AI Services• Search Service with OpenSearch• (趣味) 認証・認可関連• コミュニティ• OCHaCafe• CloudNative Days – Observability川村 修平 (Shuhei Kawamura)@shukawamTwitter/GitHub/Qiita
Copyright © 2023, Oracle and/or its affiliates3Agenda1. Warming up2. Virtual Threads Overview3. Helidon & Helidon Níma
Copyright © 2023, Oracle and/or its affiliates4Warming Up
Copyright © 2023, Oracle and/or its affiliates5java.lang.Thread を継承して、run() メソッドをオーバーライドする実行は、Thread.start() で行うJava でスレッドを扱う方法アレコレ 1/3java.lang.Thread, java.lang.Runnablepublic class SimpleThread extends Thread {@Overridepublic void run() {System.out.println(String.format("[%s]: %s", Thread.currentThread().getName(), "Hello world!"));}}SimpleThread thread = new SimpleThread();thread.start();
Copyright © 2023, Oracle and/or its affiliates6java.lang.Thread のコンストラクタに java.lang.Runnable インターフェースの実装を渡すもちろん、ラムダ式でもOKJava でスレッドを扱う方法アレコレ 2/3java.lang.Thread, java.lang.Runnablepublic class SimpleRunnable implements Runnable {@Overridepublic void run() {System.out.println(String.format("[%s]: %s", Thread.currentThread().getName(), "Hello world!"));}}public static void main(String[] args) {System.out.println(String.format("[%s]: %s", Thread.currentThread().getName(), "Hello world!"));Thread thread = new Thread(() -> {System.out.println(String.format("[%s]: %s", Thread.currentThread().getName(), "Hello world!"));});thread.start();}Thread thread = new Thread(new SimpleRunnable());thread.start();
Copyright © 2023, Oracle and/or its affiliates7• Concurrency Utilities: 並行処理でよく使われるユーティリティ・パッケージ• スレッドプール(※後述)によるスレッド管理や Callable/Future などの非同期処理の結果を取得する仕組み等を提供• java.lang.Thread をそのまま用いるよりも安全かつ抽象度の高い実装が可能となるので、通常はこちらを使うJava でスレッドを扱う方法アレコレ 3/3java.util.concurrent.*ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();Future result = singleThreadPool.submit(new SimpleCallableTask());System.out.println(result.get());singleThreadPool.shutdown();if (!singleThreadPool.awaitTermination(1, TimeUnit.SECONDS)) {singleThreadPool.shutdownNow();}public class SomeCallableTask implements Callable {@Overridepublic String call() throws Exception {return "Hello world!";}}
Copyright © 2023, Oracle and/or its affiliates8複数のスレッドをあらかじめ作成して待機させておくタスクが来たら待っているスレッドにタスクを割り当てて処理を開始させる実行させるスレッドに空きがない場合は、タスクをキューに入れてスレッドに空きができるまで待つjava.util.concurrent.ExecutorService というインターフェースを持った様々なスレッドプールの仕組みを提供している• AbstractExecutorService, ForkJoinPool, ScheduledThreadPoolExecutor, ThreadPoolExecutor通常は Executors というユーティリティクラスを使って ExecutorService インターフェースのインスタンスを取得する• Executors.newSingleThreadExecutor()• Executors.newCachedThreadPool()• Executors.newFixedThreadPool(int nThreads)• Executors.newScheduledThreadPool(int corePoolSize)• etc.スレッドプール複数のタスクを同時実行する際に、複数のスレッドを効率よく使う仕組み
Copyright © 2023, Oracle and/or its affiliates9• work-stealing アルゴリズムを実装したスレッドプール• work-stealing: 自分のタスクが終了したワーカースレッドが別のスレッドのタスクキューからタスクを盗む(steal)ようなスケジューリング方式• スレッドが遊んでいる時間を極力減らし、並行処理の密度をあげることを狙いとしている参考:ForkJoinPooltasksub-task 1sub-task 2 sub-task 3・・・・・・resultforkjoinワーカースレッド ワーカースレッドforkjoinstealタスクキュー タスクキュータスクキューが空なら別のワーカーのタスクキューからタスクを盗む(steal)work-stealing の概要イメージ Fork/Join の概要イメージ
Copyright © 2023, Oracle and/or its affiliates101. java.lang.Thread, java.lang.Runnable を使う2. java.util.concurrent.* を使うDemo1: Java でスレッドを扱う
Copyright © 2023, Oracle and/or its affiliates11• Java 5 で追加された Concurrency Utilities を拡張する形で策定• Java/Jakarta EE のコンテナがスレッドを含む各種リソースを管理する関係上、アプリケーションから Thread,Concurrency Utilities を直接使うことは非推奨• → 生成するスレッドを Java/Jakarta EE コンテナ、アプリケーションサーバーの管理下にするために拡張• Java/Jakarta EE アプリケーション実行環境から非同期に複数の処理を並行実行することを可能にする機能• Java SE で利用できる ExecutorService, ScheduledExecutorService を拡張したスレッド管理機能を提供• ManagedExecutorService: 標準的なスレッドプールを提供• ManagedScheduledExecutorService: タスクの繰り返しやタイマー実行が可能なスレッドプールを提供• ManagedThreadFactory: コンテナ管理用のスレッドを生成するファクトリー• ContextService: 並行タスクにコンテナの情報を設定するためのユーティリティ参考:https://jakarta.ee/specifications/concurrency/2.0/concurrency-spec-2.0.html#goals-of-this-specificationJakarta Concurrency/Concurrency Utilities for Java EE
Copyright © 2023, Oracle and/or its affiliates12初期の Solaris における Java のスレッドの仕組み• 複数の JVM スレッドが 1 OS スレッドに紐づく• = グリーンスレッド• JVM スレッド:OS スレッド= M:1 の関係• シングルコアの時代にマルチスレッドを扱うための仕組みJava 18 までの Java のスレッドの仕組み• 1 つの JVM スレッドが 1 OS スレッドに対して厳密に紐づく• = プラットフォームスレッド• JVM スレッド:OS スレッド= 1:1 の関係Java のスレッド事情 – ざっくり歴史OS スレッドJVM スレッド ・・・JVM スレッドOS スレッド・・・OS スレッドJVM スレッド JVM スレッド11M1
Copyright © 2023, Oracle and/or its affiliates13• (初期の Solaris は厳密には違うが)1 つの Java スレッドは、1 つの OS スレッドの薄いラッパー• OS スレッドの生成は、CPU、メモリ、等のコストが高い• スレッド間の切り替え(= コンテキスト・スイッチ)もコストが高い• 利用可能なスレッドの数を制限している• 事前に一定数作成したスレッドを効率的に使いまわすための仕組み(= スレッドプール)を用いてきたJava でスレッドを扱う際の問題点(~Java 18)とその対応
Copyright © 2023, Oracle and/or its affiliates14Virtual Threads Overview
Copyright © 2023, Oracle and/or its affiliates15JEP(JDK Enhancement-Proposal)• JDK に対する重要な変更を設計および実装するための提案Projects• 大きなインパクトのある機能については、JEP だけではなく OpenJDK のサブプロジェクトを発足して開発を行う• Project Amber, Loom, Panama, Valhalla, etc.前提知識
Copyright © 2023, Oracle and/or its affiliates16Project Loom• Java プラットフォーム上で動作し、使いやすくスループットの高い軽量並行処理と新しいプログラミングモデルを実現することを目的としたプロジェクト• 新しい構成要素を追加することで対応• 仮想スレッド ← 今回のテーマ• 限定継続• 末尾呼出しの削除Project Loom
Copyright © 2023, Oracle and/or its affiliates17• JVM 上に仮想的なスレッド(Virtual Threads)を導入すること• Java でも 言語のスレッド:OS のスレッド = M(:N):N な関係を作れるようにする!• 他の言語でいうところの Fiber, Coroutine, SemiCoroutine, …• java.lang.Thread の API を使用する既存のコードに対して、最小限の変更で仮想スレッドを利用できるようにすること• 既存の JDK ツールを用いたトラブルシューティング、デバッグ、プロファイリングは容易に行えるようにVirtual Threads – JEP 436導入の目的 & ゴールOS スレッド・・・OS スレッドJVM スレッド JVM スレッドOS スレッド・・・OS スレッドJVM スレッド JVM スレッドJVMスレッドJVMスレッド・・・ JVMスレッドJVMスレッド・・・11MNN~ Java 18 までの Java のスレッドの仕組み JEP 425 で追加される仮想スレッドの仕組み
Copyright © 2023, Oracle and/or its affiliates18使い方• プレビュー版の API なので、コンパイルや実行時に --enable-preview のフラグが必要• e.g. java --source 19 --enable-preview Main.java, jshell --enable-preview• 仮想スレッドの作り方 → 通常は、java.util.concurrent.Executors に新しく追加された API を用いるVirtual Threads – JEP 436Virtual Threads の使い方ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();// orExecutorService executor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory());
Copyright © 2023, Oracle and/or its affiliates19{n ∈ N | n ≤ 10,000} に対して、スリープ(1,000 ms)を挟みながら FizzBuzz 問題*を行う*FizzBuzz 問題:• n が 3 の倍数 → Fizz を出力• n が 5 の倍数 → Buzz を出力Demo2: Virtual Threads を実際に使ってみるvs. Platform Threadpublic static void main(String[] args) {long start = System.currentTimeMillis();ThreadFactory virtualThreadFactory = Thread.ofVirtual().factory();createThreadPerExecFizzBuzz(virtualThreadFactory);long end = System.currentTimeMillis();logger.info("Execution time: " + Long.toString(end - start) + "[ms]");}public static void createThreadPerExecFizzBuzz(ThreadFactory factory) {FizzBuzzTask task = new FizzBuzzTask();try (ExecutorService exector = Executors.newThreadPerTaskExecutor(factory)) {IntStream.rangeClosed(1, 10_000).forEach(i -> {exector.submit(() -> {task.execWithSleep(i, 1_000);Utils.log(i + “: ” + task.exec(i)); // スレッド名と併せてログ出力});});}}public static void main(String[] args) {long start = System.currentTimeMillis();ThreadFactory platformThreadFactory = Thread.ofPlatform().factory();createThreadPerExecFizzBuzz(platformThreadFactory);long end = System.currentTimeMillis();logger.info("Execution time: " + Long.toString(end - start) + "[ms]");}public static void createThreadPerExecFizzBuzz(ThreadFactory factory) {FizzBuzzTask task = new FizzBuzzTask();try (ExecutorService exector = Executors.newThreadPerTaskExecutor(factory)) {IntStream.rangeClosed(1, 10_000).forEach(i -> {exector.submit(() -> {task.execWithSleep(i, 1_000);Utils.log(i + “: ” + task.exec(i)); // スレッド名と併せてログ出力});});}}Virtual Thread Platform Thread
Copyright © 2023, Oracle and/or its affiliates20• .oO そういえば、 java.util.concurrent.Executors には、固定長のスレッドプール(Single/FixedThreadPool)を作る API や上限(Integer.MAX_VALUE)を設けたうえで、キャッシュしながら必要な数分のスレッドを作るスレッドプール(CachedThreadPool)が提供されているが、それらを使うと何か問題になるのだろうか…?• 仮想スレッドはプールせずに使うこと!• 生成コストが高価な OS のスレッドを必要以上に作成しないために、今まではスレッドプールを使っていた• そもそも仮想スレッドの生成は高価ではないので、プールする必要がない• プールする代わりに、スレッドの生成に制約を設けないようなモデル(= タスクの実行毎に仮想スレッドを作成するモデル)を採用• Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory())• Executors.newVirtualThreadTaskExecutor()• リソースへのアクセス数を制限したい場合は、java.util.concurrent.Semaphore を使用するVirtual Threads – JEP 436仮想スレッドはプールせずに使用する実現するための API が追加されています!
Copyright © 2023, Oracle and/or its affiliates21• 従来 JDK が提供してきたデバッグや監視用途のツールも同様に使用可能• Debugger• 所謂、デバッガー• 仮想スレッドのステップ実行やコールスタックの表示、スタックフレーム内の変数の検査が可能• JFR(JDK Flight Recorder)• 実行中の Java アプリケーションに関する診断およびプロファイリングのデータを低オーバーヘッドで収集するツール• アプリケーションコードからのイベント(オブジェクトの割り当て、I/O 操作、etc.)を正しく仮想スレッドに割り当て可能• スレッドダンプ(プロセスの一部となっているすべてのスレッドの状態のスナップショット)• jcmd や jstack で取得可能• スレッドの数が莫大に増えることを考慮し、従来のフラットな構造に加え、JSON 形式の出力をサポートVirtual Threads – JEP 436仮想スレッドの監視仮想スレッドは、JDK に実装されており、特定の OS スレッドに紐づけられていないため、監視の際は OS レベルの監視ツールではなく、上記のようなツールを用いることが重要!
Copyright © 2023, Oracle and/or its affiliates221. Debugger• 仮想スレッドのステップ実行やコールスタックの表示、スタックフレーム内の変数の検査をしてみる2. JFR, JDK Mission Control を用いてメトリクスを分析してみる3. jcmd を用いてスレッドダンプを確認してみるDemo3: Virtual Threads の監視Debugger/JFR(JDK Flight Recoder) + JDK Mission Control/スレッドダンプpublic static void createThreadPerExecFizzBuzz(ThreadFactory factory) {FizzBuzzTask task = new FizzBuzzTask();try (ExecutorService exector = Executors.newThreadPerTaskExecutor(factory)) {IntStream.rangeClosed(1, 10_000).forEach(i -> {exector.submit(() -> {task.execWithSleep(i, 1_000);Utils.log(i + “: ” + task.exec(i));});});}}デバッグ実行$ jcmd JFR.start duration=1m filename=oochacafe.jfr$ jcmd Thread.dump_to_file -format=json ochacafe.jsonJMC
Copyright © 2023, Oracle and/or its affiliates23• スレッドを有効的に使うためには、スケジューリングが必要• プラットフォームスレッド(OS スレッドの薄いラッパー)• OS 内のスケジューラーによってスケジューリングされる• 仮想スレッド• JDK 独自のスケジューラー(work-stearing ForkJoinPool)によってスケジューリングされる• 仮想スレッドをプラットフォームスレッドに割り当てた後、OS 内のスケジューラーによってスケジューリングされる• 仮想スレッドは、ライフサイクル中で異なるキャリアにスケジューリングされる可能性があるVirtual Threads – JEP 436仮想スレッドのスケジューリングプラットフォームスレッド 1仮想スレッドタスクマウントプラットフォームスレッド 1仮想スレッドタスクプラットフォームスレッド 1仮想スレッドタスクアンマウントプラットフォームスレッド 2仮想スレッドタスクマウントブロック操作完了準備OKブロック操作
Copyright © 2023, Oracle and/or its affiliates24• キャリアと仮想スレッドのスタックトレースは別もの• 仮想スレッドで投げられた例外は、キャリアのスタックフレームを含まない(※逆も同様)• スレッドダンプでは、仮想スレッドのスタックにキャリアのスタックフレームは表示されない(※逆も同様)• キャリアのスレッドローカル変数は、仮想スレッドでは使用できない• スレッドローカル変数の使用は慎重に検討する• → 膨大な数のスレッドで実行されるときのメモリのフットプリントを考慮することVirtual Threads – JEP 436仮想スレッドを使用する上での諸注意
Copyright © 2023, Oracle and/or its affiliates25• 同時実行のタスク数が多い(数千以上)• 作業負荷が CPU バウンドではないこと• 長い時間計算を行うような場合には、効果がない• e.g. 巨大な配列をソートする、etc.• 高速化(低レイテンシー)ではなく、高スループットを実現するための技術Virtual Threads – JEP 436仮想スレッドが有効な場面
Copyright © 2023, Oracle and/or its affiliates26• 計算処理の時間 << 通信、DB アクセス等の I/O 処理にかかる時間(待ち時間含む)• I/O 処理の待ち時間に別の処理を行いたい…e.g. 一般的なサーバーサイドアプリケーションclient trip-manager hotel-booking flight-bookingPOST /tripPOST /hotelbook()book()POST /flight待ち待ち待ち待ち仮想スレッドが有効的に使えるのでは?
Copyright © 2023, Oracle and/or its affiliates27• Java でも OS のスレッド:言語のスレッド = M(:N):N な関係が作れるようになる• 2023 年 2 月現在は、プレビュー機能なので、使用する際は --enable-preview のフラグが必要• 順調にいけば、次の LTS である Java 21 に導入か…?• 高速化(低レイテンシー)ではなく、高スループットを実現するための技術• Java を使う開発者にとっては、慣れ親しんだ方法で仮想スレッドを使うことができる• スレッドの作成、JDK ツールを用いた監視、etc.• 仮想スレッドを使う場合、今までのスレッドに対する常識は注意が必要なものも…• スレッドプール、(場合によっては)スレッドローカル変数、etc.ここまでのまとめ
Copyright © 2023, Oracle and/or its affiliates28Helidon & Helidon Níma
Copyright © 2023, Oracle and/or its affiliates29Oracle がホストする OSS プロジェクト• GitHub でソースコードを公開:https://github.com/oracle/helidon• Helidon の商用サポートは WebLogic Server/Coherence/Verrazzano のサポート契約に含まれるマイクロサービスアプリケーションが必要とする機能を提供する Java ライブラリの集合体• 単体の JVM として動作し、アプリケーションサーバ不要、容易なコンテナ化• 必要なコンポーネントを追加して拡張することも可能マイクロサービスの開発・運用を支援する機能を提供• OpenMetrics(監視)、OpenTracing(追跡)、OpenAPI(API 公開)• 耐障害性/回復性: ヘルスチェック、サーキット・ブレーカ2つのプログラミングモデルを提供• Helidon MP:宣言的記法(Java/Jakarta EE 開発者フレンドリー)• Helidon SE:関数的型記法Project Helidonクラウドネイティブなマイクロサービスを開発するための Java アプリケーションフレームワーク
Copyright © 2023, Oracle and/or its affiliates30Helidon SE と MP• Eclipse MicroProfile 準拠• 軽量フットプリント• 宣言型• Java EE サブセット +マイクロサービス関連機能• マイクロ・フレームワーク• 超軽量フットプリント• 関数型• Reactive, non-blockingHelidon MP Helidon SE≒ + EclipseMicroProfile+ 拡張機能フットプリント重視Polyglot 言語開発者向け機能性・互換性重視Java/Jakarta EE 開発経験者向け
Copyright © 2023, Oracle and/or its affiliates31• Helidon はライブラリをパッケージングした Java SE アプリケーションとして実行される(= Executable JAR)• Helidon アプリケーションは個々の JVM 上で実行• 複数のパッケージング・オプション、Docker にも対応 – ビルド・ツールを使って簡単に作成多彩なパッケージング方法を提供Executable JAR Jlink ランタイム・イメージ GraalVM ネイティブ・イメージContainerApplicationJava RuntimeLinuxContainerCustomJava RuntimeLinuxContainerNative ApplicationLinuxApplication
Copyright © 2023, Oracle and/or its affiliates322018 2020 2022 2023Helidon の生い立ちとこれからHelidon 0.x – 1.x Helidon 2.x Helidon 3.x Helidon 4.x• Java 8• MicroProfile 3.2• Java 11• MicroProfile 3.3• Java 17• MicroProfile 5.0• Java 19 (Java 21)• MicroProfile Next
Copyright © 2023, Oracle and/or its affiliates33アプローチ 1 – Reactive Programming• ノンブロッキング• イベントループの使用• 少数のスレッドですべてのリクエストを処理するアプローチ 2 – Virtual Threads // NEW!!• ブロッキング• 安価にスレッド(仮想スレッド)を作成することで高いスループットを実現高スループットを実現するための “Helidon” のアプローチReactive プログラミングの詳細はこちらから
Copyright © 2023, Oracle and/or its affiliates34• https://helidon.io/nima• νήμα(nima)は、ギリシャ語で「糸、毛糸、編み糸」という意味• 仮想スレッド(Virtual Threads)ベースの Java のマイクロサービス・フレームワーク• Helidon のエコシステムにおける Netty* の置き換えがゴール• アプリケーションをブロッキングでシンプルに実装可能• ブロッキングな実装でも高いスループットが実現可能• 2023 年 2 月現在:テクノロジープレビュー版を Helidon 4.0.0-ALPHA4で公開中• https://github.com/helidon-io/helidon/tree/4.0.0-ALPHA4• 提供機能:• HTTP/1, HTTP/2, gRPC, Metrics, Health Checks, Tracing, Fault Tolerance, Testing Integration*: イベントドリブンな非同期通信を行うアプリケーションを開発するためのフレームワークHelidon NímaVirtual Threads によるブロッキング・スレッドモデルの採用
Copyright © 2023, Oracle and/or its affiliates35Níma vs. other servers0100000200000300000400000500000600000700000800000900000JSON HTTP/2 TLSRequests/secondNettyNímaHelidon MP (Níma)Dropwizardhttps://medium.com/helidon/helidon-n%C3%ADma-helidon-on-virtual-threads-130bb2ea2088
Copyright © 2023, Oracle and/or its affiliates36Helidon MP on NímaVirtual Threads ベースの Helidon によるスループットの向上050000100000150000200000250000300000350000400000JSON HTTP/2 TLSRequests/secondPlatform threadsVirtual threads
Copyright © 2023, Oracle and/or its affiliates37対象のアプリケーション:• Helidon MP on Níma• 4.0.0-ALPHA4• Helidon MP/SE• 3.1.0• Spring Boot• 3.0.2Demo4: Helidon Níma のスループットを体験してみるシンプルな文字列を返却するアプリケーションに負荷をかけてスループットを体験する--vus 50--duration 15s/cowsay/say
Copyright © 2023, Oracle and/or its affiliates38仮想スレッド(Virtual Threads)• Java でも OS のスレッド:言語のスレッド = M(:N):N な関係を作れるように• 高速化(低レイテンシー)ではなく、高スループットを実現するための技術• 慣れ親しんだ方法で仮想スレッドを使うことができるが、一部使い方には注意が必要• スレッドプール、(場合によっては)スレッドローカル変数、etc.Helidon Níma• 仮想スレッド(Virtual Threads)ベースの Java のマイクロサービス・フレームワーク• 仮想スレッドを前提とした HTTP、gRPC、WebSocket のブロッキング・モデルの実装を提供• ブロッキング・モデル実装でも高いスループットを実現まとめ
Copyright © 2023, Oracle and/or its affiliates39• JEP 436: Virtual Threads (Second Preview)• https://openjdk.org/jeps/436• Loom – Fibers, Continuations and Tail-Calls for the JVM• https://openjdk.org/projects/loom/• JavaのProject Loomと仮想スレッドの内部• https://blogs.oracle.com/oracle4engineer/post/going-inside-javas-project-loom-and-virtual-threads-ja• Helidon Níma• https://helidon.io/nima• Helidon Níma – Helidon on Virtual Thread• https://medium.com/helidon/helidon-n%C3%ADma-helidon-on-virtual-threads-130bb2ea2088• oracle-japan/ochacafe-concurrent-programming• https://github.com/oracle-japan/ochacafe-concurrent-programming参考情報
Thank youCopyright © 2023, Oracle and/or its affiliates40