Рина Сергеева
29 листопада 2018 р. 00:58

Кілька способів синхронізації процесів і потоків в Java

Пропоную розглянути кілька способів синхронізації потоків і процесів, що найчастіше використовуються в Java. Вони відрізняються реалізацією та варіантами використання. Ми розглянемо усі методи на цікавих прикладах.

Критична секція

Цей метод підходить вам, якщо:

  • паралельні потоки працюють із загальним ресурсом;
  • Потрібна синхронізація між потоками, а не процесами;

Цей метод синхронізації називається синхронізацією ресурсів (синхронізація «відкрити - закрити»). Ідея цього методу полягає в тому, що кожен об'єкт у Java має пов'язаний з ним монітор. Монітор - це свого роду інструмент управління доступом до об'єкта.


Синхронізований оператор використовується для створення критичної секції. Коли виконання коду досягає синхронізованого оператора, монітор об'єкта блокується. У момент блокування монопольний доступ до блоку коду має лише один потік, що здійснив блокування. Після завершення роботи блоку коду об'єктний монітор звільняється та стає доступним для інших потоків.

Коли монітор звільняється, він захоплюється іншим потоком, а решта потоків продовжують чекати його звільнення.

Приклад

Уявіть, як працює роздрібний інтернет-магазин. Після того, як один з покупців поклав товар у кошик, слід перерахувати товар, що залишився. Тільки після цього інший покупець зможе покласти в кошик бажану кількість товару. Після цього купіть їх.

  1. public class Program {
  2.  
  3. private static final Boolean key = true; //the object will be used for synchronization
  4. //it must be final
  5. private static Integer amountOfGoods = 20;
  6.  
  7.  
  8. public static class Buyer implements Runnable {
  9.  
  10. @Override
  11. public void run() {
  12. final int goodsInTheShoppingCart = (int) (1 + Math.random() * amountOfGoods); //the number of goods that the buyer will put in the basket
  13. synchronized (key) {
  14. if (amountOfGoods - goodsInTheShoppingCart >= 0) {
  15. System.out.println("The " + Thread.currentThread().getName() + " placed " + goodsInTheShoppingCart + " items in the basket.");
  16. if ((amountOfGoods = amountOfGoods - goodsInTheShoppingCart) < 0) {
  17. System.out.println("Whoops! The product is out of stock, but the " + Thread.currentThread().getName() + " didn't know about it.");
  18. return;
  19. } else {
  20. System.out.println("There are " + amountOfGoods + " items left in the store.");
  21. }
  22. } else {
  23. System.out.println("The " + Thread.currentThread().getName() + " is notified that the goods are over.");
  24. return;
  25. }
  26. }
  27. System.out.println("The " + Thread.currentThread().getName() + " made a purchase.");
  28. }
  29. }
  30.  
  31. public static void main(String[] args) {
  32.  
  33. for (int i = 1; i <= 5; i++) {
  34. Thread t = new Thread(new Buyer()); // create five threads
  35. t.setName("buyer " + i);
  36. System.out.println("The " + t.getName() + " went to the store website.");
  37. t.start();
  38. }
  39. }
  40. }

Робота програми до використання блоку синхронізації

Робота програми до використання блоку синхронізації

Ми бачимо, що потоки виконують програмний код випадковим чином. Через це "покупці" складають товар у кошики, але залишаються без товару.

Тепер додамо блок синхронізації та подивимося, як зміниться робота програми.

Робота програми з використанням блоку синхронізації

Тепер програма працює саме так, як ми очікували. Великий!

Інтерфейсний канал

Ви можете використовувати інтерфейсний канал, якщо вам потрібно синхронізувати різні процеси. Канал створюється на рівні JVM і буде єдиним у системі.

>Канал являє собою відкрите з'єднання з об'єктом, таким як апаратний пристрій, файл, мережний сокет або програмний компонент, який здатний виконувати одну або кілька різних операцій введення-виводу, наприклад, читання або запис.
> Канал або відкритий або закритий. Канал відкритий при створенні та після закриття залишається закритим. Як тільки канал закритий, будь-яка спроба викликати для нього операцію вводу-виводу викликає виняток ClosedChannelException . Чи відкритий канал можна перевірити, викликавши його методи isOpen() і tryLock(). ).

