Java Thread Safety & Unsafety


Thread Safety


Thread Safetyとは、マルチスレッドで実行されるプログラムにおいて、共有されたリソースが存在する場合、そのリソースが開発者の意図に従って実行されることを意味する.

Thread Unsafety Example


マルチスレッドの動作により,開発者の意図通りに共有リソースのデータ値を保証できない場合をThread Unsafetyと呼ぶ.コードで理解してみましょう.次のコードはCommonCalculateという名前のオブジェクトで、内部インスタンス変数amountがあり、プラス記号でインスタンス変数amountの値を変更できます.
public class CommonCalculate {
    private int amount;
    public CommonCalculate() {
        amount = 0;
    }

    public void plus(int value) {
        amount += value;
    }

    public int getAmount() {
        return amount;
    }
}
次に、CommonCalculateオブジェクトのamountインスタンス変数の値を変更するThreadを実装します.スレッドはRunnable classを実装(実装)するか、Thread classを継承して実装することができる.ここでは後者の方式でコードを記述する.
public class ModifyAmountThread extends Thread {
    private CommonCalculate calc;
    private boolean addFlag;
    public ModifyAmountThread(CommonCalculate calc, boolean addFlag) {
        this.calc = calc;
        this.addFlag = addFlag;
    }

    public void run() {
        for(int loop=0; loop<10000; loop++) {
            if(addFlag) {
                calc.plus(1);
            }
        }
    }
}
次に、ModifyAmountThreadを実行し、行main関数を含むclassを生成します.RunSyncクラスは2つのModifyAmountThreadを実行しています.このとき、最後の行FinalValueを出力する文の期待値は20000です.addFlagがtrueであるため、Threadごとに10000個のプラス記号メソッドが呼び出されるため、CommonCalculateオブジェクトのamountインスタンス変数は10000+10000に変更する必要があります.
public class RunSync {
    public static void main(String[] args) {
        RunSync runSync = new RunSync();
        runSync.runCommonCalculate();
    }

    public void runCommonCalculate() {
        CommonCalculate calc = new CommonCalculate();
        ModifyAmountThread thread1 = new ModifyAmountThread(calc, true);
        ModifyAmountThread thread2 = new ModifyAmountThread(calc, true);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
            System.out.println("Final value is " + calc.getAmount());
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}
ただし、実行結果は20000ではなく13657の値を示しています.これはclass instance変数の共有によるThread Unsafetyである.
CommonCalculateクラスでは、amount+=valueというコマンドを、プラス記号メソッドを使用して見つけることができます.このコマンドは、amountにamount+valueの値を割り当てることを意味します.さらに,この方法は各Threadにおいてアクセス可能である.したがって、2つのModifyAmountThreadを作成して実行すると、次のようなことが起こります.

thread 1でamountの値を2に変更する前に、thread 2はamountの値を1に読み込むので、3のはずのthread 2の実行結果はthread 1と同じ2になります.したがって、最後の行の出力値は10000+10000未満になります.スレッドが常に20000であることを安全に保証するには、plusメソッドにsynchronizedを追加するだけです.
public synchronized void plus(int value) {
        amount += value;
    }
synchronizedの役割は何ですか?synchronizedメソッドの場合、あるスレッドでメソッドが呼び出されると、別のスレッドで呼び出されない役割を果たします.これにより、各threadが符号化メソッドを実行した結果をインスタンス変数に反映することができる.

これにより,マルチスレッドアクセスが制御された共有リソースに単独でアクセスする必要がない場合,開発者はThread Unsafeyと呼ばれる所望の値を得ることができる.

Thread Safety Example


上記の例では、パラメータで指定された領域変数valueは、インスタンス変数とは異なり、Threadごとに管理されています.ゾーン変数はthreadによって管理されます.これは、異なるthreadの影響を受けず、各threadとは独立して管理されることを意味します.すなわち、ゾーン変数はThread-safeです.なぜ領域変数はthread-safeですか?先に調べたJVMの構造でRuntime Data Areaを見ると回答が得られます.

Runtime Data Areaには、以下に示すように、ThreadごとのJVMスタックがあります.メソッドを呼び出すと、メソッド情報とパラメータ情報がJVM stackに格納されます.この場合、JVM stackは他のスレッドと共有されるのではなく、各スレッドに存在します.従って、マルチスレッド環境においても他のスレッドの影響を受けないためthread-safeを用いることができる.
可変オブジェクトもthread-safeです.可変オブジェクトは値が変わらないオブジェクトであるため、マルチスレッドアクセスは影響を受けません.どのスレッドにアクセスしても同じ値が保証されるため,開発者は自分の意思で操作することができる.
[参照]https://en.wikipedia.org/wiki/Thread_safety
[参考]教材『ジャワの神』