- Несколько способов синхронизации процессов и потоков в Java
- Критические секции
- Пример
- Interface Channel
- Пример
- События
- Пример
- Заключение
- Справочник по синхронизаторам java.util.concurrent.*
- Semaphore
- CountDownLatch
- CyclicBarrier
- Exchanger
- Phaser
- Синхронизация потоков
- 1. Что такое синхронизация?
- Пример 1. Одновременный доступ к ресурсу
- 2. Способы синхронизации кода
- 3. Модификатор volatile
- 4. Монитор
- Пример 2. Синхронизация доступа к ресурсу
- 5. Синхронизация статических методов
- Пример 3. Синхронизация статических методов
- 6. Блокировка
- Варианты блокировки:
- 7. Методы и состояние блокировки
Несколько способов синхронизации процессов и потоков в Java
Предлагаю рассмотреть несколько способов синхронизации потоков и процессов, которые наиболее часто используются в Java. Они отличаются реализацией и случаями применения. Все методы рассмотрим на интересных примерах.
Критические секции
Данный метод подойдет вам, если:
- параллельные потоки работают с общим ресурсом;
- требуется синхронизация между потоками, а не процессами;
Данный метод синхронизации называют синхронизацией по ресурсам (синхронизация типа “открыть – закрыть”). Идея данного метода состоит в том, что каждый объект в Java имеет ассоциированный с ним монитор. Монитор представляет своего рода инструмент для управления доступа к объекту.
Для создания критической секции используется оператор synchronized . Когда выполнение кода доходит до оператора synchronized , монитор объекта блокируется, и на время его блокировки монопольный доступ к блоку кода имеет только один поток, который и произвел блокировку. После окончания работы блока кода, монитор объекта освобождается и становится доступным для других потоков.
После освобождения монитора его захватывает другой поток, а все остальные потоки продолжают ожидать его освобождения.
Пример
Представим как работает розничный онлайн-магазин. После того, как один из покупателей положил товар в корзину, должен произойти подсчет оставшихся товаров. Только после этого другой покупатель сможет положить в корзину желаемое количество товаров. После чего, их купить.
Работа программы до использования блока синхронизации
Мы видим, что потоки выполняют код программы рандомно. Из-за этого «покупатели» кладут товар в корзины, но остаются без товаров.
Теперь добавим блок синхронизации и посмотрим, как изменится работа программы.
Теперь программа работает точно так, как мы и предполагали. Отлично!
Interface Channel
Вы можете использовать Interface Channel в случае, если вам нужно синхронизировать разные процессы . Канал создается на уровне JVM и будет един в системе.
Канал (Channel) представляет собой открытое соединение с объектом, таким как аппаратное устройство, файл, сетевой сокет или программный компонент, способный выполнять одну или несколько отдельных операций ввода-вывода, например чтение или запись.
Канал может быть «открыт» или «закрыт». Как только канал закрыт, любая попытка вызвать его приведет к выбросу ClosedChannelException. Открыт ли канал можно проверить, вызывав метод isOpen( ) или tryLock( ).
Пакет java.nio.channels содержит такие каналы, как:
- AsynchronousChannel
- ByteChannel
- NetworkChannel
- FileChannel
- InterruptibleChannel
- MulticastChannel и другие.
Пример
Два процесса записывают данные в файл. Они должны производить запись поочередно, не вмешиваясь, в работу друг друга.
Создадим два проекта, отличающиеся только тем, что они будут записывать в файл — «Программа 1» или «Программа 2». Запускать будем их одновременно.
Если бы мы использовали классический способ записи в файл с помощью FileWriter, то в файле бы обнаружили что-то подобное.
Теперь посмотрим, что будет в файле, если использовать каналы.
Это то, что нам нужно! Процессы захватывают доступ к файлу поочередно.
События
Кроме синхронизации по данным имеется синхронизация по событиям. Когда параллельно выполняющиеся потоки приостанавливаются вплоть до наступления некоторого события, о котором им сигнализирует другой поток. Основными операциями при таком типе синхронизации являются wait («ждать») и notify («оповестить»).
Это синхронизация типа “ждать – пропустить”:
- параллельные потоки ждут наступления некоторого события (операция ждать);
- один из потоков отвечает за наступление события и оповещает о нем остальные потоки(один, используя notify или все — notifyAll,) разблокируя их (операция пропустить).
- подходит только для синхронизации потоков, не процессов;
Пример
Представим как работает система автоматической перезарядки оружия. Например, всего имеется 15 патронов в запасе. В магазине помещается только 6. Стрелок хочет использовать все патроны.
Результат работы программы
Заключение
Как вы заметили, рассмотренные выше методы синхронизации имеют разные применения. Выбирайте подходящий и всегда синхронизируйте свои потоки и процессы, чтобы они жили мирно и никогда не вмешивались в работу друг друга.
Подписывайтесь на раздел Java, если хотите больше интересных статей. Пишите свое мнение и идеи, касательно данной темы, в комментариях.
Источник
Справочник по синхронизаторам java.util.concurrent.*
Целью данной публикации не является полный анализ синхронизаторов из пакета java.util.concurrent. Пишу её, прежде всего, как справочник, который облегчит вхождение в тему и покажет возможности практического применения классов для синхронизации потоков (далее поток = thread).
В java.util.concurrent много различных классов, которые по функционалу можно поделить на группы: Concurrent Collections, Executors, Atomics и т.д. Одной из этих групп будет Synchronizers (синхронизаторы).
Синхронизаторы – вспомогательные утилиты для синхронизации потоков, которые дают возможность разработчику регулировать и/или ограничивать работу потоков и предоставляют более высокий уровень абстракции, чем основные примитивы языка (мониторы).
Semaphore
Синхронизатор Semaphore реализует шаблон синхронизации Семафор. Чаще всего, семафоры необходимы, когда нужно ограничить доступ к некоторому общему ресурсу. В конструктор этого класса ( Semaphore(int permits) или Semaphore(int permits, boolean fair) ) обязательно передается количество потоков, которому семафор будет разрешать одновременно использовать заданный ресурс.
Доступ управляется с помощью счётчика: изначально значение счётчика равно int permits , когда поток заходит в заданный блок кода, то значение счётчика уменьшается на единицу, когда поток его покидает, то увеличивается. Если значение счётчика равно нулю, то текущий поток блокируется, пока кто-нибудь не выйдет из блока (в качестве примера из жизни с permits = 1 , можно привести очередь в кабинет в поликлинике: когда пациент покидает кабинет, мигает лампа, и заходит следующий пациент).
Рассмотрим следующий пример. Существует парковка, которая одновременно может вмещать не более 5 автомобилей. Если парковка заполнена полностью, то вновь прибывший автомобиль должен подождать пока не освободится хотя бы одно место. После этого он сможет припарковаться.
CountDownLatch
CountDownLatch (замок с обратным отсчетом) предоставляет возможность любому количеству потоков в блоке кода ожидать до тех пор, пока не завершится определенное количество операций, выполняющихся в других потоках, перед тем как они будут «отпущены», чтобы продолжить свою деятельность. В конструктор CountDownLatch ( CountDownLatch(int count) ) обязательно передается количество операций, которое должно быть выполнено, чтобы замок «отпустил» заблокированные потоки.
Блокировка потоков снимается с помощью счётчика: любой действующий поток, при выполнении определенной операции уменьшает значение счётчика. Когда счётчик достигает 0, все ожидающие потоки разблокируются и продолжают выполняться (примером CountDownLatch из жизни может служить сбор экскурсионной группы: пока не наберется определенное количество человек, экскурсия не начнется).
Рассмотрим следующий пример. Мы хотим провести автомобильную гонку. В гонке принимают участие пять автомобилей. Для начала гонки нужно, чтобы выполнились следующие условия:
- Каждый из пяти автомобилей подъехал к стартовой прямой;
- Была дана команда «На старт!»;
- Была дана команда «Внимание!»;
- Была дана команда «Марш!».
Важно, чтобы все автомобили стартовали одновременно.
CyclicBarrier
CyclicBarrier реализует шаблон синхронизации Барьер. Циклический барьер является точкой синхронизации, в которой указанное количество параллельных потоков встречается и блокируется. Как только все потоки прибыли, выполняется опционное действие (или не выполняется, если барьер был инициализирован без него), и, после того, как оно выполнено, барьер ломается и ожидающие потоки «освобождаются». В конструктор барьера ( CyclicBarrier(int parties) и CyclicBarrier(int parties, Runnable barrierAction) ) обязательно передается количество сторон, которые должны «встретиться», и, опционально, действие, которое должно произойти, когда стороны встретились, но перед тем когда они будут «отпущены».
Барьер похож на CountDownLatch, но главное различие между ними в том, что вы не можете заново использовать «замок» после того, как его счётчик достигнет нуля, а барьер вы можете использовать снова, даже после того, как он сломается. CyclicBarrier является альтернативой метода join() , который «собирает» потоки только после того, как они выполнились.
Рассмотрим следующий пример. Существует паромная переправа. Паром может переправлять одновременно по три автомобиля. Чтобы не гонять паром лишний раз, нужно отправлять его, когда у переправы соберется минимум три автомобиля.
Exchanger
Exchanger (обменник) может понадобиться, для того, чтобы обменяться данными между двумя потоками в определенной точки работы обоих потоков. Обменник — обобщенный класс, он параметризируется типом объекта для передачи.
Обменник является точкой синхронизации пары потоков: поток, вызывающий у обменника метод exchange() блокируется и ждет другой поток. Когда другой поток вызовет тот же метод, произойдет обмен объектами: каждая из них получит аргумент другой в методе exchange() . Стоит отметить, что обменник поддерживает передачу null значения. Это дает возможность использовать его для передачи объекта в одну сторону, или, просто как точку синхронизации двух потоков.
Рассмотрим следующий пример. Есть два грузовика: один едет из пункта A в пункт D, другой из пункта B в пункт С. Дороги AD и BC пересекаются в пункте E. Из пунктов A и B нужно доставить посылки в пункты C и D. Для этого грузовики в пункте E должны встретиться и обменяться соответствующими посылками.
Phaser
Phaser (фазер), как и CyclicBarrier, является реализацией шаблона синхронизации Барьер, но, в отличии от CyclicBarrier, предоставляет больше гибкости. Этот класс позволяет синхронизировать потоки, представляющие отдельную фазу или стадию выполнения общего действия. Как и CyclicBarrier, Phaser является точкой синхронизации, в которой встречаются потоки-участники. Когда все стороны прибыли, Phaser переходит к следующей фазе и снова ожидает ее завершения.
Если сравнить Phaser и CyclicBarrier, то можно выделить следующие важные особенности Phaser:
- Каждая фаза (цикл синхронизации) имеет номер;
- Количество сторон-участников жестко не задано и может меняться: поток может регистрироваться в качестве участника и отменять свое участие;
- Участник не обязан ожидать, пока все остальные участники соберутся на барьере. Чтобы продолжить свою работу достаточно сообщить о своем прибытии;
- Случайные свидетели могут следить за активностью в барьере;
- Поток может и не быть стороной-участником барьера, чтобы ожидать его преодоления;
- У фазера нет опционального действия.
Объект Phaser создается с помощью одного из конструкторов:
Параметр parties указывает на количество сторон-участников, которые будут выполнять фазы действия. Первый конструктор создает объект Phaser без каких-либо сторон, при этом барьер в этом случае тоже «закрыт». Второй конструктор регистрирует передаваемое в конструктор количество сторон. Барьер открывается когда все стороны прибыли, или, если снимается последний участник. (У класса Phaser еще есть конструкторы, в которые передается родительский объект Phaser, но мы их рассматривать не будем.)
Основные методы:
- int register() — регистрирует нового участника, который выполняет фазы. Возвращает номер текущей фазы;
- int getPhase() — возвращает номер текущей фазы;
- int arriveAndAwaitAdvance() — указывает что поток завершил выполнение фазы. Поток приостанавливается до момента, пока все остальные стороны не закончат выполнять данную фазу. Точный аналог CyclicBarrier.await() . Возвращает номер текущей фазы;
- int arrive() — сообщает, что сторона завершила фазу, и возвращает номер фазы. При вызове данного метода поток не приостанавливается, а продолжает выполнятся;
- int arriveAndDeregister() — сообщает о завершении всех фаз стороной и снимает ее с регистрации. Возвращает номер текущей фазы;
- int awaitAdvance(int phase) — если phase равно номеру текущей фазы, приостанавливает вызвавший его поток до её окончания. В противном случае сразу возвращает аргумент.
Официальная документация по Phaser.
Рассмотрим следующий пример. Есть пять остановок. На первых четырех из них могут стоять пассажиры и ждать автобуса. Автобус выезжает из парка и останавливается на каждой остановке на некоторое время. После конечной остановки автобус едет в парк. Нам нужно забрать пассажиров и высадить их на нужных остановках.
Если кому-нибудь пригодилось, то я очень рад=)
Более подробно о Phaser здесь.
Почитать ещё о синхронизаторах и посмотреть примеры можно здесь.
Источник
Синхронизация потоков
1. Что такое синхронизация?
Все потоки, принадлежащие одному процессу, разделяют некоторые общие ресурсы (адресное пространство, открытые файлы). Что произойдет, если один поток еще не закончил работать с каким-либо общим ресурсом, а система переключилась на другой поток, использующий тот же ресурс?
Когда два или более потоков имеют доступ к одному разделенному ресурсу, они нуждаются в обеспечении того, что ресурс будет использован только одним потоком одновременно. Процесс, с помощью которого это достигается, называется синхронизацией.
Пример 1. Одновременный доступ к ресурсу
Два потока в предыдущем примере находятся в состоянии гонок. Состояние гонок – это одновременный вызов в потоках исполнения одного и того же метода для того же самого объекта.
Чтобы защитить данные, нам необходимо выполнить два действия:
- Объявить переменные как private.
- Синхронизировать код.
2. Способы синхронизации кода
Синхронизировать прикладной код можно двумя способами:
- С помощью синхронизированных методов. Метод объявляется с использованием ключевого слова synchronized:
- Заключить вызовы методов в блок оператора synchronized:
Только методы и блоки могут быть синхронизированы, но не переменные и классы.
Не все методы в классе должны быть синхронизированы.
3. Модификатор volatile
Поток создается с чистой рабочей памятью, и должен перед использованием загрузить все необходимые переменные из основного хранилища (можно сказать что он имеет некий кэш).
Любая переменная сначала создается в основном хранилище и лишь затем копируется в рабочую память потоков, которые будут ее применять.
Если переменная объявлена, как volatile, то ее чтение и запись будет производиться из\в основное хранилище.
Чтение volatile переменных синхронизировано и запись в volatile переменные синхронизирована, а неатомарные операции – нет.
4. Монитор
Каждый объект в Java имеет ассоциированный с ним монитор. Монитор — это объект, используемый в качестве взаимоисключающей блокировки. Когда поток исполнения запрашивает блокировку, то говорят, что он входит в монитор.
Только один поток исполнения может в одно, и то же время владеть монитором. Все другие потоки исполнения, пытающиеся войти в заблокированный монитор, будут приостановлены до тех пор, пока первый поток не выйдет из монитора. Говорят, что они ожидают монитор.
Поток, владеющий монитором, может, если пожелает, повторно войти в него.
Если поток засыпает, то он удерживает монитор.
Поток может захватить сразу несколько мониторов.
Рассмотрим разницу между доступом к объекту без синхронизации и из синхронизированного кода. Доступ к банковскому счету без синхронизации:
И с синхронизацией:
Когда выполнение кода доходит до оператора synchronized, монитор объекта счет блокируется, и на время его блокировки монопольный доступ к блоку кода имеет только один поток, который и произвел блокировку (Люси).
После окончания работы блока кода, монитор объекта счет освобождается и становится доступным для других потоков.
После освобождения монитора его захватывает другой поток, а все остальные потоки продолжают ожидать его освобождения.
Синхронизируем методы из предыдущего примера для корректного совместного доступа потоков:
Пример 2. Синхронизация доступа к ресурсу
5. Синхронизация статических методов
Статические методы тоже могут быть синхронизированы с помощью ключевого слова synchronized.
Для синхронизации статических методов используется один монитор для одного класса. Каждый загруженный в Java класс имеет соответствующий объект класса Class, представляющий этот класс. Монитор именно этого объекта используется для синхронизации статических методов (если они синхронизированы).
Пример 3. Синхронизация статических методов
И эквивалентный код:
6. Блокировка
Если поток пытается зайти в синхронизированный метод, а монитор уже захвачен, то поток блокируется по монитору объекта.
Поток попадает в специальный пул для этого конкретного объекта и должен находиться там пока монитор не будет освобожден. После этого поток возвращается в состояние runnable.
Варианты блокировки:
- Потоки, вызывающие нестатические синхронизированные методы одного и того же класса, будут блокировать друг друга только если они вызваны для одного объекта.
- Потоки, вызывающие статические синхронизированные методы одного класса, будут всегда блокировать друг друга. Они блокируются по монитору Class объекта. Статические синхронизированные и нестатические синхронизированные методы не будут блокировать друг друга никогда.
- Для синхронизированных блоков нужно смотреть какой объект используется для синхронизации.
- Блоки синхронизированные по одному объекту будут блокировать друг друга.
7. Методы и состояние блокировки
Освобождают монитор
Удерживают монитор
Класс определяющий метод
Источник