#chiroito ’s blog

Java を中心とした趣味の技術について

Java 24のVirtual Threadでsynchronizedの注意点はずっと昔と同じになっただけ

Java 24で導入された、JEP 491: Synchronize Virtual Threads without Pinningwithout Pinningです!ここが重要です。

3行で

  • Java 24からでもVirtualThread上でsynchronizedを使ってもいいわけではなく、これまでどおりできるだけ使わない。
  • JEP 491はPlatform Threadがsynchronizedを含むVirtual Threadによって占有されることを防ぎ、ほかのVirtual Threadを処理できるようにする。
  • 複数のVirtual Threadで同じオブジェクトをモニタ(≒ロック)しているsynchronized 処理はこれまで通りロックの取り合いで止まる。

背景

Java 24は、JEP 491: Synchronize Virtual Threads without Pinningが導入されました。 Java 24がリリースされた日にJavaOneがやっていたのですが、そこでの質問や会話、SNSなどでこのJEPによってsynchronizedを使っても良いと理解している人が多いなと思うことがありました。 おおくの人がVirtual Threadに注目・期待していることもあり、これは危険だなと思ったので、実際の振る舞いも紹介します。

synchronizedの振る舞いについて

Virtual Threadが登場する前の並列処理

まずはじめに、昔からある普通のスレッドでsynchronized を使うとどうなるかを見てみます。Synchronized Taskは、synchronizedの中でブロッキング処理をするタスクです。この例では、Task 1 と Task 2 が同じオブジェクトでロックするとします。これらを2つのスレッドでそれぞれ実行してみます。

すると、先に実行したTask 1がロックを取得し、後から実行したタスク 2はTask 1がロックを解除するまでロック待ちになります。点線のある位置で、Task 2 がロックを取得すると、同様に処理を行います。

Java 23 までのVirtual Thread

Java 23 までは、 Virtual Thread 上で synchronizedを使うと、そのVirtual Threadは別のVirtual ThreadにPlatform Threadを譲りません。synchronizedの中でブロッキング処理をすると、実際に処理しているPlatform Threadをブロックします。 そのため、このPlatform Threadはその間に他のVirtualThreadを処理できません。

詳細を知りたい方はJEP 491: Synchronize Virtual Threads without PinningVirtual threads are pinned in synchronized methodsThe reason for pinningをごらん下さい。

実際に2つの例を見てみます。 1つ目は同じPlatform Thread上に、同じオブジェクトをロックするタスクがVirtual Threadとして連続で来た例です。 2つ目は異なるPlatform Thread 上に、同じオブジェクトをロックするタスクがVirtual Threadとして同じタイミングで来た例です。

これらの例を図解します。 図は、Platform Thread からの視点と、各Virtual Treadからの視点の両方をまとめて記載しています。

1つ目の例は、2つのタスクがそれぞれ処理されます。 synchronizedなのでブロッキングしていても他の Virtual Thread に処理を受け渡さないため、タスクがそれぞれ順番に処理されます。 順番に処理されるのでロック待ちはありません。 これらのタスクが終わるまで、他の種類のタスクも同様に待たされます。

2つ目の例は、2つのタスクが異なるPlatform Threadで処理されます。 これは、普通のスレッドで処理される場合と同じで、ロックを取れなかったタスクはロックを取得するまで待ちます。 Virtual Threadはsynchronizedの間、Platform Threadを占有し続けるため、ほかのVirtual Threadは待ち続けます。 また、ロック待ちをしているほうの Virtual Thread では、Platform Thread がロック待しています。 そのため、そのPlatform Threadは、ほかのVirtual Threadを処理する期間が短くなります。 並列度が上がるとロック待ちはどんどん長くなっていくため、そうなるとPlatform ThreadはVirtual Threadを処理できなくなります。

Java 24 からのVirtual Thread

Java 24は JEP 491: Synchronize Virtual Threads without Pinning が導入されました。 これはVirtual Threadが synchronized 内でブロッキング処理をしても、Platform Threadを他のVirtual Threadに譲ります。 (重要)しかし、 synchronizedのロックの動きはこれまで通り有効で、Virtual Thread 内では、ロック待ちが行われます。

検証

次のソースコードは、先ほどまで図に記載していたものです。 synchronizedの中でブロッキング処理をするタスクと、ただログを出力するタスクがあります。 synchronizedの中でブロッキング処理をするタスクは、SynchronizedTask です。 ログを出力するタスクは、NoSynchronizedTaskです。

ブロッキング処理はスレッドを3秒間スリープさせます。

