Руководство по синхронизированному ключевому слову в Java

Руководство по синхронизированному ключевому слову в Java

1. обзор

Эта краткая статья будет введением в использование блокаsynchronized в Java.

Проще говоря, в многопоточной среде возникает состояние состязания, когда два или более потоков пытаются обновить изменяемые совместно используемые данные одновременно. 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());
}

Мы просто используемExecutorService с пулом из 3 потоков для выполненияcalculate() 1000 раз.

Если бы мы выполняли это последовательно, ожидаемый результат был бы 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 используется монитор, также известный как блокировка монитора или внутренняя блокировка, для обеспечения синхронизации. Эти мониторы привязаны к объекту, поэтому все синхронизированные блоки одного и того же объекта могут иметь только один поток, выполняющий их одновременно.

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 над экземпляром класса, владеющего методом. Это означает, что только один поток на экземпляр класса может выполнять этот метод.

3.2. Synchronized Static Методы

Статические методы - этоsynchronized, как и методы экземпляра:

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

Этими методами являютсяsynchronized для объектаClass, связанного с классом, и поскольку существует только один объектClass для каждой JVM на класс, только один поток может выполняться внутриstaticsynchronizedдля каждого класса, независимо от количества экземпляров, которые он имеет.

Давайте проверим это:

@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());
}

Обратите внимание, что мы передали параметрthis в блокsynchronized. Это объект монитора, код внутри блока синхронизируется с объектом монитора. Проще говоря, только один поток на объект монитора может выполняться внутри этого блока кода.

В случае, если используется метод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.