Что такое семафор java
Перейти к содержимому

Что такое семафор java

  • автор:

Что делает семафор?

Семафор – один из старейших примитивов синхронизации. Он был изобретен Дейкстрой в 1968 году. По большому счету это счетчик, который можно увеличивать и уменьшать из разных потоков. Уменьшение до 0 блокирует уменьшающий поток. Состояние, когда счетчик больше нуля называют сигнальное состояние, операцию его увеличения – release (освобождение) или signal, уменьшения – acquire (захват) или wait.

На практике можно представить, что release – выделение квоты доступа к критической секции программы. acquire – использование необходимого объема доступной квоты, или ожидание, если её не хватает. Подробнее с деталями работы семафора поможет ознакомиться перевод статьи с картинками на хабре.

В Java семафор реализован классом Semaphore . Состоит этот класс в основном из разных форм методов acquire (с таймаутом, с игнорированием InterruptedException , неблокирующий) и release . Методы могут принимать параметр permits – тот самый объем квот, которые необходимо освободить/захватить.

Несколько вспомогательных методов позволяют узнать больше о количестве и составе очереди потоков, которые ждут освобождения пермитов. А методы availablePermits и drainPermits позволяют узнать количество оставшихся пермитов, и захватить их все соответственно. В конструкторе конфигурируются изначальное количество пермитов, и свойство fair (аналогичное свойству ReentrantLock).

Семафоры в Java

В этом кратком руководстве мы рассмотрим основы семафоров и мьютексов в Java.

2. Семафор

Начнем с java.util.concurrent.Semaphore. Мы можем использовать семафоры, чтобы ограничить количество одновременных потоков, обращающихся к определенному ресурсу.

В следующем примере мы реализуем простую очередь входа, чтобы ограничить количество пользователей в системе:

 class LoginQueueUsingSemaphore     private Semaphore semaphore;    public LoginQueueUsingSemaphore(int slotLimit)    semaphore = new Semaphore(slotLimit);   >    boolean tryLogin()    return semaphore.tryAcquire();   >    void logout()    semaphore.release();   >    int availableSlots()    return semaphore.availablePermits();   >    > 

Обратите внимание, как мы использовали следующие методы:

  • tryAcquire() — возвращает true, если разрешение доступно немедленно, и получает его, в противном случае возвращает false, ноacquire() получает разрешение и блокирует его до тех пор, пока оно не будет доступно.
  • release() — освободить разрешение
  • availablePermits() — возвращает количество доступных текущих разрешений

Чтобы проверить нашу очередь входа в систему, мы сначала попытаемся достичь предела и проверим, будет ли заблокирована следующая попытка входа:

 @Test   public void givenLoginQueue_whenReachLimit_thenBlocked()    int slots = 10;   ExecutorService executorService = Executors.newFixedThreadPool(slots);   LoginQueueUsingSemaphore loginQueue = new LoginQueueUsingSemaphore(slots);   IntStream.range(0, slots)   .forEach(user -> executorService.execute(loginQueue::tryLogin));   executorService.shutdown();    assertEquals(0, loginQueue.availableSlots());   assertFalse(loginQueue.tryLogin());   > 

Далее мы увидим, доступны ли какие-либо слоты после выхода из системы:

 @Test   public void givenLoginQueue_whenLogout_thenSlotsAvailable()    int slots = 10;   ExecutorService executorService = Executors.newFixedThreadPool(slots);   LoginQueueUsingSemaphore loginQueue = new LoginQueueUsingSemaphore(slots);   IntStream.range(0, slots)   .forEach(user -> executorService.execute(loginQueue::tryLogin));   executorService.shutdown();   assertEquals(0, loginQueue.availableSlots());   loginQueue.logout();    assertTrue(loginQueue.availableSlots() > 0);   assertTrue(loginQueue.tryLogin());   > 

3. Временный семафор

Далее мы обсудим Apache Commons TimedSemaphore. TimedSemaphore допускает ряд разрешений как простой семафор, но в течение заданного периода времени, после этого периода время сбрасывается и все разрешения освобождаются.

