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. А все потоки также продолжают ожидать его освобождения.

Источник

Читайте также:  Python print array line by line
Оцените статью