- Руководство по синхронизированному ключевому слову в Java
- 2. Почему синхронизация?
- 3. Синхронизированное ключевое слово
- 3.1. Синхронизированные методы экземпляра
- 3.2. Синхронизированные статические методы _
- 3.3. Синхронизированные блоки внутри методов
- 3.4. Повторный вход
- 4. Вывод
- Synchronize java где используется
Руководство по синхронизированному ключевому слову в 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 .
Synchronize java где используется
При работе потоки нередко обращаются к каким-то общим ресурсам, которые определены вне потока, например, обращение к какому-то файлу. Если одновременно несколько потоков обратятся к общему ресурсу, то результаты выполнения программы могут быть неожиданными и даже непредсказуемыми. Например, определим следующий код:
public class Program < public static void main(String[] args) < CommonResource commonResource= new CommonResource(); for (int i = 1; i < 6; i++)< Thread t = new Thread(new CountThread(commonResource)); t.setName("Thread "+ i); t.start(); >> > class CommonResource < int x=0; >class CountThread implements Runnable < CommonResource res; CountThread(CommonResource res)< this.res=res; >public void run() < res.x=1; for (int i = 1; i < 5; i++)< System.out.printf("%s %d \n", Thread.currentThread().getName(), res.x); res.x++; try< Thread.sleep(100); >catch(InterruptedException e)<> > > >
Здесь определен класс CommonResource , который представляет общий ресурс и в котором определено одно целочисленное поле x.
Этот ресурс используется классом потока CountThread. Этот класс просто увеличивает в цикле значение x на единицу. Причем при входе в поток значение x=1:
То есть в итоге мы ожидаем, что после выполнения цикла res.x будет равно 4.
В главном классе программы запускается пять потоков. То есть мы ожидаем, что каждый поток будет увеличивать res.x с 1 до 4 и так пять раз. Но если мы посмотрим на результат работы программы, то он будет иным:
Thread 1 1 Thread 2 1 Thread 3 1 Thread 5 1 Thread 4 1 Thread 5 6 Thread 2 6 Thread 1 6 Thread 3 6 Thread 4 6 Thread 4 11 Thread 2 11 Thread 5 11 Thread 3 11 Thread 1 11 Thread 4 16 Thread 1 16 Thread 3 16 Thread 5 16 Thread 2 16
То есть пока один поток не окончил работу с полем res.x, с ним начинает работать другой поток.
Чтобы избежать подобной ситуации, надо синхронизировать потоки. Одним из способов синхронизации является использование ключевого слова synchronized . Этот оператор предваряет блок кода или метод, который подлежит синхронизации. Для его применения изменим класс CountThread:
class CountThread implements Runnable < CommonResource res; CountThread(CommonResource res)< this.res=res; >public void run() < synchronized(res)< res.x=1; for (int i = 1; i < 5; i++)< System.out.printf("%s %d \n", Thread.currentThread().getName(), res.x); res.x++; try< Thread.sleep(100); >catch(InterruptedException e)<> > > > >
При создании синхронизированного блока кода после оператора synchronized идет объект-заглушка: synchronized(res) . Причем в качестве объекта может использоваться только объект какого-нибудь класса, но не примитивного типа.
Каждый объект в Java имеет ассоциированный с ним монитор . Монитор представляет своего рода инструмент для управления доступа к объекту. Когда выполнение кода доходит до оператора synchronized, монитор объекта res блокируется, и на время его блокировки монопольный доступ к блоку кода имеет только один поток, который и произвел блокировку. После окончания работы блока кода, монитор объекта res освобождается и становится доступным для других потоков.
После освобождения монитора его захватывает другой поток, а все остальные потоки продолжают ожидать его освобождения.
В итоге консольный вывод изменится:
Thread 1 1 Thread 1 2 Thread 1 3 Thread 1 4 Thread 3 1 Thread 3 2 Thread 3 3 Thread 3 4 Thread 5 1 Thread 5 2 Thread 5 3 Thread 5 4 Thread 4 1 Thread 4 2 Thread 4 3 Thread 4 4 Thread 2 1 Thread 2 2 Thread 2 3 Thread 2 4
При применении оператора synchronized к методу пока этот метод не завершит выполнение, монопольный доступ имеет только один поток — первый, который начал его выполнение. Для применения synchronized к методу, изменим классы программы:
public class Program < public static void main(String[] args) < CommonResource commonResource= new CommonResource(); for (int i = 1; i < 6; i++)< Thread t = new Thread(new CountThread(commonResource)); t.setName("Thread "+ i); t.start(); >> > class CommonResource < int x; synchronized void increment()< x=1; for (int i = 1; i < 5; i++)< System.out.printf("%s %d \n", Thread.currentThread().getName(), x); x++; try< Thread.sleep(100); >catch(InterruptedException e)<> > > > class CountThread implements Runnable < CommonResource res; CountThread(CommonResource res)< this.res=res; >public void run() < res.increment(); >>
Результат работы в данном случае будет аналогичен примеру выше с блоком synchronized. Здесь опять в дело вступает монитор объекта CommonResource — общего объекта для всех потоков. Поэтому синхронизированным объявляется не метод run() в классе CountThread, а метод increment класса CommonResource. Когда первый поток начинает выполнение метода increment, он захватывает монитор объекта CommonResource. А все потоки также продолжают ожидать его освобождения.