Автор оригинала: Kamlesh Kumar.
1. Обзор
В этом уроке мы увидим различные способы реализации мьютекса в Java .
2. Мьютекс
В многопоточном приложении двум или более потокам может потребоваться доступ к общему ресурсу одновременно, что приводит к неожиданному поведению. Примерами таких общих ресурсов являются структуры данных, устройства ввода-вывода, файлы и сетевые подключения.
Мы называем этот сценарий условием гонки . И часть программы, которая обращается к общему ресурсу, известна как критический раздел . Итак, чтобы избежать состояния гонки, нам нужно синхронизировать доступ к критическому разделу.
Мьютекс (или взаимное исключение) – это самый простой тип синхронизатора – он гарантирует, что только один поток может одновременно выполнять критическую часть компьютерной программы .
Чтобы получить доступ к критическому разделу, поток получает мьютекс, затем обращается к критическому разделу и, наконец, освобождает мьютекс. Тем временем все остальные потоки блокируются до тех пор, пока мьютекс не освободится. Как только поток выходит из критического раздела, другой поток может войти в критический раздел.
3. Почему мьютекс?
Во-первых, давайте возьмем пример Генератора последовательностей класса, который генерирует следующую последовательность, увеличивая 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 uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
Assert.assertEquals(count, uniqueSequences.size());
}
private Set getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
Set uniqueSequences = new LinkedHashSet<>();
List> futures = new ArrayList<>();
for (int i = 0; i < count; i++) {
futures.add(executor.submit(generator::getNextSequence));
}
for (Future 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)
Предполагается, что уникальные последовательности имеют размер, равный количеству раз, когда мы выполняли метод getNextSequence в нашем тестовом примере. Однако это не так из – за состояния гонки. Очевидно, что мы не хотим такого поведения.
Таким образом, чтобы избежать таких условий гонки, нам нужно убедиться, что только один поток может выполнять метод getNextSequence одновременно . В таких сценариях мы можем использовать мьютекс для синхронизации потоков.
Существуют различные способы реализации мьютекса в Java. Итак, далее мы рассмотрим различные способы реализации мьютекса для нашего генератора последовательностей класса.
4. Использование синхронизированного ключевого слова
Во-первых, мы обсудим ключевое слово synchronized , которое является самым простым способом реализации мьютекса в Java.
Каждый объект в Java имеет внутреннюю блокировку, связанную с ним. Метод /synchronized и | блок synchronized используют эту встроенную блокировку для ограничения доступа к критической секции только одним потоком за раз.
Поэтому, когда поток вызывает метод synchronized или вводит блок synchronized , он автоматически получает блокировку. Блокировка снимается, когда метод или блок завершаются или из них выбрасывается исключение.
Давайте изменим getNextSequence на мьютекс, просто добавив ключевое слово synchronized :
public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
@Override
public synchronized int getNextSequence() {
return super.getNextSequence();
}
}Блок synchronized аналогичен методу synchronized , с большим контролем над критической секцией и объектом, который мы можем использовать для блокировки.
Итак, давайте теперь посмотрим, как мы можем использовать блок synchronized для синхронизации на пользовательском объекте мьютекса :
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. Он обеспечивает большую гибкость и контроль, чем подход synchronized keyword.
Давайте посмотрим, как мы можем использовать 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.
В то время как в случае мьютекса только один поток может получить доступ к критическому разделу, Семафор позволяет фиксированному числу потоков получить доступ к критическому разделу . Поэтому мы также можем реализовать мьютекс, установив количество разрешенных потоков в семафоре равным одному .
Теперь давайте создадим еще одну потокобезопасную версию Генератора последовательностей с использованием Семафора :
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. Использование класса монитора Гуавы
До сих пор мы видели варианты реализации мьютекса с использованием функций, предоставляемых Java.
Однако класс Monitor библиотеки Guava от Google является лучшей альтернативой классу ReentrantLock . Согласно его документации , код, использующий Монитор , более удобочитаем и менее подвержен ошибкам, чем код, использующий ReentrantLock .
Во-первых, мы добавим зависимость Maven для Guava :
com.google.guava guava 28.0-jre
Теперь мы напишем еще один подкласс Генератора последовательностей , используя класс 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 .