- 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 містить такі канали, як:
- Асинхронний канал
- ByteChannel
- Мережевий канал
- Файловий канал
- InterruptibleChannel
- MulticastChannel та інші.
Приклад
Два процеси записують дані у файл. Вони мають записувати по черзі, не заважаючи роботі один одного.
Створимо два проекти, які відрізняються лише тим, що вони будуть записані у файл - "Програма 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, якщо хочете більше цікавих статей. Пишіть свою думку та ідеї на цю тему в коментарях.