Java package.nio.channels містить такі канали, як:

  • Асинхронний канал
  • ByteChannel
  • Мережевий канал
  • Файловий канал
  • InterruptibleChannel
  • MulticastChannel та інші.

Приклад

Два процеси записують дані у файл. Вони мають записувати по черзі, не заважаючи роботі один одного.

Створимо два проекти, які відрізняються лише тим, що вони будуть записані у файл - "Програма 1" або "Програма 2". Ми запускаємо їх одночасно.

  1. public class Program {
  2.  
  3.  
  4. public static void main(String[] args) throws InterruptedException, IOException {
  5.  
  6. File file = new File("C:\\text.txt");
  7. FileChannel fileChannel = new FileOutputStream(file, true).getChannel();
  8.  
  9. while(fileChannel.tryLock() == null){ // the process checks if the channel is free to write to the file
  10. Thread.sleep(10000);
  11. }
  12. try {
  13. String lineSeparator = System.getProperty("line.separator");
  14.  
  15. for (int i = 0; i < 10; i++) {
  16.  
  17. String newData = "The program 2 is recording." + lineSeparator;
  18. ByteBuffer buf = ByteBuffer.allocate(200);
  19. buf.put(newData.getBytes());
  20. buf.flip();
  21. fileChannel.write(buf);
  22. Thread.sleep(1000);
  23. }
  24.  
  25. } catch (IOException e) {
  26. e.printStackTrace();
  27. }
  28. fileChannel.close();
  29. }
  30. }

Якби ми використовували класичний спосіб запису файл за допомогою FileWriter, ми знайшли б у файлі щось схоже.

Тепер подивимося, що буде у файлі, якщо ми використовуватимемо канали.

Це саме те, що нам потрібне! Процеси одержують доступ до файлу один за одним.

Події

Крім синхронізації даних, є синхронізація за подіями. Коли одночасно запущені потоки припиняються доти, доки будь-яка подія не буде ініційована іншим потоком. Основними операціями для цього типу синхронізації є очікування та сповіщення.

Це «очікування-повідомлення» синхронізації:

  • Паралельні потоки чекають деякої події;
  • один з потоків відповідає за виникнення події і повідомляє інші потоки (один за допомогою notify або всі - notifyAll,) розблоковуючи їх:
  • підходить тільки для синхронізації потоків, а чи не процесів;

Приклад

