- 1. Критическая секция
- 1. Пример
- 2. Интерфейсный канал
- 1. Пример
- 3. События
Предлагаю рассмотреть несколько способов синхронизации потоков и процессов, наиболее часто используемых в Java. Они отличаются реализацией и вариантами использования. Мы рассмотрим все методы на интересных примерах.
Критическая секция
Этот метод подходит вам, если:
- параллельные потоки работают с общим ресурсом;
- требуется синхронизация между потоками, а не процессами;
Этот метод синхронизации называется синхронизацией ресурсов («синхронизация «открыть — закрыть»). Идея этого метода заключается в том, что каждый объект в Java имеет связанный с ним монитор. Монитор — это своего рода инструмент для управления доступом к объекту.
Синхронизированный оператор используется для создания критической секции. Когда выполнение кода достигает синхронизированного оператора, монитор объекта блокируется. В момент блокировки монопольный доступ к блоку кода имеет только один поток, выполнивший блокировку. После завершения работы блока кода объектный монитор освобождается и становится доступным для других потоков.
Когда монитор освобождается, он захватывается другим потоком, а все остальные потоки продолжают ждать его освобождения.
Пример
Представьте, как работает розничный интернет-магазин. После того, как один из покупателей положил товар в корзину, следует пересчитать оставшийся товар. Только после этого другой покупатель сможет положить в корзину желаемое количество товара. После этого купите их.
- public class Program {
- private static final Boolean key = true; //the object will be used for synchronization
- //it must be final
- private static Integer amountOfGoods = 20;
- public static class Buyer implements Runnable {
- @Override
- public void run() {
- final int goodsInTheShoppingCart = (int) (1 + Math.random() * amountOfGoods); //the number of goods that the buyer will put in the basket
- synchronized (key) {
- if (amountOfGoods - goodsInTheShoppingCart >= 0) {
- System.out.println("The " + Thread.currentThread().getName() + " placed " + goodsInTheShoppingCart + " items in the basket.");
- if ((amountOfGoods = amountOfGoods - goodsInTheShoppingCart) < 0) {
- System.out.println("Whoops! The product is out of stock, but the " + Thread.currentThread().getName() + " didn't know about it.");
- return;
- } else {
- System.out.println("There are " + amountOfGoods + " items left in the store.");
- }
- } else {
- System.out.println("The " + Thread.currentThread().getName() + " is notified that the goods are over.");
- return;
- }
- }
- System.out.println("The " + Thread.currentThread().getName() + " made a purchase.");
- }
- }
- public static void main(String[] args) {
- for (int i = 1; i <= 5; i++) {
- Thread t = new Thread(new Buyer()); // create five threads
- t.setName("buyer " + i);
- System.out.println("The " + t.getName() + " went to the store website.");
- t.start();
- }
- }
- }
Работа программы до использования блока синхронизации
Мы видим, что потоки выполняют программный код случайным образом. Из-за этого «покупатели» складывают товар в корзины, но остаются без товара.
Теперь добавим блок синхронизации и посмотрим, как изменится работа программы.
Теперь программа работает именно так, как мы и ожидали. Большой!
Интерфейсный канал
Вы можете использовать интерфейсный канал, если вам нужно синхронизировать различные процессы . Канал создается на уровне JVM и будет единым в системе.
Канал представляет собой открытое соединение с объектом, таким как аппаратное устройство, файл, сетевой сокет или программный компонент, который способен выполнять одну или несколько различных операций ввода-вывода, например чтение или запись.
Канал либо открыт, либо закрыт. Канал открыт при создании и после закрытия остается закрытым. Как только канал закрыт, любая попытка вызвать для него операцию ввода-вывода вызовет исключение ClosedChannelException . Открыт ли канал, можно проверить, вызвав его методы isOpen() и tryLock(). ).
Java package.nio.channels содержит такие каналы, как:
- AsynchronousChannel
- ByteChannel
- NetworkChannel
- FileChannel
- InterruptibleChannel
- MulticastChannel and other.
Пример
Два процесса записывают данные в файл. Они должны записывать поочередно, не мешая работе друг друга.
Создадим два проекта, отличающихся только тем, что они будут записаны в файл — «Программа 1» или «Программа 2». Мы запускаем их одновременно.
- public class Program {
- public static void main(String[] args) throws InterruptedException, IOException {
- File file = new File("C:\\text.txt");
- FileChannel fileChannel = new FileOutputStream(file, true).getChannel();
- while(fileChannel.tryLock() == null){ // the process checks if the channel is free to write to the file
- Thread.sleep(10000);
- }
- try {
- String lineSeparator = System.getProperty("line.separator");
- for (int i = 0; i < 10; i++) {
- String newData = "The program 2 is recording." + lineSeparator;
- ByteBuffer buf = ByteBuffer.allocate(200);
- buf.put(newData.getBytes());
- buf.flip();
- fileChannel.write(buf);
- Thread.sleep(1000);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- fileChannel.close();
- }
- }
Если бы мы использовали классический способ записи в файл с помощью FileWriter, мы бы нашли в файле что-то похожее.
Теперь посмотрим, что будет в файле, если мы будем использовать каналы.
Это именно то, что нам нужно! Процессы получают доступ к файлу один за другим.
События
Кроме синхронизации данных есть синхронизация по событиям. Когда одновременно запущенные потоки приостанавливаются до тех пор, пока какое-либо событие не будет инициировано другим потоком. Основными операциями для этого типа синхронизации являются ожидание и уведомление.
Это «ожидание-уведомление» синхронизации:
- параллельные потоки ждут некоторого события;
- один из потоков отвечает за возникновение события и уведомляет остальные потоки (один с помощью notify или все - notifyAll,) разблокируя их:
- подходит только для синхронизации потоков, а не процессов;
Пример
Представим, как работает автоматическая система перезарядки оружия. Например, на складе всего 15 патронов. Магазин вмещает только 6. Стрелок хочет использовать все патроны.
- public class Program {
- public static void main(String[] args) {
- WeaponStore weaponStore = new WeaponStore();
- Arsenal arsenal = new Arsenal(weaponStore);
- Shooter shooter = new Shooter(weaponStore);
- new Thread(arsenal).start();
- new Thread(shooter).start();
- }
- }
- class WeaponStore {
- private int cartridges = 0;
- public synchronized void shot() { // synchronization of the method is required
- // because two threads will have access to it
- while (cartridges < 1) {
- try {
- wait();
- }
- catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- //was thrown out notify by another thread, then the cartridge is received
- cartridges--;
- System.out.println("Produced 1 shot." + " Cartridges in the store: " + cartridges);
- notify();
- }
- public synchronized void reload(int arsenalCartriges) { // synchronization of the method is required
- // because two threads will have access to it
- while (cartridges >= 6) {
- try {
- wait();
- }
- catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- cartridges++;
- System.out.println("1 cartridge has been added to the store." + " Cartridges in stock: " + arsenalCartriges);
- notify();
- }
- }
- class Arsenal implements Runnable{
- private int cartridges = 15;
- private WeaponStore weapon;
- Arsenal(WeaponStore weapon){
- this.weapon = weapon;
- }
- public void run(){
- for (int cartridges = 15; cartridges > 0; cartridges--) {
- weapon.reload(cartridges);
- }
- }
- }
- class Shooter implements Runnable{
- private WeaponStore weapon;
- Shooter(WeaponStore weapon){
- this.weapon = weapon;
- }
- public void run(){
- for (int i = 1; i <= 15; i++) {
- weapon.shot();
- }
- }
- }
Результат программы
- 1 cartridge has been added to the store. Cartridges in stock: 15
- 1 cartridge has been added to the store. Cartridges in stock: 14
- 1 cartridge has been added to the store. Cartridges in stock: 13
- 1 cartridge has been added to the store. Cartridges in stock: 12
- 1 cartridge has been added to the store. Cartridges in stock: 11
- Produced 1 shot. Cartridges in the store: 4
- Produced 1 shot. Cartridges in the store: 3
- Produced 1 shot. Cartridges in the store: 2
- Produced 1 shot. Cartridges in the store: 1
- Produced 1 shot. Cartridges in the store: 0
- 1 cartridge has been added to the store. Cartridges in stock: 10
- 1 cartridge has been added to the store. Cartridges in stock: 9
- 1 cartridge has been added to the store. Cartridges in stock: 8
- 1 cartridge has been added to the store. Cartridges in stock: 7
- 1 cartridge has been added to the store. Cartridges in stock: 6
- 1 cartridge has been added to the store. Cartridges in stock: 5
- Produced 1 shot. Cartridges in the store: 5
- Produced 1 shot. Cartridges in the store: 4
- Produced 1 shot. Cartridges in the store: 3
- Produced 1 shot. Cartridges in the store: 2
- Produced 1 shot. Cartridges in the store: 1
- Produced 1 shot. Cartridges in the store: 0
- 1 cartridge has been added to the store. Cartridges in stock: 4
- 1 cartridge has been added to the store. Cartridges in stock: 3
- 1 cartridge has been added to the store. Cartridges in stock: 2
- 1 cartridge has been added to the store. Cartridges in stock: 1
- Produced 1 shot. Cartridges in the store: 3
- Produced 1 shot. Cartridges in the store: 2
- Produced 1 shot. Cartridges in the store: 1
- Produced 1 shot. Cartridges in the store: 0
Вывод
Как вы заметили, рассмотренные выше методы синхронизации имеют разное применение. Выбирайте подходящий и всегда синхронизируйте свои потоки и процессы, чтобы они жили мирно и никогда не мешали работе друг друга.
Подпишитесь на раздел Java, если хотите больше интересных статей. Пишите свое мнение и идеи по этой теме в комментариях.