Мы можем использовать TimedSemaphore для создания простой очереди задержки следующим образом:

 class DelayQueueUsingTimedSemaphore     private TimedSemaphore semaphore;    DelayQueueUsingTimedSemaphore(long period, int slotLimit)    semaphore = new TimedSemaphore(period, TimeUnit.SECONDS, slotLimit);   >    boolean tryAdd()    return semaphore.tryAcquire();   >    int availableSlots()    return semaphore.getAvailablePermits();   >    > 

Когда мы используем очередь задержки с одной секундой в качестве периода времени и после использования всех слотов в течение одной секунды, ни один из них не должен быть доступен:

 public void givenDelayQueue_whenReachLimit_thenBlocked()    int slots = 50;   ExecutorService executorService = Executors.newFixedThreadPool(slots);   DelayQueueUsingTimedSemaphore delayQueue   = new DelayQueueUsingTimedSemaphore(1, slots);    IntStream.range(0, slots)   .forEach(user -> executorService.execute(delayQueue::tryAdd));   executorService.shutdown();    assertEquals(0, delayQueue.availableSlots());   assertFalse(delayQueue.tryAdd());   > 

Но после некоторого сна семафор должен сбросить и освободить разрешения :

 @Test   public void givenDelayQueue_whenTimePass_thenSlotsAvailable() throws InterruptedException    int slots = 50;   ExecutorService executorService = Executors.newFixedThreadPool(slots);   DelayQueueUsingTimedSemaphore delayQueue = new DelayQueueUsingTimedSemaphore(1, slots);   IntStream.range(0, slots)   .forEach(user -> executorService.execute(delayQueue::tryAdd));   executorService.shutdown();    assertEquals(0, delayQueue.availableSlots());   Thread.sleep(1000);   assertTrue(delayQueue.availableSlots() > 0);   assertTrue(delayQueue.tryAdd());   > 

4. Семафор против мьютекса

Мьютекс действует аналогично бинарному семафору, мы можем использовать его для реализации взаимного исключения.

В следующем примере мы будем использовать простой двоичный семафор для создания счетчика:

 class CounterUsingMutex     private Semaphore mutex;   private int count;    CounterUsingMutex()    mutex = new Semaphore(1);   count = 0;   >    void increase() throws InterruptedException    mutex.acquire();   this.count = this.count + 1;   Thread.sleep(1000);   mutex.release();    >    int getCount()    return this.count;   >    boolean hasQueuedThreads()    return mutex.hasQueuedThreads();   >   > 

Когда много потоков одновременно пытаются получить доступ к счетчику, они просто блокируются в очереди :

 @Test   public void whenMutexAndMultipleThreads_thenBlocked()   throws InterruptedException    int count = 5;   ExecutorService executorService  = Executors.newFixedThreadPool(count);   CounterUsingMutex counter = new CounterUsingMutex();   IntStream.range(0, count)   .forEach(user -> executorService.execute(() ->    try    counter.increase();   > catch (InterruptedException e)    e.printStackTrace();   >   >));   executorService.shutdown();    assertTrue(counter.hasQueuedThreads());   > 

Когда мы ждем, все потоки будут обращаться к счетчику, и в очереди не останется ни одного потока:

 @Test   public void givenMutexAndMultipleThreads_ThenDelay_thenCorrectCount()   throws InterruptedException    int count = 5;   ExecutorService executorService  = Executors.newFixedThreadPool(count);   CounterUsingMutex counter = new CounterUsingMutex();   IntStream.range(0, count)   .forEach(user -> executorService.execute(() ->    try    counter.increase();   > catch (InterruptedException e)    e.printStackTrace();   >   >));   executorService.shutdown();    assertTrue(counter.hasQueuedThreads());   Thread.sleep(5000);   assertFalse(counter.hasQueuedThreads());   assertEquals(count, counter.getCount());   > 

5. Вывод

В этой статье мы изучили основы семафоров в Java.

Как всегда, полный исходный код доступен на GitHub .

Что такое семафор java

Семафоры представляют еще одно средство синхронизации для доступа к ресурсу. В Java семафоры представлены классом Semaphore , который располагается в пакете java.util.concurrent .

