Javaのロック機構と変数の可視性

前回
blog1.mammb.com
からの続きです。

synchronized

Javaは言語本体の機能としてロック機構を備えています。このロック機構は synchronized キーワードにより指定します。ロックはミューテックスの働きをし、1度に1つのスレッドしか所有できません。
Javaのロックは再入可能です。ロックを所有しているスレッドが、対象が同じロックを取得することができます(ロック対象が異なる場合はこの限りではありません)。

同期メソッド

synchronized ブロックがメソッド全体を指定する場合、そのメソッドは同期メソッドとなり、以下のように記述します。

public synchronized long getNextLong() {
    ・・・
}

この場合のロック対象はメソッドが属するオブジェクトとなります。あるスレッドが同期メソッドを実行中は、そのメソッドを含むオブジェクトが持つ同期メソッドに、他のスレッドが入ることができなくなります。


同期メソッドがstaticであった場合は、static同期メソッドとなり、ロック対象は そのメソッドが属するクラスオブジェクトとなります。つまりそのクラスのインスタンス全てがロック対象となります。

public static synchronized long getNextLong() {
    ・・・
}

同期ブロック

ロックをメソッド単位より細かく設定する場合は同期ブロックを使用します。synchronized キーワードに続きロック対象となるオブジェクトを指定します。

public BigInteger[] getPrimeNumbers(BigInteger i) {
    BigInteger[] ret = null;
    synchronized (this) {
        if (i.equals(lastNumber))
            ret = cache.clone();
    }
    ・・・
}

以下のように任意のオブジェクトをロック対象として指定可能です。

private final Object lock = new Object();
public void method() {
    synchronized (lock) {
    }
    synchronized (this) {
    }
}

ロック対象のオブジェクトを指定することで、細かな粒度でのロック制御が可能となります。ロックの取得と開放には多少のオーバーヘッドが伴なうため、ロックを分割しすぎるのは良くありません。

メモリの可視性

複数スレッドで共有されるステート変数が同期化されていない場合、あるスレッドが更新した内容を、他のスレッドがタイミング良く見る保証はありません。他のスレッドが見る保証すらありません。
さらに、同期化されていない場合、コンパイラはソース上の実行順序を、見かけとは異なる順番に変更することがあり、複数スレッドでの動作が予想できないものになる可能性があります。

以下の Bean はスレッドセーフではありません。あるスレッドが呼び出したsetValの内容を、他のスレッドが見る保証はありません。

public class Mutable {
    private int val;
    public int getVal() { return val; }
    public void setVal(int val) { this.val = val; }
}

Javaのメモリモデルは32bit値の読み込みと書き込みはアトミック操作であることを規定していますが、それでも上記はスレッドセーフでは無いのです。更新した値は、あるスレッドからは見えないレジスタに保存されたままになり、他のスレッドから見えないままであることがあるのです。


上記をスレッドセーフにするには以下のように synchronized を追加するだけです。

public class Mutable {
    private int val;
    public synchronized int getVal() { return val; }
    public synchronized void setVal(int val) { this.val = val; }
}

これにより、あるスレッドにより更新された値は、必ず他のスレッドから見えるようになります。setterメソッドだけを同期化したのでは不十分です。getterメソッドも同期化する必要があります。

volatile

前述のメモリの可視性は、レジスタへのキャッシュや、他のプロセッサから見えないキャッシュへ共有変数が保存されるために発生します。これを必ずスレッドが共有するメモリへ格納するようにするにはvolatile キーワードを使います。

volatile boolean init;

リードとライト操作のみをアトミックに行うことができます。

immutable

同期化を行う場合には、immutableなオブジェクトを使用することを検討してください。immutable=不可変なオブジェクトは常にスレッドセーフです。
前回の以下の実装はスレッドセーフではありませんでした。

private BigInteger lastNumber;
private BigInteger[] cache;

public synchronized BigInteger[] getPrimeNumbers(BigInteger i) {
    if (i.equals(lastNumber))
        return cache;
    else {
        BigInteger[] primes = makePrimeNumbers(i);
        lastNumber = i;
        cache = primes;
    }
}

上記を immutable なオブジェクトを使ってスレッドセーフにしてみましょう。

まず普遍オブジェクトを作成します。casheのやり取りは参照のコピーでは不十分なのでコピーを保存します。

class ValueCashe {
    private final BigInteger lastNumber;
    private final BigInteger[] cashe;
    
    public ValueCashe(BigInteger i, BigInteger[] results) {
        this.lastNumber = i;
        this.cashe = Arrays.copyOf(results, results.length);
    }
    public BigInteger[] getCashedValues(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i)) return null;
        return Arrays.copyOf(cashe, cashe.length);
    }
}

上記の不可変オブジェクトを使い volatile 宣言すると、以下の実装はスレッドセーフとなります。

private volatile ValueCashe cache = new ValueCashe(null, null);

public BigInteger[] getPrimeNumbers(BigInteger i) {
    BigInteger[] primes = cache.getCashedValues(i);
    if (primes == null)
        primes = makePrimeNumbers(i);
        cache = new ValueCashe(i, primes);
    }
    return primes; 
}