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

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

1. 概要

この簡単な記事では、Javaでsynchronizedブロックを使用する方法を紹介します。

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

synchronizedでマークされたロジックの一部は、同期ブロックallowing only one thread to execute at any given timeになります。

2. 同期する理由

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

public class exampleSynchronizedMethods {

    private int sum = 0;

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

    // standard setters and getters
}

そして、簡単なテストを書いてみましょう。

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

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

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

calculate()を1000回実行するために、3スレッドプールでExecutorServiceを使用しているだけです。

これをシリアルに実行すると、期待される出力は1000になりますが、our multi-threaded execution fails almost every timeは、一貫性のない実際の出力になります。例:

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インスタンスメソッド

メソッド宣言に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());
}

インスタンスメソッドは、メソッドを所有するクラスのインスタンスに対するsynchronizedです。 つまり、クラスのインスタンスごとに1つのスレッドのみがこのメソッドを実行できます。

3.2. Synchronized Staticメソッド

静的メソッドは、インスタンスメソッドと同じようにsynchronizedです。

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

これらのメソッドは、クラスに関連付けられたClassオブジェクトのsynchronizedであり、クラスごとのJVMごとに1つのClassオブジェクトしか存在しないため、static%(内で実行できるスレッドは1つだけです。 t4)インスタンスの数に関係なく、クラスごとのメソッド。

テストしてみましょう:

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

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

    assertEquals(1000, exampleSynchronizedMethods.staticSum);
}

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

メソッド全体を同期せずに、その中のいくつかの指示だけを同期したい場合があります。 これは、ブロックに同期されたapplyingによって実現できます。

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

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

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

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

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

パラメータthissynchronizedブロックに渡したことに注意してください。 これはモニターオブジェクトであり、ブロック内のコードはモニターオブジェクトで同期されます。 簡単に言えば、モニターオブジェクトごとに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(exampleSynchronizedBlocks::performStaticSyncTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

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

5. 結論

この簡単な記事では、synchronizedキーワードを使用してスレッドの同期を実現するさまざまな方法を見てきました。

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

このチュートリアルの完全なコードは、over on GitHubで入手できます。