Что такое synchronized?
Можно применять как модификатор метода, и как самостоятельный оператор с блоком кода. Выполняет код при захваченном мониторе объекта. В виде оператора объект указывается явно. В виде модификатора нестатического метода используется this , статического – .class текущего класса.
Один из основных инструментов обеспечения потокобезопасности. Одновременно выполняется не более одного блока synchronized на одном и том же объекте. Такая блокировка называется intrinsic lock или monitor lock, подробно рассматривается в Java Concurrency in Practice 2.3.1.
Блок synchronized также необходим для использования методов wait, notify, notifyAll.
Synchronized 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=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. А все потоки также продолжают ожидать его освобождения.
Синхронизация потоков
В многопоточной программе не всегда один поток работает независимо от другого. Бывает, они обмениваются данными или обрабатывают одни и те же объекты.
В таких случаях возникают проблемы правильной организации взаимодействия нитей так, чтобы они не мешали работе друг друга. Один поток должен знать об изменениях, внесенных другим потоком.
Синхронизация потоков – это настройка их взаимодействия. Рассмотрим пример, в котором два разных потока работают с одним и тем же объектом:
public class NoSynch public static void main(String[] args) throws InterruptedException Client client = new Client(1000); Thread operation = new Operation(client, 1000); Thread operation1 = new Operation(client, 500); operation.start(); operation1.start(); operation.join(); operation1.join(); System.out.println(client.getBill()); > > class Operation extends Thread private Client mClient; private int mPay; Operation(Client client, int pay) mClient = client; mPay = pay; > @Override public void run() System.out.println(this.getName() + ": " + mClient.getBill()); if (mClient.getBill() - mPay >= 0) try sleep(1000); > catch (InterruptedException e) e.printStackTrace(); > mClient.changeBill(mPay); > System.out.println(this.getName() + ": " + mClient.getBill()); System.out.println(this.getName() + " stop"); > > class Client private int mBill; Client(int bill) this.mBill = bill; > int getBill() return mBill; > void changeBill(int pay) mBill -= pay; > >
Thread-0: 1000 Thread-1: 1000 Thread-0: 0 Thread-0 stop Thread-1: -500 Thread-1 stop -500
Задержка sleep’ом искусственно надумана, чтобы разделить проверку условия и вычитание. В реальных программах между одним действием и другим могло бы быть множество промежуточных, на выполнение которых требуется время.
У клиента на счету оказалась отрицательная сумма, хотя по логике вещей одна из операций вычитания не должна была бы выполняться. Проблема возникла из-за того, что в каждом потоке на момент проверки ( mClient. getBill ( ) — mPay >= 0 ) было по 1000 на счету. И каждый поток «решил», что денег достаточно. После этого поток-0 уменьшил сумму. Когда поток-1 приступил к вычитанию, денег на счету уже не было.
Чтобы исключить подобную логическую ошибку, надо как-то блокировать объект client, чтобы пока он обрабатывается в одном потоке, другие не могли его изменять. Как вариант делать это в методе в run():
class Operation extends Thread private final Client mClient; private int mPay; Operation(Client client, int pay) mClient = client; mPay = pay; > @Override public void run() System.out.println(this.getName() + ": " + mClient.getBill()); synchronized (mClient) if (mClient.getBill() - mPay >= 0) try sleep(1000); > catch (InterruptedException e) e.printStackTrace(); > mClient.changeBill(mPay); > > System.out.println(this.getName() + ": " + mClient.getBill()); System.out.println(this.getName() + " stop"); > >
Thread-0: 1000 Thread-1: 1000 Thread-0: 0 Thread-0 stop Thread-1: 0 Thread-1 stop 0
И хотя оба потока сначала увидели 1000. Когда началась проверка и вычитание, действия выполнялись совместно, другой поток в это время доступ к объекту не имел.
Блок кода с ключевым словом synchronized может одновременно выполняться только одним потоком.
В примере синхронизация выполнена по объекту. В Java также возможна синхронизация по методу, т. е. пока методом пользуется один поток, другому он недоступен. Если в вышеприведенной программе мы по отдельности синхронизируем методы getBill() и changeBill() объекта, это ничего не даст. Решением может быть объединение проверки и вычитания в один метод:
class Operation1 extends Thread private Client1 mClient; private int mPay; Operation1(Client1 client, int pay) mClient = client; mPay = pay; > @Override public void run() try sleep(1000); > catch (InterruptedException e) e.printStackTrace(); > mClient.changeBill(mPay); > > class Client1 private int mBill; Client1(int bill) this.mBill = bill; > synchronized void changeBill(int pay) System.out.println(mBill); if (mBill-pay >= 0) mBill -= pay; System.out.println(mBill); > > >
У каждой нити есть собственный кэш, куда копируются данные, с которыми она работает. Таким образом, одна нить не имеет доступа к данным другой и не может прочитать произошедшие изменения, а читает устаревшие значения из обычной памяти. Если потоки работают с общими данными, можно запретить их помещение в кэш, используя ключевое слово volatile. Например:
private volatile int mBill;
X Скрыть Наверх
Программирование на Java. Курс
Синхронизация в Java. Часть 1
Прежде чем перейти к самой синхронизации, я объясню многопоточность на примере простого кода.
Первым классом будет класс “Countdown”, а класс “ThreadColor” будет выглядеть вот так:
public class ThreadColor public static final
public static final String ANSI_RED = "\u001B[31m";
public static final String ANSI_GREEN = "\u001B[32m";
public static final String ANSI_YELLOW = "\u001b[33m";
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static final String ANSI_CYAN = "\u001B[36m";
public static final String ANSI_WHITE = "\u001b[37m";
>
Здесь я создал второй класс, расширяющий Thread:
Переходим к методу main, так как мы создали два класса, а затем запустили два потока из инструкции switch (рисунок 1), которая состоит из первого потока “Thread 1”, выводящего голубой текст, и второго потока “Thread 2”, выводящего фиолетовый текст.
Теперь давайте посмотрим, что происходит.
Здесь вы видите Thread1 в голубом цвете, а Thread2 в фиолетовом. Мы не можем предсказать, каков будет результат, т.е. порядок этих двух цветов. Можете заметить, что, повторяя выполнение кода, мы будем получать разный вывод.
А теперь мы добавим переменную экземпляра “Private int I;”, которая заменит локальную переменную “I”. Взглянем на результат:
Теперь он получился совсем иным. Вместо последовательного выполнения каждым потоком отсчёта от 10 до 1, мы видим, что некоторые числа повторяются.
Почему?
Очевидно, что повторяется число 10, а также несколько других. Единственное же, что мы сделали, — это поменяли локальную переменную на переменную экземпляра:
class Countdown private int i;
Куча — это память приложения, которая совместно используется всеми потоками, будучи разделённой на их стеки, которые представляют собой отдельные отсеки памяти для каждого потока.
Говоря проще, никакой поток не может обратиться к стеку другого потока, но при этом все потоки имеют доступ к куче. Локальная переменная хранится в стеке потока, поэтому каждый поток содержит свою собственную копию этой переменной, в то время как переменные экземпляра хранятся в куче.
Поэтому, когда несколько потоков работают совместно над одним объектом, они используют этот объект вместе. В этом случае если один поток изменяет значение, другой поток использует это изменённое значение. Аналогичным образом, когда “i” выступала в роли локальной переменной, потоки имели свои собственные версии этой переменной, но как только мы сделали “i” переменной экземпляра, два потока стали обращаться к этому общему ресурсу, хранящемуся в куче, поэтому каждый поток и пропускал некоторые числа.
Цикл for
Он уменьшает I на 1 и проверяет условие i>0. Суть цикла for заключается в выполнении нескольких шагов, а именно уменьшения, проверки и т.д. Отсюда получается, что поток может быть приостановлен между этими шагами. Он может быть приостановлен после уменьшения “i”, перед проверкой состояния или же сразу после выполнения всего кода и вывода результата в консоль. Уменьшение “i”, проверка условия и вывод в консоль значений: эти три шага могут послужить причиной остановки текущего потока.Как можно догадаться, в первой попытке оба потока рассматривали значение “i” как 10, поэтому Thread 1 вывел 9, но Thread 2 вывел 8. Почему?
В то время как Thread 1 выполнял цикл for, Thread 2 должно быть его опередил, получил значение “i” в виде 9, выполнил блок for и вывел 8.
При обращении к общим ресурсам, мы вынуждены пройти через эту ситуацию, которая называется “Thread interference” (коллизия потоков) или “Race condition” (состояние гонки). Помните, что всегда возникает серьёзная проблема, когда дело доходит до написания или обновления общего ресурса.
Мы можем сделать это без пропуска чисел или избежания коллизии, т.е. передать один и тот же объект Countdown обоим потокам.
public class Main public static void main(String[] args) Countdown countdown1 = new Countdown();
Countdown countdown2 = new Countdown(); CountdownThread t1 = new CountdownThread(countdown1);
t1.setName("Thread 1");
CountdownThread t2 = new CountdownThread(countdown2);
t2.setName("Thread 2"); t1.start();
t2.start();
>
>
Взгляните на экземпляр выше, где присутствуют два новых объекта для потоков, не использующих общую кучу. Проверьте результат и вы не увидите никакой коллизии. Каждый поток успешно выполняет отсчёт от 10 до 1.
Но главный вопрос в том, будет ли это применимо в реальных ситуациях? Будет ли это работать, к примеру, для счёта в банке, где кто-либо вносит на него деньги, в то время как вы снимаете некую сумму с банкомата? Отсюда следует, что нам нужно использовать одинаковый объект с целью поддержания целостности данных, поскольку это единственный способ, который позволяет нам знать точный баланс счёта в банке после выполнения нескольких потоков (транзакций). Ведь так?
В схожих ситуациях может одновременно присутствовать несколько потоков, ожидающих своей очереди на изменение баланса счёта. Следовательно, нам нужно позволить этим нескольким потокам изменить его, предотвратив при этом состояние гонки.
Синхронизация
Поскольку реальные приложения не могут использовать приведённую выше реализацию, нам нужно искать решение, которое не избегает состояния гонки в процессе изменения общего ресурса. Для этого мы можем добавить в объявление метода ключевое слово synchronized, что позволит синхронизировать этот метод:
class Countdown private int i;
public synchronized void doCountdown() String color;
>
После добавления ключевого слова synchronized весь метод выполняется до того, как к нему получит доступ другой поток.
Следовательно, в этом сценарии два потока никогда не столкнутся.
Но является ли этот способ единственным для предотвращения состояния гонки?
В принципе, мы можем добавить synchronized только в блок инструкции, а не для всего метода.
Каждый объект в Java имеет Intrinsic Lock (монитор). Когда синхронизированный метод вызывается из потока, ему нужно получить этот монитор. Монитор будет освобождён после того, как поток завершит выполнение метода. Таким образом, мы можем синхронизировать блок инструкций, работающий с объектом, принудив потоки получать монитор, прежде чем выполнять блок инструкций. Помните, что монитор одновременно может удерживаться только одним потоком, поэтому другие потоки, желающие получить его, будут приостановлены до завершения работы текущего потока. Только после этого конкретный ожидающий поток сможет получить монитор и продолжить выполнение.
Единственный блок кода метода “doCountdown”, в который мы можем добавить ключевое слово synchronized, — это блок “цикла for”. Итак, какой же объект нам следует использовать для синхронизации цикла for? Переменную “i”? Не думаю, потому что это примитивный тип, а не объект. Монитор же присутствует только в объектах. А что насчёт объекта “color”? Давайте просто удалим synchronized из объявления метода и добавим следующим образом:
synchronized (color) for(i=10; i > 0; i--) System.out.println(color + Thread.currentThread().getName() + ": i =" + i);
>
>
Вы видите тот же результат, что и на рисунке 5 в коде, где синхронизация не применялась. Почему же? Мы используем локальную переменную “color” для синхронизации. Как я уже пояснял выше относительно стеков потоков и прочего, использование локальной переменной здесь не работает, но объекты String переиспользуются внутри jvm, так как jvm для размещения строчных объектов использует пулы строк. Да, иногда это тоже может оказаться подходящим решением.
В качестве правила просто помните, что не нужно использовать локальную переменную для синхронизации.
Итак, давайте обновим синхронизированный блок кода таким образом:
synchronized (this) for(i=10; i > 0; i--) System.out.println(color + Thread.currentThread().getName() + ": i =" + i);
>
>
Взглянув на результат, вы увидите, что потоки не столкнутся и не пропустят числа. Блок цикла for одновременно может выполняться только одним потоком.
Кроме того, мы можем синхронизировать статические методы и использовать статические объекты.
- Основы программирования TCP-сокетов на Java
- Кто на свете всех сильнее — Java, Go и Rust в сравнении
- Java-Lombok: нужны ли геттеры и сеттеры?