1. Semaphore

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

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

Підбирати цей параметр потрібно індивідуально, залежно від потужності комп'ютера або ноутбука.


public class Main {

   public static void main(String[] args) {
       Semaphore sem = new Semaphore(1);
       CommonResource res = new CommonResource();
       new Thread(new MyThread(res, sem, "MyThread_1")).start();
       new Thread(new MyThread(res, sem, "MyThread_2")).start();
       new Thread(new MyThread(res, sem, "MyThread_3")).start();
   }
}

class CommonResource {
   int value = 0;
}

class MyThread implements Runnable {
   CommonResource commonResource;
   Semaphore semaphore;
   String name;
   MyThread(CommonResource commonResource, Semaphore sem, String name) {
       this.commonResource = commonResource;
       this.semaphore = sem;
       this.name = name;
   }

   public void run() {

       try {
           System.out.println(name + " очікує на дозвіл");
           semaphore.acquire();
           commonResource.value = 1;
           for (int i = 1; i < 7; i++) {
               System.out.println(this.name + ": " + commonResource.value);
               commonResource.value++;
               Thread.sleep(150);
           }
       } catch (InterruptedException e) {
           System.out.println(e.getMessage() + " " + name);
           Thread.currentThread().interrupt();
       }
       System.out.println(name + " вивільняє дозвіл");
       semaphore.release();
   }
}

2. CountDownLatch та інші

CountDownLatch — дозволяє кільком потокам очікувати, доки не завершиться певна кількість операцій, що виконуються в інших потоках. Як приклад можна уявити установку програми: вона не почнеться, доки ти не приймеш правила користування, доки не вибереш папку, куди встановлювати нову програму тощо. І тому є спеціальний метод countDown() — він зменшує лічильник count down на одиницю.

Щойно лічильник стає рівним нулю, всі потоки в await, що очікують, продовжать свою роботу, а всі подальші виклики await будуть проходити без очікувань. Лічильник count down одноразовий, і його не можна скинути до початкового стану.

CyclicBarrier – використовується для синхронізації вказаної кількості потоків в одній точці. Бар'єр досягається у той час, коли N-потоків викличуть метод await(...) і блокуються. Після цього лічильник скидається до вихідного значення, а потоки, що очікують, будуть звільнені. Додатково, якщо потрібно, існує можливість запуску спеціального коду до розблокування потоків та скидання лічильника. Для цього через конструктор передається об'єкт з реалізацією інтерфейсу Runnable.

Exchanger<V> — клас Exchanger призначений обмінюватись даними між потоками. Він є типізованим і упорядковує тип даних, якими потоки мають обмінюватись.

Обмін даними здійснюється за допомогою єдиного методу цього класу exchange():


V exchange(V x) throws InterruptedException
V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException

Параметр x представляє буфер даних обміну. Друга форма методу також визначає параметр timeout – час очікування та unit – тип тимчасових одиниць, що застосовуються для параметра timeout.

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

Зазвичай під час роботи з класом Phaser спочатку створюється його об'єкт. Далі нам треба зареєструвати всіх учасників. Для реєстрації для кожного учасника викликається метод register(). Можна обійтися і без цього методу, якщо передати необхідну кількість учасників до конструктора Phaser.

Потім кожен учасник виконує певний набір дій, що становлять фазу. А синхронізатор Phaser чекає, доки всі учасники не завершать виконання фази. Щоб повідомити синхронізатору, що фаза завершена, учасник повинен викликати метод arrive() або arriveAndAwaitAdvance(). Після цього синхронізатор переходить до наступної фази.