Mutex é uma abreviação de exclusão mútua em inglês (mutual exclusion). Seu propósito é evitar que duas ou mais threads acessem simultaneamente o mesmo recurso compartilhado.
Para exemplificar um problema de falta de Mutex, imagine um sistema central multi thread responsável por apoiar o processamento de pedidos de um portal de compras. Neste cenário, o usuário pode clicar duas vezes e submeter duas vezes a requisição por uma questão de impaciência, ainda a rede de comunicação pode conter falha que redunde a requisição ou a aplicação servidora pode conter algum bug capaz de emitir mais de uma vez a requisição para o sistema de processamento final.
Se você for um programador desatento pode não acreditar na possibilidade de uma requisição redundante ocasionar impacto financeiro ao usuário e até mesmo a instituição responsável pelo portal sem contar o transtorno para administrar o problema.
Para todo efeito, o problema existe e somente um controle transacional delegado a um banco de dados não resolve porque uma nova transação em banco de dados só sabe da existência de uma anterior se esta (anterior) sofrer uma confirmação ou commit. O controle transacional via banco de dados só surtirá efeito se as requisições redundantes fossem em sequencia, mas a realidade de requisições redundantes é em grande parte paralela.
A esta altura a exclusão mútua deve fazer maior sentido caso você não a tenha compreendido.
O Mutex normalmente é aplicado no serviço central (aplicação) para impedir justamente impactos gerados por falhas em algum sistema intermediário (ex.: web-container) ou sistema terminal (ex.: browser ou usuário).
Construíndo uma controladora Mutex
A fim de tornar a Mutex mais eficiente, vamos fazer o uso da coleção com limpeza automática por tempo de espera chamada WTConcurrentLinkedQueue publicada no artigo Java - Limpeza automática de coleções. Essa classe possui dependência da classe WTTimeObject também publicada no mesmo artigo.
Agora vamos ao exemplo de uma classe para controle Mutex.
import java.io.IOException;
public class MutexControl {
private static Integer LOCKTIMEOUT_SECS = 120;
private static String CARDPREFIX = "_CARD";
private static String TRANSACTIONPREFIX = "_TRANSACTION";
private static MutexControl mutexControl;
static {
mutexControl = null;
}
private static synchronized MutexControl getInstance() {
if (mutexControl == null) {
mutexControl = new MutexControl();
}
return mutexControl;
}
private WTConcurrentHashMap<String, Long> list;
private MutexControl() {
list = new WTConcurrentHashMap<String, Long>("MutexControl");
}
public WTConcurrentHashMap<String, Long> getList() {
return list;
}
private static synchronized void lock(String id, Integer expirationSecs) throws IOException {
Long obj = getInstance().getList().get(id);
if (obj != null) {
if (obj < System.currentTimeMillis()) {
getInstance().getList().remove(id);
} else {
throw new IOException(String.format("A chave de operação (%s) simultânea está em uso.", id));
}
}
Long timeout = System.currentTimeMillis() + (expirationSecs * 1000L);
getInstance().getList().put(id, timeout);
}
private static synchronized void unlock(String id) {
getInstance().getList().remove(id);
}
public static void cardLock(Long cardLogicalNumber) throws IOException {
String key = String.format("%s%d", CARDPREFIX, cardLogicalNumber);
lock(key, LOCKTIMEOUT_SECS);
}
public static void cardUnlock(Long cardLogicalNumber) throws IOException {
String key = String.format("%s%d", CARDPREFIX, cardLogicalNumber);
unlock(key);
}
public static void transactionLock(Long transactionId) throws IOException {
String key = String.format("%s%d", TRANSACTIONPREFIX, transactionId);
lock(key, LOCKTIMEOUT_SECS);
}
public static void transactionUnlock(Long transactionId) throws IOException {
String key = String.format("%s%d", TRANSACTIONPREFIX, transactionId);
unlock(key);
}
public static void main(String[] args) {
/****************************************************
* Testar bloqueio/desbloqueio de um fictício cartão por seu id
****************************************************/
Long cardId = 123L;
// Teste 1: Bloqueia cartão - Resultado esperado: Deve pemitir
try {
cardLock(cardId);
} catch (IOException e) {
System.err.println("Teste 1: " + e.getMessage());
}
// Teste 2: Bloqueia cartão - Resultado esperado: Não deve pemitir
try {
cardLock(cardId);
} catch (IOException e) {
System.err.println("Teste 2: " + e.getMessage());
}
// Teste 3: Desbloqueia cartão - Resultado esperado: Deve pemitir
// Se não desbloquear até 120 segundos, desbloqueia automaticamente
// Leia a constante LOCKTIMEOUT_SECS
try {
cardUnlock(cardId);
} catch (IOException e) {
System.err.println("Teste 3: " + e.getMessage());
}
// Teste 4: Bloqueia cartão - Resultado esperado: Deve pemitir
try {
cardLock(cardId);
} catch (IOException e) {
System.err.println("Teste 4: " + e.getMessage());
}
/****************************************************
* Testar bloqueio/desbloqueio de uma fictícia transação por seu id
****************************************************/
Long transactionId = 1002L;
// Teste 5: Bloqueia transação - Resultado esperado: Deve pemitir
try {
transactionLock(transactionId);
} catch (IOException e) {
System.err.println("Teste 5: " + e.getMessage());
}
// Teste 6: Bloqueia transação - Resultado esperado: Não deve pemitir
try {
transactionLock(transactionId);
} catch (IOException e) {
System.err.println("Teste 6: " + e.getMessage());
}
// Teste 7: Desbloqueia transação - Resultado esperado: Deve pemitir
// Se não desbloquear até 120 segundos, desbloqueia automaticamente
// Leia a constante LOCKTIMEOUT_SECS
try {
transactionUnlock(transactionId);
} catch (IOException e) {
System.err.println("Teste 7: " + e.getMessage());
}
// Teste 8: Bloqueia transação - Resultado esperado: Deve pemitir
try {
transactionLock(transactionId);
} catch (IOException e) {
System.err.println("Teste 8: " + e.getMessage());
}
}
}
A classe MutexControl contém como principais funções lock e unlock para bloquear um recurso pelo seu identificador. Para facilitar o entendimento, na classe ainda foram adicionadas funções de bloqueio e desbloqueio mais concretas, são os casos das funções cardLock, cardUnlock, transactionLock e transactionUnlock. Basicamente você pode criar funções de bloqueio e desbloqueio de outros recursos diretos ao utilizar a mesma técnica de criar as funções baseadas num prefixo para o identificador para não conflitar com controles de outros recursos.
Para evitar incidentes sérios caso você se esqueça de desbloquear um recurso após a operação ou mesmo que seu controle de exceção deixe o recurso bloqueado por erro, foi determinado um limite de tempo de bloqueio em 120 segundos na constante LOCKTIMEOUT_SECS da classe MutexControl. O parâmetro pode ser alterado a seu critério de tolerância para desbloquear um recurso esquecido no Mutex.
Teste da controladora Mutex
O teste está na mesma classe controladora no método main. Execute a classe em modo de debug preferencialmente e acompanhe os exemplos de controle de operação de um cartão de id 123 e em seguida, de uma transação de id 1002.
Resultado
Na tentativas de bloqueios redundantes, devem ocorrer as seguintes exceções:
Teste 2: A chave de operação (_CARD123) simultânea está em uso.
Teste 6: A chave de operação (_TRANSACTION1002) simultânea está em uso.
Conclusão
Análogo a um controle de semáforo onde se passa apenas um carro por vez numa via, o Mutex é uma técnica simples e segura para a exclusão mútua de processamento redundante provocado por falhas de requisições redundantes;
Até o próximo artigo.