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 .