Usando um objeto Mutex em Java

Usando um objeto Mutex em Java

1. Visão geral

Neste tutorial, veremosdifferent ways to implement a mutex in Java.

2. Mutex

Em um aplicativo multithread, dois ou mais threads podem precisar acessar um recurso compartilhado ao mesmo tempo, resultando em comportamento inesperado. Exemplos desses recursos compartilhados são estruturas de dados, dispositivos de entrada e saída, arquivos e conexões de rede.

Chamamos esse cenário derace condition. E a parte do programa que acessa o recurso compartilhado é conhecida comocritical section. So, to avoid a race condition, we need to synchronize access to the critical section.

Um mutex (ou exclusão mútua) é o tipo mais simples desynchronizer – itensures that only one thread can execute the critical section of a computer program at a time.

Para acessar uma seção crítica, um segmento adquire o mutex, acessa a seção crítica e finalmente libera o mutex. Enquanto isso,all other threads block till the mutex releases. Assim que um encadeamento sai da seção crítica, outro encadeamento pode entrar na seção crítica.

3. Porquê Mutex?

Primeiro, vamos dar um exemplo de uma classeSequenceGeneraror, que gera a próxima sequência incrementandocurrentValue em um a cada vez:

public class SequenceGenerator {

    private int currentValue = 0;

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

}

Agora, vamos criar um caso de teste para ver como esse método se comporta quando vários threads tentam acessá-lo simultaneamente:

@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;
}

Depois de executar este caso de teste, podemos ver que ele falha na maioria das vezes com o motivo semelhante a:

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)

OuniqueSequences deve ter o tamanho igual ao número de vezes que executamos o métodogetNextSequence em nosso caso de teste. No entanto, este não é o caso devido às condições da corrida. Obviamente, não queremos esse comportamento.

Portanto, para evitar tais condições de corrida, precisamosmake sure that only one thread can execute the getNextSequence method at a time. Em tais cenários, podemos usar um mutex para sincronizar os threads.

Existem várias maneiras, podemos implementar um mutex em Java. Então, a seguir, veremos as diferentes maneiras de implementar um mutex para nossa classeSequenceGenerator.

4. Usando a palavra-chavesynchronized

Primeiro, discutiremos osynchronized keyword, que é a maneira mais simples de implementar um mutex em Java.

Todo objeto em Java tem um bloqueio intrínseco associado a ele. Thesynchronized method andthe synchronized block use this intrinsic lock para restringir o acesso da seção crítica a apenas um encadeamento por vez.

Portanto, quando uma thread invoca um métodosynchronized ou entra em um blocosynchronized, ela adquire automaticamente o bloqueio. O bloqueio é liberado quando o método ou bloco é concluído ou uma exceção é lançada a partir deles.

Vamos mudargetNextSequence para ter um mutex, simplesmente adicionando a palavra-chavesynchronized:

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {

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

}

O blocosynchronized é semelhante ao métodosynchronized, com mais controle sobre a seção crítica e o objeto que podemos usar para o bloqueio.

Então, vamos ver como podemosuse the synchronized block to synchronize on a custom mutex object:

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {

    private Object mutex = new Object();

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

}

5. UsandoReentrantLock

A classeReentrantLock foi introduzida no Java 1.5. Ele fornece mais flexibilidade e controle do que a abordagem de palavra-chavesynchronized.

Vamos ver como podemos usarReentrantLock para alcançar a exclusão mútua:

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {

    private ReentrantLock mutex = new ReentrantLock();

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

6. UsandoSemaphore

ComoReentrantLock, a classeSemaphore também foi introduzida no Java 1.5.

Enquanto no caso de um mutex apenas uma thread pode acessar uma seção crítica,Semaphore permitea fixed number of threads to access a critical section. Portanto,we can also implement a mutex by setting the number of allowed threads in a Semaphore to one.

Vamos agora criar outra versão thread-safe deSequenceGenerator usandoSemaphore:

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. Usando a classeMonitor da Guava

Até agora, vimos as opções para implementar mutex usando recursos fornecidos pelo Java.

No entanto, a classeMonitor da biblioteca Guava do Google é uma alternativa melhor para a classeReentrantLock. De acordo com seudocumentation, o código que usaMonitor é mais legível e menos sujeito a erros do que o código que usaReentrantLock.

Primeiro, vamos adicionar a dependência Maven paraGuava:


    com.google.guava
    guava
    28.0-jre

Agora, vamos escrever outra subclasse deSequenceGenerator usando a classeMonitor:

public class SequenceGeneratorUsingMonitor extends SequenceGenerator {

    private Monitor mutex = new Monitor();

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

}

8. Conclusão

Neste tutorial, examinamos o conceito de mutex. Além disso, vimos as diferentes maneiras de implementá-lo em Java.

Como sempre, o código-fonte completo dos exemplos de código usados ​​neste tutorial está disponívelover on GitHub.