torutkのブログ

ソフトウェア・エンジニアのブログ

java.util.concurrent.Semaphoreはバイナリセマフォにはならない?

同時に1つの処理だけが実行されるよう相互排他を実現したいが、処理の開始と終了が同じスレッドではない(処理終了は別スレッドでないと判定できない)ため、LockではなくSemaphoreを使うことにしました。

Java SE 6の日本語版Javadocには、以下のような記述があります。

値を 1 に初期化されたセマフォーは、利用できるパーミットが最大で 1 個であるセマフォーとして使用されるため、相互排他ロックとして利用できます。

そこで、許可数を1でセマフォを作成しました。

Semaphore binarySemaphore = new Semaphore(1);

デバッグのため、ログにこのセマフォをacquire/releaseするたびにavailablePermitsを出力していたところ、

binarySemaphore.release();
logger.trace("binarySemaphore permits={}", binarySemaphore.availablePermits());

その値が1より大きな(例:2, 3, ...)値がログに出ていました。

Semaphoreの実装を調べてみると、release()を呼び出すたびに、availablePermitsが1ずつ増えるようになっています。Semaphoreのコンストラクタで指定する値は、あくまで初期値であり、以降はrelease()のたびに1増え、tryAcquire()/acquire()のたびに1減る、という振る舞いをしています。

つまり、release()が何かの拍子に複数呼ばれると、相互排他(同時に1つだけ処理を実行)が成立しないということになってしまいます。

AbstractQueuedSynchronzierを使った実装

java.util.concurrent.Semaphoreクラスは、内部でAbstractQueuedSynchronzierを使って実装しています。AbstractQueuedSynchronzierは、以前Java読書会で課題図書となった次の書籍に書かれています。(が、読書会の折には理解できずにいました。あちこちを検索してみましたが、使い方が難しい・・・。)

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

この書籍を再度読みかえしながら、同期化ステートとして0または1のどちらかしか取り得ないセマフォ(BinarySemaphore)をAbstractQueuedSynchronzierを使って、なんとなくこんな使い方でいいのかな、というレベルで実装してみました。ちゃんとしたテストはしていないので、正しいかどうかは分かりません。

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class BinarySemaphore {
    private final Sync sync;

    private static class Sync extends AbstractQueuedSynchronizer {
        Sync() {
            setState(1);
        }

        protected final int tryAcquireShared(int ignored) {
            return compareAndSetState(1, 0) ? 1 : -1;
        }
        protected final boolean tryReleaseShared(int ignored) {
            setState(1);
            return true;
        }
        final int availablePermits() {
            return getState();
        }
    }

    public BinarySemaphore() {
        sync = new Sync();
    }
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
    public boolean tryAcquire() {
        return sync.tryAcquireShared(1) >= 0;
    }
    public void release() {
        sync.releaseShared(1);
    }
    public int availablePermits() {
        return sync.availablePermits();
    }
}

AbstractQueuedSynchronizerは、テンプレートパターンを使用して、同期化処理の要件に合わせて利用者がサブクラスで獲得/解放などのメソッドを実装するようなクラスです。

今回実装するセマフォは、獲得/解放は「共有的(Shared)」、つまり、獲得したスレッドでなくても解放ができるものです。よって、AbstractQueuedSynchronizerのサブクラスで次のメソッドをオーバーライドします。

  • tryAcquireShared
  • tryReleaseShared

同期化ステートの変更は、このメソッドの実装において、AbstractQueuedSynchronizerクラスのsetState/getState/compareAndSetStateメソッドを適宜呼び出すことで行います。

バイナリセマフォでは、解放(tryReleaseShared)を呼べば必ず状態が1となるので単純にsetState(1)を呼びました。獲得は、compareAndSetStateで、現状が1で変更後が0となるときに成功(1をリターン)、それ以外は失敗(-1をリターン)します。