- Synchronized Methods
- Руководство по синхронизированному ключевому слову в Java
- 2. Почему синхронизация?
- 3. Синхронизированное ключевое слово
- 3.1. Синхронизированные методы экземпляра
- 3.2. Синхронизированные статические методы _
- 3.3. Синхронизированные блоки внутри методов
- 3.4. Повторный вход
- 4. Вывод
Synchronized Methods
The Java programming language provides two basic synchronization idioms: synchronized methods and synchronized statements. The more complex of the two, synchronized statements, are described in the next section. This section is about synchronized methods.
To make a method synchronized, simply add the synchronized keyword to its declaration:
public class SynchronizedCounter < private int c = 0; public synchronized void increment() < c++; >public synchronized void decrement() < c--; >public synchronized int value() < return c; >>
If count is an instance of SynchronizedCounter , then making these methods synchronized has two effects:
- First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object.
- Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.
Note that constructors cannot be synchronized using the synchronized keyword with a constructor is a syntax error. Synchronizing constructors doesn’t make sense, because only the thread that creates an object should have access to it while it is being constructed.
Warning: When constructing an object that will be shared between threads, be very careful that a reference to the object does not «leak» prematurely. For example, suppose you want to maintain a List called instances containing every instance of class. You might be tempted to add the following line to your constructor:
But then other threads can use instances to access the object before construction of the object is complete.
Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object’s variables are done through synchronized methods. (An important exception: final fields, which cannot be modified after the object is constructed, can be safely read through non-synchronized methods, once the object is constructed) This strategy is effective, but can present problems with liveness, as we’ll see later in this lesson.
Руководство по синхронизированному ключевому слову в Java
Этот краткий учебник будет введением в использование блока 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 .