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

1. Обзор

Эта краткая статья будет введением в использование синхронизированного блока в 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: but was: 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. Синхронная Стать с Methods

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

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

Notice that we passed a parameter this to the synchronized block. This is the monitor object, the code inside the block gets synchronized on the monitor object. Simply put, only one thread per monitor object can execute inside that block of code.

In case the method is static, we would pass the class name in place of the object reference. And the class would be a monitor for synchronization of the block:

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

Let's test the block inside the static method:

@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. Reentrancy

The lock behind the synchronized methods and blocks is reentrant. That is, the current thread can acquire the same synchronized lock over and over again while holding it:

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.