Semaphoren in Java

1. Übersicht

In diesem kurzen Tutorial werden wir die Grundlagen von Semaphoren und Mutexen in Java untersuchen.

2. Semaphor

Wir beginnen mit java.util.concurrent.Semaphore. Wir können Semaphore verwenden, um die Anzahl gleichzeitiger Threads zu begrenzen, die auf eine bestimmte Ressource zugreifen.

Im folgenden Beispiel implementieren wir eine einfache Anmeldewarteschlange, um die Anzahl der Benutzer im System zu begrenzen:

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(); } }

Beachten Sie, wie wir die folgenden Methoden verwendet haben:

  • tryAcquire () - gibt true zurück, wenn eine Erlaubnis sofort verfügbar ist, und erwirbt sie, andernfalls gibt false zurück, aber purchase () erwirbt eine Erlaubnis und blockiert sie, bis eine verfügbar ist
  • release () - eine Genehmigung freigeben
  • availablePermits () - Gibt die Anzahl der aktuell verfügbaren Genehmigungen zurück

Um unsere Anmeldewarteschlange zu testen, versuchen wir zunächst, das Limit zu erreichen und zu prüfen, ob der nächste Anmeldeversuch blockiert wird:

@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()); }

Als nächstes werden wir sehen, ob nach dem Abmelden noch Slots verfügbar sind:

@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. Zeitgesteuertes Semaphor

Als nächstes werden wir Apache Commons TimedSemaphore diskutieren . TimedSemaphore erlaubt eine Reihe von Genehmigungen als einfaches Semaphor, aber in einem bestimmten Zeitraum wird nach diesem Zeitraum die Zeit zurückgesetzt und alle Genehmigungen werden freigegeben.

Wir können TimedSemaphore verwenden , um eine einfache Verzögerungswarteschlange wie folgt zu erstellen:

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(); } }

Wenn wir eine Verzögerungswarteschlange mit einer Sekunde als Zeitraum verwenden und nachdem alle Slots innerhalb einer Sekunde verwendet wurden, sollte keine verfügbar sein:

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()); }

Aber nach einiger Zeit des Schlafens sollte das Semaphor zurückgesetzt und die Genehmigungen freigegeben werden :

@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. Semaphor vs. Mutex

Mutex verhält sich ähnlich wie ein binäres Semaphor. Wir können es verwenden, um den gegenseitigen Ausschluss zu implementieren.

Im folgenden Beispiel verwenden wir ein einfaches binäres Semaphor, um einen Zähler zu erstellen:

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(); } }

Wenn viele Threads gleichzeitig versuchen, auf den Zähler zuzugreifen, werden sie einfach in einer Warteschlange blockiert :

@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()); }

Wenn wir warten, greifen alle Threads auf den Zähler zu und es verbleiben keine Threads in der Warteschlange:

@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. Schlussfolgerung

In diesem Artikel haben wir die Grundlagen von Semaphoren in Java untersucht.

Wie immer ist der vollständige Quellcode auf GitHub verfügbar.