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

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

スレッドセーフティ (Threading safety)

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

package com.scdev.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();
        //wait for threads to finish processing
        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) {
        // processing some job
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

上記のプログラムでは、ループ内のカウントは4回ずつ1ずつ増加します。また、2つのスレッドがあるため、両方のスレッドが実行を完了した後、その値は8である必要があります。しかし、上記のプログラムを複数回実行すると、カウント値が6、7、8と変動していることに気付くでしょう。これは、count++がアトミック操作のように見えるが、それは実際にはそうではなく、データの破損を引き起こしています。

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

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

  • Synchronization is the easiest and most widely used tool for thread safety in java.
  • Use of Atomic Wrapper classes from java.util.concurrent.atomic package. For example AtomicInteger
  • Use of locks from java.util.concurrent.locks package.
  • Using thread safe collection classes, check this post for usage of ConcurrentHashMap for thread safety.
  • Using volatile keyword with variables to make every thread read the data from memory, not read from thread cache.

Javaのsynchronized

同期化は、スレッドセーフを実現するためのツールです。JVMは、同期化されたコードが一度に1つのスレッドによって実行されることを保証します。Javaキーワードの”synchronized”は、同期化されたコードを作成するために使用され、内部的にはオブジェクトやクラスにロックをかけて、一度に1つのスレッドだけが同期化されたコードを実行するようにします。

  • Java synchronization works on locking and unlocking of the resource before any thread enters into synchronized code, it has to acquire the lock on the Object and when code execution ends, it unlocks the resource that can be locked by other threads. In the meantime, other threads are in wait state to lock the synchronized resource.
  • We can use synchronized keyword in two ways, one is to make a complete method synchronized and another way is to create synchronized block.
  • When a method is synchronized, it locks the Object, if method is static it locks the Class, so it’s always best practice to use synchronized block to lock the only sections of method that needs synchronization.
  • While creating a synchronized block, we need to provide the resource on which lock will be acquired, it can be XYZ.class or any Object field of the class.
  • synchronized(this) will lock the Object before entering into the synchronized block.
  • You should use the lowest level of locking, for example, if there are multiple synchronized block in a class and one of them is locking the Object, then other synchronized blocks will also be not available for execution by other threads. When we lock an Object, it acquires a lock on all the fields of the Object.
  • Java Synchronization provides data integrity on the cost of performance, so it should be used only when it’s absolutely necessary.
  • Java Synchronization works only in the same JVM, so if you need to lock some resource in multiple JVM environment, it will not work and you might have to look after some global locking mechanism.
  • Java Synchronization could result in deadlocks, check this post about deadlock in java and how to avoid them.
  • Java synchronized keyword cannot be used for constructors and variables.
  • It is preferable to create a dummy private Object to use for the synchronized block so that it’s reference can’t be changed by any other code. For example, if you have a setter method for Object on which you are synchronizing, it’s reference can be changed by some other code leads to the parallel execution of the synchronized block.
  • We should not use any object that is maintained in a constant pool, for example String should not be used for synchronization because if any other code is also locking on same String, it will try to acquire lock on the same reference object from String pool and even though both the codes are unrelated, they will lock each other.

上記のプログラムをスレッドセーフにするために必要なコードの変更がこちらにあります。

    //dummy object variable for synchronization
    private Object mutex=new Object();
    ...
    //using synchronized block to read, increment and update count value synchronously
    synchronized (mutex) {
            count++;
    }

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

public class MyObject {
 
  // Locks on the object's monitor
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Hackers code
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Indefinitely delay myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

ハッカーのコードは、myObjectのインスタンスをロックしようとしていることに注意してください。一度ロックした後、解放しないため、doSomething()メソッドはロックを待ってブロックされます。これによりシステムがデッドロック状態になり、サービスの拒否(DoS)が発生します。

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

//untrusted code

MyObject myObject = new MyObject();
//change the lock Object reference
myObject.lock = new Object();

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

public class MyObject {
  //locks on the class object's monitor
  public static synchronized void doSomething() { 
    // ...
  }
}
 
// hackers code
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

ハッカーコードがクラスモニターを固定して解放しないことに注意してください。これにより、システムがデッドロックやDoSに陥る可能性があります。以下は、複数のスレッドが同じ文字列配列で作業し、処理された後にスレッド名を配列の値に追加する別の例です。

package com.scdev.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();
        //start all the threads
        t1.start();t2.start();t3.start();
        //wait for threads to finish
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //check the shared variable value now
        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++){
            //process data and append thread name
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        // processing some job
        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]

それがJavaにおけるスレッドセーフティについての全てです。スレッドセーフなプログラミングとsynchronizedキーワードの使用について学べたことを願っています。

コメントを残す 0

Your email address will not be published. Required fields are marked *