Рубрики
Без рубрики

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

Исследуйте концепцию объекта мьютекса в Java.

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