【8/27まで】Udemyの人気コースが今なら1,200円から!!

【Java入門(スレッドセーフ)】複数スレッドが単一のインスタンスを書き換えるパターン

問題のコード

以下の2クラスを用意して3回実行してみます。これはスレッドセーフではない処理です。

Java

public class Sample1 {
    //インスタンス生成
    sampleB smp = new sampleB();

    public static void main(String[] args){
        Sample1 s = new Sample1();
        s.caller();
    }

    public void caller(){

        //スレッド1
        new Thread(() ->
            setting(10)
        ).start();

        //スレッド2
        new Thread(() ->
            setting(20)
        ).start();

        //スレッド3
        new Thread(() ->
            setting(30)
        ).start();

        //スレッド4
        new Thread(() ->
            setting(40)
        ).start();

        //スレッド5
        new Thread(() ->
            setting(50)
        ).start();

    }

    void setting(int num){

        //スレッド毎の値をセット。
        smp.setNum(num);
        try {
            //少し待機
            Thread.sleep(1);
        } catch (InterruptedException e) {
            // 適当コード
            e.printStackTrace();
        }
        //スレッドごとの値を出力。
        System.out.println("入力値:" + num + "  結果:" + smp.getNum());
    }
}

Java

public class sampleB {

    public int num;

    public void setNum(int num){
        this.num = num;
    }

    public int getNum(){
        return this.num;
    }
}

実行結果は以下のとおりです。

1回目の実行結果

入力値:10  結果:20
入力値:20  結果:30
入力値:30  結果:30
入力値:40  結果:50
入力値:50  結果:50

2回目の実行結果

入力値:10  結果:30
入力値:30  結果:30
入力値:20  結果:30
入力値:50  結果:50
入力値:40  結果:40

3回目の実行結果

入力値:20  結果:30
入力値:10  結果:30
入力値:30  結果:30
入力値:40  結果:50
入力値:50  結果:50

この実行結果を見てみると、ところどころおかしい箇所があります。
例えば1回目の実行結果を見ると入力値は10~50まであるのに対し、結果は20 or 30しかありません。
2回目の実行結果や3回目の実行結果も同様です。
このクラスは、5つのスレッドがsampleB smp = new sampleB()という単一のインスタンスに対してsmp.setNum(num);で値の書き換えを行い、smp.getNum()で値の読み取りを行うロジックになっています。
そうすると何が起こるのかというと、1個目のスレッドがインスタンスの値を書き換えてからそれを読み取るまでの間に、2個目、3個目・・・のスレッドが次々と値を書き換えていってしまうのです。
1個目のスレッドの処理が終了するのを待たずに2個目、3個目・・・の書き換え処理が走ってしまうので、1個目のスレッドが値を読み取る頃には、他のスレッドが書き換えた値を取得することになってしまうのです。

対処1:synchronizedを付与する

1個目のスレッドが単一のインスタンスに対し値を書き換えてから読み取るまでの間に、他のスレッドが処理に侵入してこないようにすれば良いので、処理に対してロックを掛けます。
synchronizedを付与することによって1個目のスレッドが処理をしている間、他のスレッドが侵入してこないようにすることができます。
変更点は39行目のsynchronized void setting(int num)です。

Java

public class Sample1 {
    //インスタンス生成
    sampleB smp = new sampleB();

    public static void main(String[] args){
        Sample1 s = new Sample1();
        s.caller();
    }

    public void caller(){

        //スレッド1
        new Thread(() ->
        setting(10)
        ).start();

        //スレッド2
        new Thread(() ->
        setting(20)
        ).start();

        //スレッド3
        new Thread(() ->
        setting(30)
        ).start();

        //スレッド4
        new Thread(() ->
        setting(40)
        ).start();

        //スレッド5
        new Thread(() ->
        setting(50)
        ).start();

    }

    synchronized void setting(int num){

        //スレッド毎の値をセット。
        smp.setNum(num);
        try {
            //少し待機
            Thread.sleep(1);
        } catch (InterruptedException e) {
            // 適当コード
            e.printStackTrace();
        }
        //スレッドごとの値を出力。
        System.out.println("入力値:" + num + "  結果:" + smp.getNum());
    }
}

3回実行した結果は以下のとおりです。

1回目の実行結果

入力値:10  結果:10
入力値:30  結果:30
入力値:20  結果:20
入力値:50  結果:50
入力値:40  結果:40

2回目の実行結果

入力値:10  結果:10
入力値:50  結果:50
入力値:40  結果:40
入力値:30  結果:30
入力値:20  結果:20

3回目の実行結果

入力値:10  結果:10
入力値:40  結果:40
入力値:30  結果:30
入力値:20  結果:20
入力値:50  結果:50

どの実行結果を見ても入力値と結果が等しいことを確認できました。
つまり、スレッド自身が書き換えた値と同じものを取得することができています。

コメントを残す

メールアドレスが公開されることはありません。