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」を究める―
- 作者: Brian Goetz,Joshua Bloch,Doug Lea
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/11/22
- メディア: 単行本
- 購入: 30人 クリック: 442回
- この商品を含むブログ (174件) を見る
この書籍を再度読みかえしながら、同期化ステートとして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をリターン)します。