同時に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」を究める—
この書籍を再度読みかえしながら、同期化ステートとして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をリターン)します。
0 件のコメント:
コメントを投稿