🐨

Java: 排他制御について

に公開

プロローグ

こんにちは。秋はどこいった? って思ってしまうほど寒くなりましたね。
この記事は、私が本やネットで勉強して自分なりに言葉にしたものです。自分の備忘録みたいなものです。 なので内容に関して誤りもあるかもしれませんが、あしからずご了承くださいませ。

テーマ

今回、排他制御について記載しています。主にsynchronizedについて記載してます。

synchronizedについて

スレッドが、クラスやメソッドを利用している間、他スレッドが待機するようコントロールすることを
排他制御」 と 呼ぶことが多いと思います。

synchronizedは、その手法の1つで、スレッドセーフな設計を実現させるための比較的シンプルな形にすることができます。

synchronizedメソッド と synchronized文について

synchronized実装には、以下2パターンで使えます。

synchronizedメソッドは、メソッドの修飾子に記載します。

//1. synchronizedメソッド
class MyCounter {
    private int nCounter = 0;
    //インスタンスメソッド
    public synchronized void increment() {
        this.nCounter++;
    }
    private static int nCounter2 = 0;
    //staticメソッド
    public static synchronized void increment2() {
       nCounter2++;
    }
}

synchronizedメソッドは、メソッドを抜けたタイミングで、自動的にロックを解放します。
上記コードはあえて、インスタンスメソッド と staticメソッドを記載しました。

理由は、

インスタンスメソッドは、呼び出した側の対象インスタンスごとにロックを獲得。
staticメソッドは、対象クラスのロックを獲得。

の違いがあります。

synchronized文は以下のように記載します。

//2. synchronized文
class MyCounter {
    //対象インスタンスごと
    private int nCounter = 0;
    public void increment() {
        synchronized(this) {
            this.nCounter++;
        }
    }
    //classのロック
    private static int nCounter2 = 0;
    public static void increment2() {
        synchronized(MyCounter.class) {
            nCounter2++;
        }
    }
}

synchronized文は、括弧内で指定したオブジェクトのロックを獲得します。
ブロック外に抜けることで自動的にロックが解除されます。

synchronizedメソッド と synchronized文 の違いは

にあります。

一般的に、メソッド内を簡潔にすることを心掛けていれば、synchronizedメソッドが推奨されて
います。

synchronizedの落とし穴について

<例1> 呼び出し側のインスタンスの注意点

public class Main {
    public static void main() {
     //.....省略......
        MyCounter counter = new MyCounter();
        Future<?> result1 = executor.submit(new Me(counter));
        Future<?> result2 = executor.submit(new Me(counter));
        //.....省略......
    }
}
class Me implements Runnable {
    private final int nMax = 1000_000_000;
    private MyCounter counter
    public Me(MyCounter counter) { this.counter = counter; }
    @Override
    public synchronized void run() {
        IntStream.rangeClosed(0, this.nMax).forEach(n -> this.counter.increment());
    }
}
class MyCounter {
    private int nCounter = 0;
    public void increment() { this.nCounter++; }
}

この場合、呼び出し側の処理で、class Meの各インスタンスが作られているので、runメソッドに
synchronized を つけていても意味をなしていない。MyCounterクラス側のincrementメソッドに
つけていないと意味をなさない。 以下のように修正する必要があると考えます。

//synchronizedメソッド
public synchronized void increment() { this.nCounter++; }

or

//synchronized文
public void increment() {
    synchronized(this) {
        this.nCounter++;
    }
}

<例2> イミュータブルなクラスを使用していた場合の注意点

class MyCounter {
    private Integer nCounter = 0;
    public void increment() {
        synchronized(this.nCounter) {
            this.nCounter++;
        }
    }
}

この場合、値がおかしくなります。仮にincrementメソッドをsynchronizedメソッドにする又は、
synchronizedの指定をインスタンス全体とすることで、正常に処理されます。

理由は、Integerのような不変クラスのインクリメントは、新規にIntegerのオブジェクトを作成、代入するので synchronizedの括弧内より指定していたオブジェクトとは異なるものとなります。
よって、別スレッドの処理するタイミングによっては、nCounterが参照しているオブジェクト と
synchronizedが指定しているオブジェクトが異なることで、処理が可能となってしまう。

ReadWriteLock インタフェースについて

最後に、synchronized以外の方法で、ReadWriteLockインターフェースを使用する方法がある。
ReadWriteLockは、読取り専用操作用および書込み用の、2つのモードをもつロック用インターフェースです。

メソッドは以下2つをもつ。

例えば、synchronizedを使用していたincrementメソッドを以下のようにも可能。

class MyCounter {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();

    private Integer nCounter = 0;
    public int get() { return this.nCounter; }
    public void increment() {
        writeLock.lock();
        this.nCounter++;
        writeLock.unlock();
    }
}

エピローグ

だらだらした内容となりすいません。ここまで読んで頂きありがとうございます。
最近、暖房をつけているところも増えてきて、ドライアイをもつ私にはなかなか耐え難いものがあります。皆様もお体に気をつけてお過ごしください。

Discussion