2023.06.29 JJUG Java仕様勉強会資料
Javaの並列/並行処理の基本Java in the Box櫻庭 祐一
View Slide
AgendaThreadの基本Concurrency Utilities非同期タスクを記述するConcurrency Utilitiesの拡張Folk/Join FrameworkCompletableFuture
Threadの基本
Thread処理を並列/並行に処理するための最小単位基本的にはOSスレッドのラッパーただし、Virtual ThreadはJVMが管理するスレッドThreadに対する操作 OSスレッドに対する操作
Threadにおける2つの側面ライフサイクルの管理生成から廃棄までスレッドスケジューリングスレッドの実行順序実行中のスレッドの切り替え Context Switch
var thread = new Thread(new Runnable() {@Overridepublic void run() {// 処理while (!condition) {Thread.yield();}// 処理}});thread.start();スレッド生成非同期タスクスレッドを譲るただし、本当に譲るかどうかはスケジューラしだい非同期タスクが完了でスレッド廃棄タスクの実行ただし、実際の実行タイミングはスケジューラが決める
スレッドの生成時間がかかる メモリ大量消費勝手に野良スレッドを作成されると管理が難しいコンテキストスイッチ時間がかかる メモリ大量消費勝手にスレッド切替されるとスケジューリングできない非同期処理の実行・管理はJVMにまかせる開発者は非同期タスクの記述に集中する
Thread を直接使用することはもはや アンチパターン特に以下のメソッドは使用しない• stop()• suspend()• resume()これらのメソッドはJava 21から@Deprecated(forRemoval=true)
Concurrency Utilities
Concurrency Utilities非同期タスクの実行・管理並列コレクションアトミック操作 APIロック API
非同期タスクの実行・管理ExecutorServiceタスクの実行管理 主にスレッドプールExecutorsExecutorServiceのファクトリRunnable/Callable非同期タスクFuture非同期タスクの管理
ExecutorsExecutorServiceのファクトリ用途に合わせてExecutorServiceオブジェクトを生成newSingleThreadExecutornewFixedThreadPoolnewCachedThreadPoolnewScheduledThreadPoolnewWorkStealingPoolnewVirtualThreadPerTaskExecutorシングルスレッド動作のExecutorServceスレッド数固定のスレッドプール必要に応じてスレッド生成するスレッドプール周期的タスクを実行するスレッドプールWork Stealingを使用するスレッドプール (Java 8)Virtual Threadを使用するExecutorService (Java 21)
ExecutorService非同期タスクの実行タスクの実行方法/スケジューリングは実装クラスに依存主なメソッドsubmitcloseinvokeAll/invokeAnyshutdown/shutdownNow非同期タスクの登録 戻り値はFutureAutoClosable (Java 19)複数タスクの実行 今後Structured Concurrencyで置き換えJava 19以前に使用していたExecutorServiceの終了
Runnable/Callable非同期タスクを記述@FunctionalInterfaceなので、ラムダ式で記述RunnableCallable引数なし 戻り値なしのタスクChecked ExceptionはスローできないRuntimeExceptionはUncaught Exceptionとして扱われる引数なし 戻り値ありのタスクChecked Exceptionをスローできる
Future非同期タスクの管理型パラメータはタスクの戻り値型Runnableの場合、Future>getcancelisDone/isCancelledresultNowexceptionNowstate結果の取得 タスクが完了するまでブロックするタスクのキャンセル (キャンセルできるかはタスクしだい)タスクの状態を調べるブロックせずに結果取得 完了していなければ例外 (Java 19)ブロックせずに例外取得 例外で完了していなければ例外 (Java 19)タスクの状態を調べる 戻り値はFuture.State列挙型 (Java 19)
final var path = ...try (var pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())) {Future> future = pool.submit(() -> {return Files.readAllLines(path);});List contents = future.get();} catch (ExecutionException ex) {// タスク実行時の例外} catch (InterruptedException ex) {// タスク実行中に割り込み発生}例: 非同期ファイル読み込み引数でスレッド数指定ここではCPUのコア数を指定タスク登録IOExceptionが発生した場合ExecutionExceptionのcauseとなりFuture.getメソッドでcatchできるgetはタスク完了までブロックするためこのままだと並行処理の意味がない
final var path = ...try (var pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())) {Future> future = pool.submit(() -> {return Files.readAllLines(path);});/*たとえば*/ while(!future.isDone()) { /* タスク完了まで他の処理を実行 */ }List contents = future.get();} catch (ExecutionException ex) {// タスク実行時の例外} catch (InterruptedException ex) {// タスク実行中に割り込み発生}例: 非同期ファイル読み込み引数でスレッド数指定ここではCPUのコア数を指定タスク登録IOExceptionが発生した場合ExecutionExceptionのcauseとなりFuture.getメソッドでcatchできる
非同期タスクを記述する
非同期タスクのポイントいわゆるスレッドセーフ非同期実行でもデータを壊さない実行タイミングによらず結果が同一並行度と性能が比例する並行処理のボトルネックが少スケーラビリティ安全性
安全性を損なう要因競り合い状態複数スレッドが同タイミングで書き込みを行うことでデータが破壊されるデータ書き込みの非可視化メモリ書き込みタイミングにより予測不可能な動作になる
競り合い状態の例class Counter {private int count = 0;public int getNext() {return count++;}}read 複製 加算 writeThread #1read 複製 加算 writeThread #2
競り合い状態の例class Counter {private int count = 0;public int getNext() {return count++;}}read 複製 加算 writeThread #1read 複製 加算 writeThread #2複数スレッドからcountにアクセス可能アクセスを単一スレッドに制限 = 同期化
競り合い状態の解消class SyncCounter {private int count = 0;synchronized public int getNext() {return count++;}}class LockedCounter {private ReentrantLock lock= new ReentrantLock();private int count = 0;public int getNext() {lock.lock();try {return count++;} finally {lock.unlock();}}}
class OneDataContainer {private int value;synchronized public int getValue() {return value;}synchronized public void setValue(int v) {value = v;}}boolean update(int value) {if (container.getValue() == value) {return false;} else {container.setValue(value);return true;}}スレッドセーフ?CheckActCheck-Then-Actは処理全体を同期化する
class OneDataContainer {private int value;synchronized public int getValue() {return value;}synchronized public void setValue(int v) {value = v;}}synchronized boolean update(int value) {if (container.getValue() == value) {return false;} else {container.setValue(value);return true;}}スレッドセーフ?synchronizedメソッドはsynchronized(this) { … }containerが逸脱している場合スレッドセーフではない
class OneDataContainer {private int value;synchronized public int getValue() {return value;}synchronized public void setValue(int v) {value = v;}}boolean update(int value) {synchronized(container) {if (container.getValue() == value) {return false;} else {container.setValue(value);return true;}}}containerを同期化することでスレッドセーフになる
データ書き込みの非可視化の例public class NoVisibility {private static boolean ready;private static int number;public static void main(String... args) {try (var pool = Executors.newCachedThreadPool()){pool.submit(() -> {while (!ready) {Thread.yield();}System.out.println(number);});number = 42;ready = true;}}}Java並行処理プログラミングp40より引用、改変同期化により可視化を保証
同期化同期化を行うことで• リソースへのアクセスを単一スレッドに制限複数スレッドでも逐次動作になりボトルネック化• データ書き込みの可視化を保証メインメモリへのアクセスにより多大な時間を消費同期化することでスケーラビリティが低下
安全性を担保しつつスケールさせるためには同期化させる部分を最小限に安易にsynchronizedメソッドを定義しないsynchronizedよりもReentrantLockなどのロックAPIを使用スレッド間でのリソースを共有しないリソースを共有するため競り合い状態などが発生する引数、戻り値だけでスレッド間のやり取りを行う状態を変更しない状態が変更できるために競り合い状態などが発生する状態を変更できないイミュータブル性を重視Java 16で導入されたRecord型を活用
スケーラビリティ向上させるためにタスクの独立性タスク間のやり取りやリソース共有を排除するタスクの均質化同じタスクの並行処理は効率的同じタスクでなくても、なるべく同質な処理にするタスク粒度の最適化タスクの粒度が大きいとタスクスケジューリングが難しい計算処理であれば細粒度 (分割統治法)
Concurrency Utilitiesの拡張
Concurrency Utilitiesの拡張Fork/Join Framework (Java 7)大量の計算処理、データ処理に対する非同期処理応答性の向上CompletableFuture (Java 8)I/Oを含む業務ロジックの非同期処理スループットの向上
Fork/Join Framework (Java 7)大量の計算処理、データ処理を効率的に処理するフレームワークキーとなる技術分割統治法Work-StealingタスクスケジューリングParallel Stream、Arrays.sortなどで使用オーバーヘッドがあるため、データ数が多い場合のみArrays.sortの場合、デフォルトで4096以上開発者が直接使うことはほぼないはず…
分割統治法大きいタスクを処理しやすいサイズまで分割して処理例: ソート1 245 8 101131 245 8 101131 245 8 10113 4 1015 11 2832 1015 11 4838 1152 4 1031
Work-Stealingタスクスケジューリング個々のスレッドがタスクキューを持つタスクキューにタスクあり先頭から取り出して処理タスクキューにタスクなし他スレッドのタスクキューの末尾からタスクを取り出す(Steal)分割したタスクはキューに積まれる末尾に近いほどタスクが小さい効率的にタスクを処理できる
Fork/Join FrameworkのAPIForkJoinPoolWork-Stealを使用したスレッドプールForkJoinTask分割統治法を使用してタスクを記述する基底クラスRecursiveTask/RecursiveAction再帰的タスクを記述用クラス RecursiveActionは戻り値なしCountedCompleter保留中のタスクがない場合に完了アクションを記述できるタスク
例: フィボナッチ数 F(n) = F(n-1) + F(n-2)class FibonacciTask extends RecursiveTask {private final int n;public FibonacciTask(int n) { this.n = n; }protected Integer compute() {if (n <= 1) return n;var f1 = new FibonacciTask(n - 1);f1.fork();var f2 = new FibonacciTask(n - 2);return f2.compute() + f1.join();}}ForkJoinPool fjPool = new ForkJoinPool();var task = fjPool.submit(new FibonacciTask(30));System.out.println(task.get());タスク分割タスク分割タスクのフォークタスクの完了を待って、結果を取得タスク記述メソッド
CompletableFuture (Java 8)非同期で行う一連の処理を関数で連ねるI/O処理を非同期で行うことでスループット向上開始 (staticメソッド)completedFuturerunAsyncsuplyAsync一連のタスク記述メソッドthenAccept/Apply/Run(Async)他に処理の合成、例外処理など多くのメソッドを提供引数が次のラムダ式の引数になるタスクはRunnableタスクはSupplier引数、戻り値の有無でラムダ式が決まるメソッドの最後がAsyncの場合、非同期に実行される
CompletableFuture.supplyAsync(() -> Path.of(...)).thenApplyAsync(path -> {try {return Optional.of(Files.readAllLines(path));} catch (IOException ex) {return Optional.>empty();}}).thenAccept(opt -> {opt.ifPresent(contents -> {contents.forEach(System.out::println);});});例: 非同期ファイル読み込み引数あり、戻り値ありのタスクを非同期に実行ファイル読み込みタスク完了後、実行
ConclusionThreadを直接使用するのはもはやアンチパターンConcurrency Utilitiesでスレッド管理とタスクを分離安全性とスケーラビリティに注意してタスクを記述応答性向上: Fork/Join Frameworkスループット向上: CompletableFuture
おまけJavaで並列・並行処理を行うのであれば絶版だけど…電子版もないけど…