- 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, если хотите больше интересных статей. Пишите свое мнение и идеи по этой теме в комментариях.