Перейти к основному содержимому

Использование объекта Mutex в Java

· 5 мин. чтения

1. Обзор

В этом руководстве мы увидим различные способы реализации мьютекса в Java .

2. Мьютекс

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

Мы называем этот сценарий состоянием гонки . И часть программы, которая обращается к общему ресурсу, называется критической секцией . Итак, чтобы избежать состояния гонки, нам нужно синхронизировать доступ к критической секции.

Мьютекс (или взаимное исключение) — это простейший тип синхронизатора — он гарантирует, что только один поток может одновременно выполнять критическую секцию компьютерной программы .

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

3. Почему мьютекс?

Во-первых, давайте рассмотрим пример класса SequenceGeneraror , который генерирует следующую последовательность, каждый раз увеличивая значение currentValue на единицу:

public class SequenceGenerator {

private int currentValue = 0;

public int getNextSequence() {
currentValue = currentValue + 1;
return currentValue;
}

}

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

@Test
public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
int count = 1000;
Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
Assert.assertEquals(count, uniqueSequences.size());
}

private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
Set<Integer> uniqueSequences = new LinkedHashSet<>();
List<Future<Integer>> futures = new ArrayList<>();

for (int i = 0; i < count; i++) {
futures.add(executor.submit(generator::getNextSequence));
}

for (Future<Integer> future : futures) {
uniqueSequences.add(future.get());
}

executor.awaitTermination(1, TimeUnit.SECONDS);
executor.shutdown();

return uniqueSequences;
}

Как только мы выполним этот тестовый пример, мы увидим, что в большинстве случаев он терпит неудачу по следующей причине:

java.lang.AssertionError: expected:<1000> but was:<989>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:645)

Предполагается , что uniqueSequences имеет размер, равный количеству раз, которое мы выполнили метод getNextSequence в нашем тестовом примере. Однако это не так из-за состояния гонки. Очевидно, мы не хотим такого поведения.

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

Существуют различные способы реализации мьютекса в Java. Итак, далее мы увидим различные способы реализации мьютекса для нашего класса SequenceGenerator .

4. Использование синхронизированного ключевого слова

Во-первых, мы обсудим ключевое слово synchronized , которое является самым простым способом реализации мьютекса в Java.

С каждым объектом в Java связана встроенная блокировка. Синхронизированный метод и синхронизированный блок используют эту встроенную блокировку , чтобы ограничить доступ к критической секции только одним потоком за раз. ``

Таким образом, когда поток вызывает синхронизированный метод или входит в синхронизированный блок, он автоматически получает блокировку. Блокировка снимается, когда метод или блок завершается или из них выбрасывается исключение.

Давайте изменим getNextSequence на наличие мьютекса, просто добавив ключевое слово synchronized :

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {

@Override
public synchronized int getNextSequence() {
return super.getNextSequence();
}

}

Синхронизированный блок подобен синхронизированному методу с большим контролем над критическим разделом и объектом, который мы можем использовать для блокировки.

Итак, давайте теперь посмотрим, как мы можем использовать синхронизированный блок для синхронизации пользовательского объекта мьютекса :

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {

private Object mutex = new Object();

@Override
public int getNextSequence() {
synchronized (mutex) {
return super.getNextSequence();
}
}

}

5. Использование ReentrantLock

Класс ReentrantLock был представлен в Java 1.5. Он обеспечивает большую гибкость и контроль, чем подход с синхронизированными ключевыми словами.

Давайте посмотрим, как мы можем использовать ReentrantLock для достижения взаимного исключения:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {

private ReentrantLock mutex = new ReentrantLock();

@Override
public int getNextSequence() {
try {
mutex.lock();
return super.getNextSequence();
} finally {
mutex.unlock();
}
}
}

6. Использование семафора

Как и ReentrantLock , класс Semaphore также был представлен в Java 1.5.

В то время как в случае мьютекса только один поток может получить доступ к критическому разделу, семафор позволяет фиксированному количеству потоков получить доступ к критическому разделу . Следовательно, мы также можем реализовать мьютекс, установив количество разрешенных потоков в семафоре равным одному .

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

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {

private Semaphore mutex = new Semaphore(1);

@Override
public int getNextSequence() {
try {
mutex.acquire();
return super.getNextSequence();
} catch (InterruptedException e) {
// exception handling code
} finally {
mutex.release();
}
}
}

7. Использование класса монитора Guava

До сих пор мы видели варианты реализации мьютекса с использованием функций, предоставляемых Java.

Однако класс Monitor библиотеки Google Guava является лучшей альтернативой классу ReentrantLock . Согласно его документации , код, использующий Monitor , более удобочитаем и менее подвержен ошибкам, чем код, использующий ReentrantLock .

Во-первых, мы добавим зависимость Maven для Guava :

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>

Теперь мы напишем еще один подкласс SequenceGenerator , используя класс Monitor :

public class SequenceGeneratorUsingMonitor extends SequenceGenerator {

private Monitor mutex = new Monitor();

@Override
public int getNextSequence() {
mutex.enter();
try {
return super.getNextSequence();
} finally {
mutex.leave();
}
}

}

8. Заключение

В этом уроке мы рассмотрели концепцию мьютекса. Кроме того, мы видели различные способы реализации этого в Java.

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