16
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Java仮想スレッドではsynchronizedの代わりにReentrantLockで同期化する

Last updated at Posted at 2023-11-10

仮想スレッドと synchronized

Java 21 では高スケーラビリティを実現するためのイノベーションとして、仮想スレッドが正式に導入されました。しかし、言語機能としてのロック機構 synchronized キーワードは、1 つ以上の仮想スレッドが動作する従来のプラットフォームスレッド (キャリアスレッド) に対して作用するため、仮想スレッドがキャリからアンマウントできずにスタックしてしまい、synchronized を抜けるまでキャリアにピンニング (固定化) され独占されしまいます。仮想スレッドとかプラットフォームスレッドとかを気にせず、どちらでも以前の synchronized と同じように同期化するには、代わりに ReentrantLock クラスを使用します。

標準の ReentrantLock を使用したイディオム
    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();
        try {
            // 共有リソースへのアクセスなど
        } finally {
            lock.unlock();
        }
    }

ただし、上記のイディオムは柔軟さと引き換えに、lock()try の間に処理を書けないことを強制できず、また finallyunlock() とかきっちり書かないと不具合が発生するため、注意する必要があります。

この対応が必要なのは JDK (java.io や java.net など) や各種フレームワーク、ライブラリも同じです。たとえば synchronized やブロッキング操作が残っている古い通信ライブラリや JDBC ドライバなどを使用すると、アプリケーションを仮想スレッド対応しても、スケーラビリティが損なわれる可能性があります。

ReentrantLock を安全に使う 3 つの方法

構造的に安全に記述するには、以下のような方法があります。

1. ReentrantLock を try-with-resources

unlockAutoCloseable へ委譲するクラスを作成し、try-with-resources で使用します。open というメソッド名で close と対になることを明確にしています。サブクラスに AutoCloseableimplements してしまうと try (var _ = lock) みたいに lock() せずに try できてまうので open() の戻り値にし、AutoCloseable を毎回生成しないように最初に委譲フィールドに保持しています。JDK Lock 系クラスの try-with-resources 対応は 2011 年に Project Coin と JSR 334 で要望がありましたが、JDK の下位互換性を保てないなどの理由により却下されました。

AutoCloseable 対応した ReentrantLock のサブクラス
public class CloseableReentrantLock extends ReentrantLock {

    private final AutoCloseable autoCloseable = this::unlock;

	public AutoCloseable open() {
		lock();
		return autoCloseable;
	}
}
使用例: try-with-resources で自動アンロック
    private final CloseableReentrantLock lock = new CloseableReentrantLock();

    public void m() throws Exception {
        try (var _ = lock.open()) {
            // 共有リソースへのアクセスなど
        }
    }

コメントで新しいクラスを作らない版をいただきました。ただし、lock() 後に処理が書けてしまうことと、現在は軽微だと思いますが 毎回 Closeable インスタンス生成と関連する GC コストがあります。この生成オーバーヘッドに関しては前出の Project Coin での検討時にも指摘されていました。

新しいクラスを作らない版
	private final ReentrantLock lock = new ReentrantLock();

	public void m(int i) throws Exception {
	    lock.lock();
	    try (Closeable _ = lock::unlock) {
            // 共有リソースへのアクセスなど
	    }
	}

2. ReentrantLock をラムダで使う

同期ブロックをラムダとして渡せるサブクラスを作成して使用します。2013 年に JDK への組み込み要望があったのですが、この頃はラムダの毎回生成コストが無視できないことにより、却下されています。あと、Java 標準の関数インターフェースでは例外を外側でキャッチできないので、ちょっと使いにくいかもしれません。

ラムダで使えるようにした ReentrantLock のサブクラス
public class GuardableReentrantLock extends ReentrantLock {

	public void guard(Runnable guarded) {
		lock();
		try {
			guarded.run();
		} finally {
			unlock();
		}
	}
}
使用例: ラムダで自動アンロック
    private final GuardableReentrantLock lock = new GuardableReentrantLock();

    public void m() {
        lock.guard(() -> {
            // 共有リソースへのアクセスなど
        });
    }

3. Lombok の @Locked で ReentrantLock

Lombok の v1.18.31 から ReentrantLock をサポートするアノテーション @Locked が使用できます。フィールドも自動生成されるため、synchronized と同じ雰囲気で ReentrantLock できます。

Lombok で ReentrantLock を使う
    @Locked
    public void m() {
        // 共有リソースへのアクセスなど
    }

最大スレッド数

仮想スレッド数は無制限、プールすると逆に効率が落ちるためプールされません。仮想スレッドが使用するプラットフォームスレッドは、デフォルトで並列実行数が論理プロセッサー数、プラットフォームスレッドがブロックされたときの代替用として使用される最大プールサイズは 256 です。Web アプリケーションでは、Tomcat や Web サーバ、ゲートウェイなどの設定でリクエスト数を制限することが考えられます。

フレームワークなどの仮想スレッド対応

Spring Boot

Spring Boot では 3.2 から application.properties の spring.threads.virtual.enabled (デフォルト false) を true にするだけで、Tomcat や Jetty のリクエスト、@Async@Scheduled などで仮想スレッドが使用されるようになります。
https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html#features.task-execution-and-scheduling

副作用として仮想スレッドはデーモンスレッドになります。非デーモンスレッドが存在しない場合、JVM は終了します。この動作は、たとえば @Scheduled を使用したアプリケーションがすぐ終了してしまう問題が発生します。非デーモンスレッドがなくなった場合でもアプリケーションを存続させたい場合は spring.main.keep-alive (デフォルト false) を true に設定します。

関連記事

Tomcat

Tomcat 8.5.90 以降、server.xml Connector 要素の useVirtualThreads 属性 (デフォルト false) に true を設定することで、仮想スレッドが有効になります。

16
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?