Для управления доступом к ресурсу семафор использует счетчик, представляющий количество разрешений. Если значение счетчика больше нуля, то поток получает доступ к ресурсу, при этом счетчик уменьшается на единицу. После окончания работы с ресурсом поток освобождает семафор, и счетчик увеличивается на единицу. Если же счетчик равен нулю, то поток блокируется и ждет, пока не получит разрешение от семафора.

Установить количество разрешений для доступа к ресурсу можно с помощью конструкторов класса Semaphore:

Semaphore(int permits) Semaphore(int permits, boolean fair)

Параметр permits указывает на количество допустимых разрешений для доступа к ресурсу. Параметр fair во втором конструкторе позволяет установить очередность получения доступа. Если он равен true , то разрешения будут предоставляться ожидающим потокам в том порядке, в каком они запрашивали доступ. Если же он равен false , то разрешения будут предоставляться в неопределенном порядке.

Для получения разрешения у семафора надо вызвать метод acquire() , который имеет две формы:

void acquire() throws InterruptedException void acquire(int permits) throws InterruptedException

Для получения одного разрешения применяется первый вариант, а для получения нескольких разрешений — второй вариант.

После вызова этого метода пока поток не получит разрешение, он блокируется.

После окончания работы с ресурсом полученное ранее разрешение надо освободить с помощью метода release() :

void release() void release(int permits)

Первый вариант метода освобождает одно разрешение, а второй вариант — количество разрешений, указанных в permits.

Используем семафор в простом примере:

import java.util.concurrent.Semaphore; public class Program < public static void main(String[] args) < Semaphore sem = new Semaphore(1); // 1 разрешение CommonResource res = new CommonResource(); new Thread(new CountThread(res, sem, "CountThread 1")).start(); new Thread(new CountThread(res, sem, "CountThread 2")).start(); new Thread(new CountThread(res, sem, "CountThread 3")).start(); >> class CommonResource < int x=0; >class CountThread implements Runnable < CommonResource res; Semaphore sem; String name; CountThread(CommonResource res, Semaphore sem, String name)< this.res=res; this.sem=sem; this.name=name; >public void run() < try< System.out.println(name + " ожидает разрешение"); sem.acquire(); res.x=1; for (int i = 1; i < 5; i++)< System.out.println(this.name + ": " + res.x); res.x++; Thread.sleep(100); >> catch(InterruptedException e) System.out.println(name + " освобождает разрешение"); sem.release(); > >

Итак, здесь есть общий ресурс CommonResource с полем x, которое изменяется каждым потоком. Потоки представлены классом CountThread, который получает семафор и выполняет некоторые действия над ресурсом. В основном классе программы эти потоки запускаются. В итоге мы получим следующий вывод:

CountThread 1 ожидает разрешение CountThread 2 ожидает разрешение CountThread 3 ожидает разрешение CountThread 1: 1 CountThread 1: 2 CountThread 1: 3 CountThread 1: 4 CountThread 1 освобождает разрешение CountThread 3: 1 CountThread 3: 2 CountThread 3: 3 CountThread 3: 4 CountThread 3 освобождает разрешение CountThread 2: 1 CountThread 2: 2 CountThread 2: 3 CountThread 2: 4 CountThread 2 освобождает разрешение

Семафоры отлично подходят для решения задач, где надо ограничивать доступ. Например, классическая задача про обедающих философов. Ее суть: есть несколько философов, допустим, пять, но одновременно за столом могут сидеть не более двух. И надо, чтобы все философы пообедали, но при этом не возникло взаимоблокировки философами друг друга в борьбе за тарелку и вилку:

import java.util.concurrent.Semaphore; public class Program < public static void main(String[] args) < Semaphore sem = new Semaphore(2); for(int i=1;i<6;i++) new Philosopher(sem,i).start(); >> // класс философа class Philosopher extends Thread < Semaphore sem; // семафор. ограничивающий число философов // кол-во приемов пищи int num = 0; // условный номер философа int id; // в качестве параметров конструктора передаем идентификатор философа и семафор Philosopher(Semaphore sem, int id) < this.sem=sem; this.id=id; >public void run() < try < while(num<3)// пока количество приемов пищи не достигнет 3 < //Запрашиваем у семафора разрешение на выполнение sem.acquire(); System.out.println ("Философ " + id+" садится за стол"); // философ ест sleep(500); num++; System.out.println ("Философ " + id+" выходит из-за стола"); sem.release(); // философ гуляет sleep(500); >> catch(InterruptedException e) < System.out.println ("у философа " + id + " проблемы со здоровьем"); >> >

