スレッドセーフとは何ですか?

1. 概要

Javaは、すぐに使用できるマルチスレッドをサポートしています。 これは、別のワーカースレッドでバイトコードを同時に実行することで、https://www.baeldung.com/jvm-vs-jre-vs-jdk [JVM]がアプリケーションのパフォーマンスを改善できることを意味します。
マルチスレッドは強力な機能ですが、代償が伴います。 マルチスレッド環境では、スレッドセーフな方法で実装を記述する必要があります。 これは、誤った動作を公開したり、予測できない結果を生成したりすることなく、異なるスレッドが同じリソースにアクセスできることを意味します。
*このプログラミング方法論は「スレッドセーフ」として知られています。*
このチュートリアルでは、それを達成するためのさまざまなアプローチを見ていきます。

2. ステートレスな実装

ほとんどの場合、マルチスレッドアプリケーションのエラーは、複数のスレッド間で状態を誤って共有した結果です。
したがって、私たちが検討する最初のアプローチは、スレッドセーフ**を達成することですステートレス実装を使用して**。
このアプローチをよりよく理解するために、数値の階乗を計算する静的メソッドを持つ単純なユーティリティクラスを考えてみましょう。
public class MathUtils {

    public static BigInteger factorial(int number) {
        BigInteger f = new BigInteger("1");
        for (int i = 2; i <= number; i++) {
            f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
    }
}
  • _factorial()_メソッドはステートレスの決定論的関数です。*特定の入力が与えられると、常に同じ出力を生成します。

    このメソッドは、外部状態に依存せず、状態をまったく維持しません*。 したがって、スレッドセーフであると見なされ、複数のスレッドから同時に安全に呼び出すことができます。
    すべてのスレッドは_factorial()_メソッドを安全に呼び出すことができ、互いに干渉したり、メソッドが他のスレッドに対して生成する出力を変更したりすることなく、期待される結果を取得します。
    したがって、*ステートレス実装は、スレッドセーフ*を実現する最も簡単な方法です。

3. 不変の実装

*異なるスレッド間で状態を共有する必要がある場合は、不変にすることでスレッドセーフなクラスを作成できます*。
不変性は、言語に依存しない強力な概念であり、Javaで実現するのはかなり簡単です。
簡単に言うと、*クラスインスタンスは、内部状態が構築された後に変更できない場合、不変です*。
Javaで不変クラスを作成する最も簡単な方法は、_private_および_final_フィールドをすべて宣言し、セッターを提供しないことです。
public class MessageService {

    private final String message;

    public MessageService(String message) {
        this.message = message;
    }

    // standard getter

}
_MessageService_オブジェクトは、構築後に状態を変更できないため、事実上不変です。 したがって、スレッドセーフです。
さらに、_MessageService_が実際に可変であるが、複数のスレッドが読み取り専用でしかアクセスできない場合、スレッドセーフでもあります。
したがって、*不変性は、スレッドセーフ*を実現するためのもう1つの方法です。

4. スレッドローカルフィールド

オブジェクト指向プログラミング(OOP)では、オブジェクトは実際にフィールドを介して状態を維持し、1つ以上のメソッドを介して動作を実装する必要があります。
実際に状態を維持する必要がある場合は、*フィールドをスレッドローカルにすることで、スレッド間で状態を共有しないスレッドセーフなクラスを作成できます*
_https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html [Thread] _クラスでプライベートフィールドを定義するだけで、フィールドがスレッドローカルなクラスを簡単に作成できます。
たとえば、_integers_の_array_を格納する_Thread_クラスを定義できます。
public class ThreadA extends Thread {

    private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

    @Override
    public void run() {
        numbers.forEach(System.out::println);
    }
}
別のものは_strings_の_array_を保持するかもしれません:
public class ThreadB extends Thread {

    private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");

    @Override
    public void run() {
        letters.forEach(System.out::println);
    }
}
*両方の実装で、クラスには独自の状態がありますが、他のスレッドとは共有されません。 したがって、クラスはスレッドセーフです。*
同様に、_https://www.baeldung.com/java-threadlocal [ThreadLocal] _インスタンスをフィールドに割り当てることにより、スレッドローカルフィールドを作成できます。
たとえば、次の_StateHolder_クラスを考えてみましょう。
public class StateHolder {

    private final String state;

    // standard constructors / getter
}
次のように簡単にスレッドローカル変数にすることができます。
public class ThreadState {

    public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() {

        @Override
        protected StateHolder initialValue() {
            return new StateHolder("active");
        }
    };

    public static StateHolder getState() {
        return statePerThread.get();
    }
}
スレッドローカルフィールドは、通常のクラスフィールドに非常に似ていますが、セッター/ゲッターを介してフィールドにアクセスする各スレッドが独自に初期化されたフィールドのコピーを取得するため、各スレッドは独自の状態を持ちます。

5. 同期されたコレクション

https://docs.oracle.com/javase/8/docs/technotes/guides/collections/overview.html [コレクションフレームワーク]に含まれる同期ラッパーのセットを使用して、スレッドセーフなコレクションを簡単に作成できます。
たとえば、次のlink:/java-synchronized-collections[synchronization wrappers]のいずれかを使用して、スレッドセーフコレクションを作成できます。
Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12)));
thread1.start();
thread2.start();
同期コレクションでは、各メソッドで固有のロックを使用することに注意してください(固有のロックについては後で説明します)。
*これは、一度に1つのスレッドのみがメソッドにアクセスできることを意味しますが、他のスレッドは、メソッドが最初のスレッドによってロック解除されるまでブロックされます。*
したがって、同期アクセスの基礎となるロジックにより、同期にはパフォーマンスが低下します。

6. 同時コレクション

同期されたコレクションの代わりに、並行コレクションを使用してスレッドセーフコレクションを作成できます。
Javaは_https://docs.oracle.com/javase/8/docs/api/?java / util / concurrent / package-summary.html [java.util.concurrent] _パッケージを提供します。これには、次のようないくつかの同時コレクションが含まれます。 _https://docs.oracle.com/javase/8/docs/api/?java / util / concurrent / package-summary.html [ConcurrentHashMap] _として:
Map<String,String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1", "one");
concurrentMap.put("2", "two");
concurrentMap.put("3", "three");
同期コレクション**とは異なり、コンカレントコレクションは、データをセグメントに分割することでスレッドセーフを実現します**。 たとえば、_ConcurrentHashMap_では、複数のスレッドが異なるマップセグメントでロックを取得できるため、複数のスレッドが同時に_Map_にアクセスできます。
*同時コレクションは、同時スレッドアクセスに固有の利点があるため、*同期コレクションより*はるかに高性能です*。
*同期コレクションおよび同時コレクションは、コレクション自体をスレッドセーフにするだけであり、コンテンツを作成しないことに注意してください*。

7. 原子オブジェクト

_https://docs.oracle.com/javase/8/を含む、Javaが提供するlink:/java-atomic-variables[atomic classes]のセットを使用してスレッドセーフを実現することも可能です。 docs / api / java / util / concurrent / atomic / AtomicInteger.html [AtomicInteger] _、_https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicLong.html [ AtomicLong] _、_ https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicBoolean.html [AtomicBoolean] _、および_https://docs.oracle.com/javase /8/docs/api/java/util/concurrent/atomic/AtomicReference.html[AtomicReference]_。
*アトミッククラスを使用すると、同期を使用せずにスレッドセーフなアトミック操作を実行できます*。 アトミック操作は、単一のマシンレベル操作で実行されます。
これが解決する問題を理解するために、次の_Counter_クラスを見てみましょう。
public class Counter {

    private int counter = 0;

    public void incrementCounter() {
        counter += 1;
    }

    public int getCounter() {
        return counter;
    }
}
*競合状態で、2つのスレッドが_incrementCounter()_メソッドに同時にアクセスすると仮定します。*
理論的には、_counter_フィールドの最終値は2になります。 しかし、スレッドが同じコードブロックを同時に実行しており、インクリメントはアトミックではないため、結果を確認することはできません。
_AtomicInteger_オブジェクトを使用して、_Counter_クラスのスレッドセーフな実装を作成しましょう。
public class AtomicCounter {

    private final AtomicInteger counter = new AtomicInteger();

    public void incrementCounter() {
        counter.incrementAndGet();
    }

    public int getCounter() {
        return counter.get();
    }
}
*これはスレッドセーフです。インクリメントは、複数の操作を必要とするが、_incrementAndGet_はアトミックであるためです*。

8. 同期メソッド

以前のアプローチはコレクションやプリミティブには非常に適していますが、それよりもさらに制御が必要になる場合があります。
そのため、スレッドセーフを実現するために使用できるもう1つの一般的なアプローチは、同期メソッドの実装です。
簡単に言えば**、同期されたメソッドにアクセスできるスレッドは一度に1つだけで、他のスレッドからこのメソッドへのアクセスはブロックされます**。 他のスレッドは、最初のスレッドが終了するか、メソッドが例外をスローするまでブロックされたままになります。
別の方法で_incrementCounter()_のスレッドセーフバージョンを作成するには、同期メソッドを作成します。
public synchronized void incrementCounter() {
    counter += 1;
}
メソッドの署名の先頭にlink:/java-synchronized[_synchronized_]キーワードを付けて、同期メソッドを作成しました。
一度に1つのスレッドが同期メソッドにアクセスできるため、1つのスレッドが_incrementCounter()_メソッドを実行し、他のスレッドも同様に実行します。 重複した実行は一切発生しません。
*同期メソッドは、「固有のロック」または「モニターロック」の使用に依存しています*。 組み込みロックは、特定のクラスインスタンスに関連付けられた暗黙的な内部エンティティです。
マルチスレッドコンテキストでは、_monitor_という用語は、指定されたメソッドまたはステートメントのセットへの排他的アクセスを強制するため、ロックが関連付けられたオブジェクトに対して実行するロールへの単なる参照です。
*スレッドは同期メソッドを呼び出すと、固有のロックを取得します。*スレッドはメソッドの実行を終了した後、ロックを解除します。したがって、他のスレッドがロックを取得してメソッドにアクセスできます。
インスタンスメソッド、静的メソッド、およびステートメント(同期されたステートメント)で同期を実装できます。

*9. 同期されたステートメント

*
メソッドのセグメントをスレッドセーフにするだけでよい場合、メソッド全体を同期するのはやり過ぎかもしれません。
このユースケースを例示するために、_incrementCounter()_メソッドをリファクタリングしましょう:
public void incrementCounter() {
    // additional unsynced operations
    synchronized(this) {
        counter += 1; 
    }
}
この例は簡単ですが、同期ステートメントを作成する方法を示しています。 メソッドが同期を必要としないいくつかの追加操作を実行するようになったと仮定すると、関連する状態変更セクションを同期ブロック内にラップすることによってのみ同期します。
同期メソッドとは異なり、同期ステートメントは、固有のロックを提供するオブジェクト、通常link:/java-this[_this_]参照を指定する必要があります。
*同期は高価なので、このオプションを使用すると、メソッドの関連部分のみを同期できます*。

10. 揮発性フィールド

同期化されたメソッドとブロックは、スレッド間の変数の可視性の問題に対処するのに便利です。 それでも、通常のクラスフィールドの値はCPUによってキャッシュされる場合があります。 したがって、特定のフィールドへの結果的な更新は、たとえ同期されていても、他のスレッドからは見えない可能性があります。
この状況を防ぐために、https://www.baeldung.com/java-volatile [_volatile_]クラスフィールドを使用できます。
public class Counter {

    private volatile int counter;

    // standard constructors / getter

}
  • _volatile_キーワードを使用して、JVMとコンパイラに_counter_変数をメインメモリに格納するよう指示します。*このようにして、JVMが_counter_変数の値を読み取るたびに、実際にメインメモリから読み取るようにします。 、CPUキャッシュからではなく。 同様に、JVMが_counter_変数に書き込むたびに、値はメインメモリに書き込まれます。

    さらに、* _volatile_変数を使用すると、特定のスレッドに表示されるすべての変数がメインメモリからも読み取られるようになります*。
    次の例を考えてみましょう。
public class User {

    private String name;
    private volatile int age;

    // standard constructors / getters

}
この場合、JVMが_age_ _volatile_変数をメインメモリに書き込むたびに、不揮発性_name_変数もメインメモリに書き込みます。 これにより、両方の変数の最新の値がメインメモリに保存されるため、結果として変数が更新されると、他のスレッドから自動的に表示されます。
同様に、スレッドが_volatile_変数の値を読み取る場合、スレッドに表示されるすべての変数もメインメモリから読み取られます。
  • _volatile_変数が提供するこの拡張保証は、http://tutorials.jenkov.com/java-concurrency/volatile.html [完全な揮発性可視性保証] *として知られています。

11. 外部ロック

組み込みのロックではなく、外部のモニターロックを使用することで、_Counter_クラスのスレッドセーフな実装をわずかに改善できます。
外部ロックは、マルチスレッド環境で共有リソースへの協調アクセスも提供しますが、外部エンティティを使用してリソースへの排他アクセスを強制します*:
public class ExtrinsicLockCounter {

    private int counter = 0;
    private final Object lock = new Object();

    public void incrementCounter() {
        synchronized(lock) {
            counter += 1;
        }
    }

    // standard getter

}
外部ロックを作成するには、単純なhttps://docs.oracle.com/javase/8/docs/api/java/lang/Object.html[_Object_]インスタンスを使用します。 この実装は、ロックレベルでセキュリティを促進するため、わずかに優れています。
同期メソッドとブロックが_this_参照に依存している組み込みロックでは、*攻撃者は組み込みロックを取得し、サービス拒否(DoS)条件をトリガーすることでデッドロックを引き起こす可能性があります。
固有の対応物とは異なり、*外部ロックは、外部からアクセスできないプライベートエンティティを使用します*。これにより、攻撃者がロックを取得してデッドロックを引き起こすのが難しくなります。

12. 再入可能ロック

Javaは、_https://www.baeldung.com/java-concurrent-locks [Lock] _実装の改善されたセットを提供します。この実装の動作は、上記で説明した固有のロックよりもわずかに洗練されています。
*組み込みロックでは、ロック取得モデルはかなり厳格です:* 1つのスレッドがロックを取得し、メソッドまたはコードブロックを実行し、最後にロックを解放するため、他のスレッドがロックを取得してメソッドにアクセスできます。
キューに入れられたスレッドをチェックし、最も長く待機しているスレッドに優先的にアクセスするための基礎となるメカニズムはありません。
_https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.html [ReentrantLock] _インスタンスを使用すると、それを正確に行うことができます。 https://en.wikipedia.org/wiki/Starvation_(computer_science)[resource starvation]:*の
public class ReentrantLockCounter {

    private int counter;
    private final ReentrantLock reLock = new ReentrantLock(true);

    public void incrementCounter() {
        reLock.lock();
        try {
            counter += 1;
        } finally {
            reLock.unlock();
        }
    }

    // standard constructors / getter

}
_ReentrantLock_ constructorは、オプションの_fairness_ _boolean_パラメーターを取ります。 _true_に設定され、複数のスレッドがロックを取得しようとすると、* JVMは最も長い待機スレッドに優先順位を与え、ロックへのアクセスを許可します*。

13. 読み取り/書き込みロック

スレッドセーフを実現するために使用できるもう1つの強力なメカニズムは、_https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReadWriteLock.html [ReadWriteLock] _実装の使用です。 。
_ReadWriteLock_ロックは、実際には読み取り専用操作用と書き込み操作用に関連付けられたロックのペアを使用します。
その結果、*リソースに書き込むスレッドがない限り、多くのスレッドがリソースを読み取ることができます。 さらに、リソースに書き込むスレッドは、他のスレッドがそれを読み取れないようにします*。
次のように_ReadWriteLock_ロックを使用できます。
public class ReentrantReadWriteLockCounter {

    private int counter;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    public void incrementCounter() {
        writeLock.lock();
        try {
            counter += 1;
        } finally {
            writeLock.unlock();
        }
    }

    public int getCounter() {
        readLock.lock();
        try {
            return counter;
        } finally {
            readLock.unlock();
        }
    }

   // standard constructors

}

14. 結論

この記事では、* Javaのスレッドセーフとは何かを学び、それを実現するためのさまざまなアプローチを詳しく調べました*。
いつものように、この記事に示されているすべてのコードサンプルはhttps://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-basic[GitHubで]から入手できます。