2012年12月11日火曜日

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」を究める—

この書籍を再度読みかえしながら、同期化ステートとして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 件のコメント:

コメントを投稿