Уявимо, як працює автоматична система перезаряджання зброї. Наприклад, на складі лише 15 набоїв. Магазин вміщує лише 6. Стрілець хоче використовувати всі патрони.

  1. public class Program {
  2.  
  3. public static void main(String[] args) {
  4.  
  5. WeaponStore weaponStore = new WeaponStore();
  6. Arsenal arsenal = new Arsenal(weaponStore);
  7. Shooter shooter = new Shooter(weaponStore);
  8. new Thread(arsenal).start();
  9. new Thread(shooter).start();
  10. }
  11. }
  12.  
  13.  
  14. class WeaponStore {
  15.  
  16. private int cartridges = 0;
  17.  
  18. public synchronized void shot() { // synchronization of the method is required
  19. // because two threads will have access to it
  20. while (cartridges < 1) {
  21. try {
  22. wait();
  23. }
  24. catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. //was thrown out notify by another thread, then the cartridge is received
  29. cartridges--;
  30. System.out.println("Produced 1 shot." + " Cartridges in the store: " + cartridges);
  31. notify();
  32. }
  33.  
  34. public synchronized void reload(int arsenalCartriges) { // synchronization of the method is required
  35. // because two threads will have access to it
  36. while (cartridges >= 6) {
  37. try {
  38. wait();
  39. }
  40. catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. cartridges++;
  45. System.out.println("1 cartridge has been added to the store." + " Cartridges in stock: " + arsenalCartriges);
  46. notify();
  47. }
  48. }
  49.  
  50. class Arsenal implements Runnable{
  51.  
  52. private int cartridges = 15;
  53.  
  54. private WeaponStore weapon;
  55.  
  56. Arsenal(WeaponStore weapon){
  57. this.weapon = weapon;
  58. }
  59.  
  60. public void run(){
  61. for (int cartridges = 15; cartridges > 0; cartridges--) {
  62. weapon.reload(cartridges);
  63. }
  64. }
  65. }
  66.  
  67.  
  68. class Shooter implements Runnable{
  69.  
  70. private WeaponStore weapon;
  71.  
  72. Shooter(WeaponStore weapon){
  73. this.weapon = weapon;
  74. }
  75.  
  76. public void run(){
  77. for (int i = 1; i <= 15; i++) {
  78. weapon.shot();
  79. }
  80. }
  81. }

Результат програми

  1. 1 cartridge has been added to the store. Cartridges in stock: 15
  2. 1 cartridge has been added to the store. Cartridges in stock: 14
  3. 1 cartridge has been added to the store. Cartridges in stock: 13
  4. 1 cartridge has been added to the store. Cartridges in stock: 12
  5. 1 cartridge has been added to the store. Cartridges in stock: 11
  6. Produced 1 shot. Cartridges in the store: 4
  7. Produced 1 shot. Cartridges in the store: 3
  8. Produced 1 shot. Cartridges in the store: 2
  9. Produced 1 shot. Cartridges in the store: 1
  10. Produced 1 shot. Cartridges in the store: 0
  11. 1 cartridge has been added to the store. Cartridges in stock: 10
  12. 1 cartridge has been added to the store. Cartridges in stock: 9
  13. 1 cartridge has been added to the store. Cartridges in stock: 8
  14. 1 cartridge has been added to the store. Cartridges in stock: 7
  15. 1 cartridge has been added to the store. Cartridges in stock: 6
  16. 1 cartridge has been added to the store. Cartridges in stock: 5
  17. Produced 1 shot. Cartridges in the store: 5
  18. Produced 1 shot. Cartridges in the store: 4
  19. Produced 1 shot. Cartridges in the store: 3
  20. Produced 1 shot. Cartridges in the store: 2
  21. Produced 1 shot. Cartridges in the store: 1
  22. Produced 1 shot. Cartridges in the store: 0
  23. 1 cartridge has been added to the store. Cartridges in stock: 4
  24. 1 cartridge has been added to the store. Cartridges in stock: 3
  25. 1 cartridge has been added to the store. Cartridges in stock: 2
  26. 1 cartridge has been added to the store. Cartridges in stock: 1
  27. Produced 1 shot. Cartridges in the store: 3
  28. Produced 1 shot. Cartridges in the store: 2
  29. Produced 1 shot. Cartridges in the store: 1
  30. Produced 1 shot. Cartridges in the store: 0

Висновок

Як ви помітили, розглянуті методи синхронізації мають різне застосування. Вибирайте відповідний і завжди синхронізуйте свої потоки та процеси, щоб вони жили мирно і ніколи не заважали роботі один одного.

Підпишіться на розділ Java, якщо хочете більше цікавих статей. Пишіть свою думку та ідеї на цю тему в коментарях.

По статті запитували0питання

1

Вам це подобається? Поділіться в соціальних мережах!

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up
  • Останні коментарі
  • Evgenii Legotckoi
    16 квітня 2025 р. 17:08
    Благодарю за отзыв. И вам желаю всяческих успехов!
  • IscanderChe
    12 квітня 2025 р. 17:12
    Добрый день. Спасибо Вам за этот проект и отдельно за ответы на форуме, которые мне очень помогли в некоммерческих пет-проектах. Профессиональным программистом я так и не стал, но узнал мно…
  • AK
    01 квітня 2025 р. 11:41
    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
  • Evgenii Legotckoi
    09 березня 2025 р. 21:02
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    09 березня 2025 р. 16:14
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…