スレッドセーフティー(Thread Safety)はJava

Javaにおけるスレッドセーフティーは非常に重要なトピックです。JavaはJavaスレッドを使用してマルチスレッド環境のサポートを提供します。同じオブジェクトから作成された複数のスレッドがオブジェクト変数を共有することを知っていますが、これはスレッドが共有データを読み取りおよび更新する際にデータの一貫性を損なう可能性があります。

スレッドセーフティー

データの一貫性の理由は、任意のフィールド値を更新するときに、それがアトミックプロセスではなく、3つのステップが必要であるためです。まず現在の値を読み取り、次に更新された値を取得するための必要な操作を行い、最後に更新された値をフィールド参照に割り当てます。複数のスレッドが共有データを更新しているシンプルなプログラムでこれを確認しましょう。

package com.journaldev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {
    
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        //スレッドの処理が完了するのを待ちます
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;
    
    @Override
    public void run() {
        for(int i=1; i < 5; i++){
            processSomething(i);
        	count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        //ジョブの処理
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

上記のプログラムでは、forループでcountが4回インクリメントされ、スレッドが両方の処理を終えた後、その値は8になるはずです。しかし、上記のプログラムを複数回実行すると、countの値が6、7、8の間で変動していることに気づくでしょう。これは、count++がアトミック操作のように見えるものの、実際にはそうではなく、データの破損を引き起こしているためです。

Javaにおけるスレッドセーフティ

Javaにおけるスレッドセーフティは、プログラムをマルチスレッド環境で安全に使用するためのプロセスです。プログラムをスレッドセーフにするためのさまざまな方法があります。

  • 同期は、Javaにおけるスレッドセーフティのために最も簡単で広く使用されているツールです。
  • java.util.concurrent.atomicパッケージからのAtomic Wrapperクラスの使用。例えば、AtomicInteger
  • java.util.concurrent.locksパッケージからのロックの使用。
  • スレッドセーフなコレクションクラスの使用。スレッドセーフティのためのConcurrentHashMapの使用については、この投稿をチェックしてください。
  • 変数に対してvolatileキーワードを使用して、すべてのスレッドがデータをメモリから読み取るようにし、スレッドキャッシュから読み取らないようにする。

Javaのsynchronized

同期は、スレッドセーフティを実現するためのツールであり、JVMは同期コードが一度に1つのスレッドによって実行されることを保証します。javaキーワードのsynchronizedは、同期コードを作成するために使用され、内部的にはオブジェクトまたはクラスにロックを使用して、同期コードが実行されていることを確認します。

  • Javaの同期は、スレッドが同期されたコードに入る前にリソースをロックし、コードの実行が終了すると、他のスレッドがロックできるリソースをアンロックすることで機能します。その間、他のスレッドは同期されたリソースをロックするために待機状態にあります。
  • synchronizedキーワードは、完全なメソッドを同期化する方法と、同期化ブロックを作成する方法の2つがあります。
  • メソッドが同期されると、オブジェクトがロックされます。メソッドが静的である場合は、クラスがロックされます。したがって、同期化が必要なメソッドの一部のセクションのみをロックするために、同期化ブロックを使用するのが最善の方法です。
  • 同期化ブロックを作成する際には、ロックが取得されるリソースを指定する必要があります。これはXYZ.classまたはクラスの任意のオブジェクトフィールドである可能性があります。
  • synchronized(this)は、同期化ブロックに入る前にオブジェクトをロックします。
  • 最も低いレベルのロックを使用する必要があります。たとえば、クラスに複数の同期化ブロックがあり、そのうちの1つがオブジェクトをロックしている場合、他の同期化ブロックも他のスレッドによる実行が利用できなくなります。オブジェクトをロックすると、オブジェクトのすべてのフィールドにロックが取得されます。
  • Javaの同期化はパフォーマンスのコストでデータの整合性を提供するため、絶対に必要な場合にのみ使用する必要があります。
  • Javaの同期化は同じJVMでのみ機能するため、複数のJVM環境でリソースをロックする必要がある場合は機能せず、グローバルなロックメカニズムを考慮する必要があります。
  • Javaの同期はデッドロックを引き起こす可能性があります。このポストをチェックしてください:Javaでのデッドロックとその回避方法
  • Javaの`synchronized`キーワードはコンストラクタや変数には使用できません。
  • 同期化ブロックに使用するためにダミーのプライベートオブジェクトを作成することが好ましいです。これにより、他のコードによって参照が変更されないようになります。たとえば、同期化しているオブジェクトのsetterメソッドがある場合、その参照は他のコードによって変更される可能性があり、同期化ブロックが並行して実行されることにつながります。
  • 定数プールで維持されているオブジェクトは使用しないでください。たとえば、文字列は同期化に使用すべきではありません。同じ文字列にロックをかけている他のコードがある場合、両方のコードが関係なくても、同じ参照オブジェクトから文字列プール上のロックを取得しようとします。

上記のプログラムをスレッドセーフにするために必要なコード変更を以下に示します。

    //同期化用のダミーオブジェクト変数
    private Object mutex=new Object();
    ...
    //同期的にカウント値を読み取り、増分し、更新するための同期化ブロックの使用
    synchronized (mutex) {
            count++;
    }

いくつかの同期化の例とそれらから学べることを見てみましょう。

public class MyObject {
 
  //オブジェクトのモニターへのロック
  public synchronized void doSomething() { 
    // ...
  }
}
 
//ハッカーのコード
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    //私のオブジェクトを無期限に遅延
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

注意してください、ハッカーのコードがmyObjectインスタンスをロックしようとしており、一度ロックを取得すると、それを解放しないため、doSomething()メソッドがロックの待ちでブロックされ、これによりシステムがデッドロックに陥り、サービスの拒否(DoS)が発生する可能性があります。

public class MyObject {
  public Object lock = new Object();
 
  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//信頼できないコード

MyObject myObject = new MyObject();
//ロックオブジェクトの参照を変更する
myObject.lock = new Object();

ロックオブジェクトがパブリックであることに注意してください。参照を変更することで、複数のスレッドで同期ブロックを並行して実行できます。プライベートオブジェクトを持ち、参照を変更するためのセッターメソッドがある場合も同様です。

public class MyObject {
  //クラスオブジェクトのモニターにロックをかける
  public static synchronized void doSomething() { 
    // ...
  }
}
 
//ハッカーのコード
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

ハッカーコードがクラスモニターにロックをかけており、それを解放しないため、システムでデッドロックとDoSが発生します。次に、複数のスレッドが同じ文字列配列で作業し、処理されると、スレッド名が配列値に追加されます。

package com.journaldev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //すべてのスレッドを開始する
        t1.start();t2.start();t3.start();
        //スレッドの終了を待つ
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //共有変数の値を確認する
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{
    
    private String[] strArr = null;
    
    public HashMapProcessor(String[] m){
        this.strArr=m;
    }
    
    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i < strArr.length; i++){
            //データを処理してスレッド名を追加する
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        // いくつかのジョブを処理する
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

上記のプログラムを実行すると、次のような出力が表示されます。

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

文字列配列の値が共有データと同期が取れていないため、破損しています。プログラムをスレッドセーフにするために、addThreadName()メソッドを変更する方法を以下に示します。

    private Object lock = new Object();
    private void addThreadName(int i, String name) {
        synchronized(lock){
        strArr[i] = strArr[i] +":"+name;
        }
    }

この変更後、プログラムは正常に動作し、次にプログラムの正しい出力が表示されます。

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

{
“error”: “Upstream error…”
}

Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java