Рубрики
Без рубрики

Руководство по ключевому слову Synchronized в Java

В этой статье рассматривается синхронизация потоков методов, статических методов и экземпляров в Java.

Автор оригинала: slavisa.author.

1. Обзор

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

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

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

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

Методы экземпляра синхронизируются над экземпляром класса, владеющего методом. Это означает, что только один поток на экземпляр класса может выполнить этот метод.

3.2. Синхронизированные Статические методы

Статические методы синхронизированы точно так же, как методы экземпляра:

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

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

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

@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. Синхронизированные Блоки Внутри Методов

Иногда мы хотим синхронизировать не весь метод, а только некоторые инструкции внутри него. Это может быть достигнуто путем применения синхронизации к блоку:

public void performSynchronisedTask() {
    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 . Это объект монитора, код внутри блока синхронизируется с объектом монитора. Проще говоря, только один поток на объект монитора может выполняться внутри этого блока кода.

В случае, если метод статический , мы передадим имя класса вместо ссылки на объект. И класс будет монитором для синхронизации блока:

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

3.4. Повторный вход

Блокировка за синхронизированными методами и блоками является реентерабельной. То есть текущий поток может получать одну и ту же синхронизированную блокировку снова и снова, удерживая ее:

Object lock = new Object();
synchronized (lock) {
    System.out.println("First time acquiring it");

    synchronized (lock) {
        System.out.println("Entering again");

         synchronized (lock) {
             System.out.println("And again");
         }
    }
}

Как показано выше, пока мы находимся в блоке synchronized , мы можем многократно получать одну и ту же блокировку монитора.

4. Заключение

В этой краткой статье мы рассмотрели различные способы использования ключевого слова synchronized для достижения синхронизации потоков.

Мы также изучили, как состояние гонки может повлиять на наше приложение и как синхронизация помогает нам избежать этого. Для получения дополнительной информации о безопасности потоков с использованием блокировок в Java обратитесь к нашему java.util.concurrent.Замки статья .

Полный код этого руководства доступен на GitHub .