Руководство по синхронизированному ключевому слову в Java
1. обзор
Эта краткая статья будет введением в использование блокаsynchronized в Java.
Проще говоря, в многопоточной среде возникает состояние состязания, когда два или более потоков пытаются обновить изменяемые совместно используемые данные одновременно. Java предлагает механизм, позволяющий избежать состязаний за счет синхронизации доступа потоков к общим данным.
Часть логики, отмеченнаяsynchronized, становится синхронизированным блокомallowing only one thread to execute at any given time.
2. Зачем нужна синхронизация?
Давайте рассмотрим типичное состояние гонки, когда мы вычисляем сумму, а несколько потоков выполняют методcalculate():
public class exampleSynchronizedMethods {
private int sum = 0;
public void calculate() {
setSum(getSum() + 1);
}
// standard setters and getters
}
И давайте напишем простой тест:
@Test
public void givenMultiThread_whenNonSyncMethod() {
ExecutorService service = Executors.newFixedThreadPool(3);
exampleSynchronizedMethods summation = new exampleSynchronizedMethods();
IntStream.range(0, 1000)
.forEach(count -> service.submit(summation::calculate));
service.awaitTermination(1000, TimeUnit.MILLISECONDS);
assertEquals(1000, summation.getSum());
}
Мы просто используемExecutorService с пулом из 3 потоков для выполненияcalculate() 1000 раз.
Если бы мы выполняли это последовательно, ожидаемый результат был бы 1000, ноour multi-threaded execution fails almost every time с несогласованным фактическим результатом, например:
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. Ключевое словоSynchronized
Ключевое словоsynchronized может использоваться на разных уровнях:
-
Методы экземпляра
-
Статические методы
-
Кодовые блоки
Когда мы используем блокsynchronized, внутри Java используется монитор, также известный как блокировка монитора или внутренняя блокировка, для обеспечения синхронизации. Эти мониторы привязаны к объекту, поэтому все синхронизированные блоки одного и того же объекта могут иметь только один поток, выполняющий их одновременно.
3.1. Методы экземпляраSynchronized
Просто добавьте ключевое слово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());
}
Методы экземпляра - этоsynchronized над экземпляром класса, владеющего методом. Это означает, что только один поток на экземпляр класса может выполнять этот метод.
3.2. Synchronized Static Методы
Статические методы - этоsynchronized, как и методы экземпляра:
public static synchronized void syncStaticCalculate() {
staticSum = staticSum + 1;
}
Этими методами являютсяsynchronized для объектаClass, связанного с классом, и поскольку существует только один объектClass для каждой JVM на класс, только один поток может выполняться внутриstaticsynchronizedдля каждого класса, независимо от количества экземпляров, которые он имеет.
Давайте проверим это:
@Test
public void givenMultiThread_whenStaticSyncMethod() {
ExecutorService service = Executors.newCachedThreadPool();
IntStream.range(0, 1000)
.forEach(count ->
service.submit(exampleSynchronizedMethods::syncStaticCalculate));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, exampleSynchronizedMethods.staticSum);
}
3.3. Synchronized блоков внутри методов
Иногда мы не хотим синхронизировать весь метод, а только некоторые инструкции внутри него. Это может быть достигнуто синхронизациейapplying с блоком:
public void performSynchrinisedTask() {
synchronized (this) {
setCount(getCount()+1);
}
}
давайте проверим изменения:
@Test
public void givenMultiThread_whenBlockSync() {
ExecutorService service = Executors.newFixedThreadPool(3);
exampleSynchronizedBlocks synchronizedBlocks = new exampleSynchronizedBlocks();
IntStream.range(0, 1000)
.forEach(count ->
service.submit(synchronizedBlocks::performSynchronisedTask));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, synchronizedBlocks.getCount());
}
Обратите внимание, что мы передали параметрthis в блокsynchronized. Это объект монитора, код внутри блока синхронизируется с объектом монитора. Проще говоря, только один поток на объект монитора может выполняться внутри этого блока кода.
В случае, если используется методstatic,, мы передадим имя класса вместо ссылки на объект. И класс будет монитором для синхронизации блока:
public static void performStaticSyncTask(){
synchronized (SynchronisedBlocks.class) {
setStaticCount(getStaticCount() + 1);
}
}
Давайте протестируем блок внутри методаstatic:
@Test
public void givenMultiThread_whenStaticSyncBlock() {
ExecutorService service = Executors.newCachedThreadPool();
IntStream.range(0, 1000)
.forEach(count ->
service.submit(exampleSynchronizedBlocks::performStaticSyncTask));
service.awaitTermination(100, TimeUnit.MILLISECONDS);
assertEquals(1000, exampleSynchronizedBlocks.getStaticCount());
}
5. Заключение
В этой быстрой статье мы рассмотрели различные способы использования ключевого словаsynchronized для достижения синхронизации потоков.
Мы также изучили, как состояние гонки может повлиять на наше приложение, и как синхронизация помогает нам избежать этого. Дополнительные сведения о безопасности потоков с использованием блокировок в Java см. В нашемjava.util.concurrent.Locksarticle.
Полный код этого руководства доступенover on GitHub.