Автор оригинала: 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; SetuniqueSequences = 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 .