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 .