Перейти к основному содержимому

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

· 4 мин. чтения

1. Обзор

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

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

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

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

Давайте рассмотрим типичное состояние гонки, когда мы вычисляем сумму, а несколько потоков выполняют метод calculate() :

public class ForEachSynchronizedMethods {

private int sum = 0;

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

// standard setters and getters
}

Тогда давайте напишем простой тест:

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

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. Синхронизированное ключевое слово

Мы можем использовать синхронизированное ключевое слово на разных уровнях:

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

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

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

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

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

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

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

assertEquals(1000, ForEachSynchronizedMethods.staticSum);
}

3.3. Синхронизированные блоки внутри методов

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

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

Затем мы можем проверить изменение:

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

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(ForEachSynchronizedBlocks::performStaticSyncTask));
service.awaitTermination(100, TimeUnit.MILLISECONDS);

assertEquals(1000, ForEachSynchronizedBlocks.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");
}
}
}

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

4. Вывод

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

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

Полный код для этой статьи доступен на GitHub .