В итоге только два философа смогут одновременно находиться за столом, а другие будут ждать:

Философ 1 садится за стол Философ 3 садится за стол Философ 3 выходит из-за стола Философ 1 выходит из-за стола Философ 2 садится за стол Философ 4 садится за стол Философ 2 выходит из-за стола Философ 4 выходит из-за стола Философ 5 садится за стол Философ 1 садится за стол Философ 1 выходит из-за стола Философ 5 выходит из-за стола Философ 3 садится за стол Философ 2 садится за стол Философ 3 выходит из-за стола Философ 4 садится за стол Философ 2 выходит из-за стола Философ 5 садится за стол Философ 4 выходит из-за стола Философ 5 выходит из-за стола Философ 1 садится за стол Философ 3 садится за стол Философ 1 выходит из-за стола Философ 2 садится за стол Философ 3 выходит из-за стола Философ 5 садится за стол Философ 2 выходит из-за стола Философ 4 садится за стол Философ 5 выходит из-за стола Философ 4 выходит из-за стола

Java Dev Notes

Семафор используется для обмена сигналами между потоками, или же для охраны критической секции. Их также можно использовать и вместо локов. Несмотря на то, что в JDK уже реализован семафор (java.util.concurrent.Semaphore), полезно будет самим реализовать этот объект.

Итак, у нашего семафора будет всего лишь два метода: take, release . Соответственно, простейшая реализация будет такой:

public class SimpleSemaphore < boolean taken = false; public synchronized void take() < this.taken = true; this.notify(); >public synchronized void release() throws InterruptedException < while (!this.taken) wait(); this.taken = false; >>

Теперь давайте рассмотрим программу, которая использует семафор для обмена сигналами. У нас будет два потока: SignalSender, SignalReceiver , которые будут посылать друг другу сигналы. Вот код:

public class Main < public static void main(String[] args) throws InterruptedException < SimpleSemaphore semaphore = new SimpleSemaphore(); new Thread(new SignalSender(semaphore)).start(); Thread.currentThread().sleep(2000); new Thread(new SignalReceiver(semaphore)).start(); >static class SignalSender implements Runnable < private final SimpleSemaphore semaphore; public SignalSender(SimpleSemaphore semaphore) < this.semaphore = semaphore; >@Override public void run() < System.out.println("[SignalSender] run"); while (true) < try < doSomeWork(); semaphore.take(); >catch (InterruptedException e) < e.printStackTrace(); >> > private void doSomeWork() throws InterruptedException < System.out.println("[SignalSender] do some work"); Thread.sleep(500); >> static class SignalReceiver implements Runnable < private final SimpleSemaphore semaphore; public SignalReceiver(SimpleSemaphore semaphore) < this.semaphore = semaphore; >@Override public void run() < System.out.println("[SignalReceiver] run"); while (true) < try < semaphore.release(); doSomeWork(); >catch (InterruptedException e) < e.printStackTrace(); >> > private void doSomeWork() throws InterruptedException < System.out.println("[SignalReceiver] do some work"); Thread.sleep(700); >> >

Посмотрим на вывод консоли:

[SignalSender] run [SignalSender] do some work [SignalSender] do some work [SignalSender] do some work [SignalSender] do some work [SignalSender] do some work [SignalReceiver] run [SignalReceiver] do some work [SignalSender] do some work [SignalReceiver] do some work [SignalSender] do some work [SignalReceiver] do some work [SignalSender] do some work [SignalSender] do some work [SignalReceiver] do some work [SignalSender] do some work [SignalReceiver] do some work [SignalSender] do some work [SignalSender] do some work [SignalReceiver] do some work [SignalSender] do some work [SignalReceiver] do some work [SignalSender] do some work [SignalReceiver] do some work [SignalSender] do some work [SignalSender] do some work [SignalReceiver] do some work

Мы видим, как потоки обмениваются сигналами, при этом они ждут друг друга, и начинают работу, только получив соответствующий сигнал от семафора.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *