Javaの同期キーワードガイド

1概要

この簡単な記事は、Javaで synchronized ブロックを使用するためのイントロです。

簡単に言うと、マルチスレッド環境では、2つ以上のスレッドが同時に可変共有データを更新しようとすると競合状態が発生します。 Javaは、スレッドアクセスを共有データに同期させることによって競合状態を回避するメカニズムを提供します。

synchronized とマークされたロジックは同期ブロックになります。

2.なぜ同期なのか

合計を計算し、複数のスレッドが calculate() メソッドを実行する典型的な競合状態を考えてみましょう。

public class BaeldungSynchronizedMethods {

    private int sum = 0;

    public void calculate() {
        setSum(getSum() + 1);
    }

   //standard setters and getters
}

それでは簡単なテストを書きましょう。

@Test
public void givenMultiThread__whenNonSyncMethod() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    BaeldungSynchronizedMethods summation = new BaeldungSynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(summation::calculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, summation.getSum());
}

単に3つのスレッドプールを持つ ExecutorService を使用して calculate() を1000回実行しています。

これを連続して実行すると、予想される出力は1000になりますが、 マルチスレッド実行はほぼ毎回失敗します 実際の出力は矛盾しています。

java.lang.AssertionError: expected:<1000> but was:(965)
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
...

この結果はもちろん予想外のことではありません。

競合状態を回避する簡単な方法は、 synchronized キーワードを使用して操作をスレッドセーフにすることです。

3 synchronized キーワード

synchronized キーワードはさまざまなレベルで使用できます。

  • インスタンスメソッド

  • 静的メソッド

  • コードブロック

私たちが synchronized ブロックを使うとき、内部的にJavaは同期を提供するためにモニターロックまたは組み込みロックとも呼ばれるモニターを使います。

これらのモニタはオブジェクトにバインドされているため、同じオブジェクトのすべての同期ブロックに同時に実行できるスレッドは1つだけです。

3.1. __同期化されたインスタンスメソッド

メソッドを同期させるには、メソッド宣言に synchronized キーワードを追加するだけです。

public synchronized void synchronisedCalculate() {
    setSum(getSum() + 1);
}

メソッドを同期すると、テストケースが成功し、実際の出力は1000になります。

@Test
public void givenMultiThread__whenMethodSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethods method = new SynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(method::synchronisedCalculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, method.getSum());
}

インスタンスメソッドは、そのメソッドを所有するクラスのインスタンスに対して同期化されています。つまり、クラスのインスタンスごとにこのメソッドを実行できるスレッドは1つだけです。

3.2. 同期化されたStati c __メソッド

静的メソッドはインスタンスメソッドと同じように「同期化」されています。

 public static synchronized void syncStaticCalculate() {
     staticSum = staticSum + 1;
 }

これらのメソッドは、クラスに関連付けられた Class オブジェクトに対して synchronized され、クラスごとにJVMごとに1つの Class オブジェクトしか存在しないため、クラスごとに static synchronized メソッド内で実行できるスレッドは1つだけです。

テストしましょう。

@Test
public void givenMultiThread__whenStaticSyncMethod() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count ->
        service.submit(BaeldungSynchronizedMethods::syncStaticCalculate));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, BaeldungSynchronizedMethods.staticSum);
}

3.3. メソッド内の Synchronized ブロック

メソッド全体を同期させるのではなく、その中のいくつかの命令だけを同期させたくない場合があります。これは、ブロックに同期を適用することで実現できます。

public void performSynchrinisedTask() {
    synchronized (this) {
        setCount(getCount()+1);
    }
}

変更をテストしましょう。

@Test
public void givenMultiThread__whenBlockSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    BaeldungSynchronizedBlocks synchronizedBlocks = new BaeldungSynchronizedBlocks();

    IntStream.range(0, 1000)
      .forEach(count ->
        service.submit(synchronizedBlocks::performSynchronisedTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, synchronizedBlocks.getCount());
}

パラメータ this synchronized ブロックに渡したことに注意してください。

これはモニタオブジェクトです。ブロック内のコードはモニタオブジェクトと同期します。簡単に言うと、モニタオブジェクトごとに1つのスレッドだけがそのコードブロック内で実行できます。

メソッドが staticの場合は、 オブジェクト参照の代わりにクラス名を渡します。そして、クラスはブロックの同期のためのモニターになります。

public static void performStaticSyncTask(){
    synchronized (SynchronisedBlocks.class) {
        setStaticCount(getStaticCount() + 1);
    }
}

static メソッド内でブロックをテストしましょう。

@Test
public void givenMultiThread__whenStaticSyncBlock() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count ->
        service.submit(BaeldungSynchronizedBlocks::performStaticSyncTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, BaeldungSynchronizedBlocks.getStaticCount());
}

5結論

このクイック記事では、スレッド同期を達成するために synchronized キーワードを使用するさまざまな方法について説明しました。

また、競合状態がアプリケーションにどのような影響を与える可能性があるのか​​、同期化がそれを回避するのにどのように役立つかについても調査しました。 Javaでのロックを使用したスレッドセーフについての詳細は、 java.util.concurrent.Locks article を参照してください。

このチュートリアルの完全なコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-concurrency[GitHubで利用可能]です。