このコードは、SynchronizedTask を 2つ、NoSynchronizedTask を 1 つ、この順番で実行します。

package org.example;

import jdk.jfr.Configuration;
import jdk.jfr.Recording;

import java.nio.file.Paths;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

// java -Djdk.virtualThreadScheduler.maxPoolSize=1 org.example.Main
public class Main {
    public static void main(String[] args) throws Exception {
        try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor()) {
            executorService.submit(new SynchronizedTask());
            executorService.submit(new SynchronizedTask());
            executorService.submit(new NoSynchronizedTask());

            System.out.println("Main finished putting tasks");
        }
    }
}

class SynchronizedTask implements Runnable {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
    private static final Object monitorObject = new Object();

    @Override
    public void run() {
        System.out.println("Synchronized Task start at " + formatter.format(LocalTime.now()) + " @" + Thread.currentThread());
        synchronized (monitorObject) {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("Synchronized Task end at " + formatter.format(LocalTime.now()) + " @" + Thread.currentThread());
    }
}

class NoSynchronizedTask implements Runnable {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    @Override
    public void run() {
        System.out.println("No Synchronized Task run at " + formatter.format(LocalTime.now()) + " @" + Thread.currentThread());
    }
}

今回は、Platform Threadが詰まるのを容易に再現するため、Platform Threadを1つにします。これは、JVM起動引数に-Djdk.virtualThreadScheduler.maxPoolSize=1を付与します。

Java 23の結果

Java 23 では、 SynchronizedTask 同士が順番に処理されています。 1つ目のタスクが11:58:57に開始し11:59:00に終わってから、同時刻に2つ目のタスクが開始し、11:59:03に終わります。 NoSynchronizedTaskは、2つ目のタスクが終わった11:59:03に実行されてます。

java-23/bin/java.exe -Djdk.virtualThreadScheduler.maxPoolSize=1 org.example.Main
Main finished putting tasks
Synchronized Task start at 11:58:57 @VirtualThread[#38]/runnable@ForkJoinPool-1-worker-1
Synchronized Task end at 11:59:00 @VirtualThread[#38]/runnable@ForkJoinPool-1-worker-1
Synchronized Task start at 11:59:00 @VirtualThread[#40]/runnable@ForkJoinPool-1-worker-1
Synchronized Task end at 11:59:03 @VirtualThread[#40]/runnable@ForkJoinPool-1-worker-1
No Synchronized Task run at 11:59:03 @VirtualThread[#41]/runnable@ForkJoinPool-1-worker-1

Java 24の結果

Java 24 では、 すべてのタスクが12:25:38に開始しています。 1つ目の Synchronized Task は、12:25:41に終了し、2つ目は 1 つ目の終了時間から3秒後である12:25:44に終わっています。 Synchronized Task の終了を待つこと無く No Synchronized Task を実行できています。

java-24\bin\java.exe -Djdk.virtualThreadScheduler.maxPoolSize=1 org.example.Main
Main finished putting tasks
Synchronized Task start at 12:25:38 @VirtualThread[#37]/runnable@ForkJoinPool-1-worker-1
Synchronized Task start at 12:25:38 @VirtualThread[#39]/runnable@ForkJoinPool-1-worker-1
No Synchronized Task run at 12:25:38 @VirtualThread[#40]/runnable@ForkJoinPool-1-worker-1
Synchronized Task end at 12:25:41 @VirtualThread[#37]/runnable@ForkJoinPool-1-worker-1
Synchronized Task end at 12:25:44 @VirtualThread[#39]/runnable@ForkJoinPool-1-worker-1

まとめ

Java 24は、JEP 491: Synchronize Virtual Threads without Pinningが導入されました。without Pinningです。依然としてロック待ちは発生しますのでご注意ください。 Virtual Threadは、ブロッキングを意識せず、大量のタスクを実行しやすくなりました。 ですが、Virtual Thread で大量に`synchronized‘なタスクを実行すると、タイムアウトするまで無限に待たされることになるかもしれないので注意が必要です。

OpenShiftで証明書をうっかり切らしてしまったときのメモ

結論

oc login--insecure-skip-tls-verifyを付けよう。

流れ

OpenShift にログインしようとしたら、証明書の期限を過ぎているエラーが出ました。

> oc login
error: tls: failed to verify certificate: x509: certificate has expired or is not yet valid: current time 2025-03-06T09:32:11+09:00 is after 2024-12-10T06:57:42Z

とりあえず、証明書を検証しないように --insecure-skip-tls-verify を付けてログインします。

oc login --insecure-skip-tls-verify
WARNING: Using insecure TLS client config. Setting this option is not supported!

You must obtain an API token by visiting https://oauth-openshift.apps.<your-domain>/oauth/token/request

Alternatively, use "oc login --web" to login via your browser. See "oc login --help" for more information.

Tokenを取れと言われるので、ブラウザで https://oauth-openshift.apps.<your-domain>/oauth/token/request にアクセスして、再度ログインします。

oc login --token=sha256~YP4ypSQVwq6r3o6WIUupXqMEXCZq5zihYvll3PeK59U --server=https://api.<your-domain>:6443 --insecure-ski
p-tls-verify
WARNING: Using insecure TLS client config. Setting this option is not supported!

Logged into "https://api.<your-domain>:6443" as "kube:admin" using the token provided.

You have access to 83 projects, the list has been suppressed. You can list all projects with 'oc projects'

Using project "home".

これでログイン出来ました。

JVM Language Summit 2024 に行ってきた その1

JVMLSに行ってきたので、そこで新しく出てきたことだけを紹介します。

JVM Language Summit は、現在開発中の OpenJDK の機能について2.5日に濃縮して知れる一年に一回しかない機会です。 これらは、数年後にリリースされるでしょう。 これらの新しい機能について、そのコンセプトや、どのように実現するかを話されます。

場所や時期についてはこちらを参照ください。 JVM Language Summit 2023と OpenJDK Comitters' Workshop に参加してきました - #chiroito ’s blog

2024年の今回は、以下について話がありました。

  • Babylon
  • Leyden
  • Valhalla
  • GC
  • Lilliput
  • Integrity
  • Loom

これらについて何が話されたか簡単に紹介します。詳しくは後日公開される動画を確認してください。 現地で聞いた時のメモだけを頼りにこれを書いているため、間違っているかも知れませんがご了承下さい。 後日動画が公開されたら再度確認して修正します。

Babylon

BabylonプロジェクトはJavaのアプリケーションをJava言語とそれが動くCPU以外の場所(SQLやCUDA、GPGPUなど)で動かせるようにすることを目的としてます。 2023年のJVMLSで登場しました。 まだコンセプトレベルの段階です。

去年は Class File API と Code Reflection、Code Modelの紹介だけでした。 これらの中でもCode Modelはとても重要で、ASTとバイトコードの中間に新たなIntermediate representation(IR)を定義します。 去年の発表以降、いくつかの記事が公開され、Code Modelについても具体的に紹介されています。

今回、新たに Code Model Lowというものが出てきました。 Code Modelが高級言語に該当し、Code Model Lowが機械語に近いもののように見受けられました。 他にも、IntelによるSPIR-VとPHIを使ったサンプル実装が紹介されました。 SPIR-Vに対応した Code Model を実装してみたようです。

Babylonの最新情報は、以下にあります。

記事がたくさんありますので、興味のある方はごらんください。

Leyden

Leyden プロジェクトは、Javaのアプリケーションの起動速度を短縮することを目的としています。 今回の発表では、さまざまなアプリケーションの起動速度が 1/3 程度になったと発表されました。

現在Leydenは、JVMの起動時にアプリケーションのクラスをロードおよびリンクされた状態で即座に利用できるようにすることで、起動時間を改善する試みをしているようです。

詳しくはこちらのJEPをご確認ください。

Leydenの最新情報はこちらから確認できます。

LeydenのEarly-Access Buildsはこちらで入手できます。

Leydenの使い方や、ベンチマークの結果はこちらにあります。

Valhalla

Valhallaプロジェクトは、オブジェクト指向プログラミングの抽象化を維持しつつ、プリミティブの性能特性をオブジェクトに組み込むことを目的としています。 これは、Javaオブジェクトモデルに Value Object を導入することで実現します。

今回は、かなりたくさんのことが発表されました。 もっとも大きな事は、変数やメソッドの戻り値がNullを許容するかどうかを指定できるようになることでしょう。 これは、Null-Restricted and Nullable Types と呼ばれます。 Nullを許容しないのは、型の後ろに!を付けます(String!)。 Nullを許容するには、型の後ろに?を付けます(String?)。 他にも、プリミティブ型を参照型のように使えるようにする Enhanced Primitive Boxing も発表されました。

詳しくはこちらのJEPをごらんください。

ほかにも、以下の論文が紹介されていました。

The Saga of the Parametric VM

ほかにもGC、Lilliput、Integrity、Loomなどがありましたが、これらについては、次回紹介します。