1. Обзор

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

Проще говоря, в многопоточной среде возникает race condition, это когда два или более потоков пытаются обновить изменяемые общие данные одновременно. Java предлагает механизм, чтобы избежать race condition путем синхронизации доступа потока к общим данным.

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

2. Почему cинхронизация?

Давайте рассмотрим типичное race condition, где мы вычисляем сумму, а несколько потоков выполняют метод вычисления.

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-threads для выполнения calculate() 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)
...

Этот результат, конечно, не является неожиданным.

Простой способ избежать состояния race condition - сделать операцию потокобезопасной с использованием ключевого слова synchronized.

3. Ключевое слово synchronized

Слово synchronized может использоваться на разных уровнях:

  • Методы экземпляра
  • Статические методы
  • Кодовые блоки

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

3.1. Синхронизированные методы экземпляра

Просто добавьте ключевое слово synchronized в объявление метода, чтобы сделать метод синхронизированным

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

Обратите внимание, что как только мы синхронизируем метод, в тест выше стал проходить:

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

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

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

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

Давайте проверим блок внутри статического метода:

@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. Заключение

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

Мы также изучили, как race condition может повлиять на наше приложение, и как синхронизация помогает нам избежать этого.