No meu último artigo sobre investigação de deadlocks, falei sobre consertar meu código de exemplo de transferência e balanceamento de deadlocks utilizando tanto a tradicional palavra-chave synchronized do Java quanto ordenamento de lock (lock ordering). Há, no entanto, um método alternativo conhecido como “locking explícito”.
A ideia de chamar um mecanismo explícito em vez de um implícito significa que isso não faz parte da linguagem Java e que classes foram escritas para preencher as funcionalidades de locking. Locking implícito, por outro lado, pode ser definido como locking que é parte da linguagem e é implementado no background utilizando a palavra-chave synchronized.
É possível argumentar se locking explícito é uma boa ideia ou não. Deveria o Java ser melhorado para incluir recursos de locking explícito em vez de acrescentar um novo conjunto de classes à já enorme API? Por exemplo: trysynchronized().
Locking explícito é baseado na interface Lock e na implementação ReentrantLock. Lock contém um monte de métodos que te dão vários controles de locking a mais do que o tradicional synchronized. Ele possui os métodos que você esperaria que ele tivesse, como lock(), que irá criar um ponto de entrada em uma seção reservada do código e unlock(), que cria o ponto de saída. Ele também tem tryLock(), que irá apenas adquirir uma trava se este estiver disponível e não adquirido por outra thread, e tryLock(long time,TimeUnit unit), que irá tentar adquirir uma trava e, se indisponível, esperar por um tempo especificado para expirar antes de desistir.
Para implementar locking explicito, eu apenas adicionei a interface Lock à classe Account utilizada nos outros artigos desta série.
public class Account implements Lock { private final int number; private int balance; private final ReentrantLock lock; public Account(int number, int openingBalance) { this.number = number; this.balance = openingBalance; this.lock = new ReentrantLock(); } public void withDrawAmount(int amount) throws OverdrawnException { if (amount > balance) { throw new OverdrawnException(); } balance -= amount; } public void deposit(int amount) { balance += amount; } public int getNumber() { return number; } public int getBalance() { return balance; } // ------- Lock interface implementation @Override public void lock() { lock.lock(); } @Override public void lockInterruptibly() throws InterruptedException { lock.lockInterruptibly(); } @Override public Condition newCondition() { return lock.newCondition(); } @Override public boolean tryLock() { return lock.tryLock(); } @Override public boolean tryLock(long arg0, TimeUnit arg1) throws InterruptedException { return lock.tryLock(arg0, arg1); } @Override public void unlock() { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
No código acima, você pode ver que eu estou privilegiando a agregação ao encapsular o objeto ReentrantLock, o qual a classe Account delega a funcionalidade locking. A única pequena pegadinha é que você precisa estar atento a isso na implementação do unlock():
@Override public void unlock() { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }
Isso tem um argumento if() adicional que checa se a thread de chamada é a que atualmente segura o lock. Se essa linha de código for omitida, então você terá o seguinte IllegalMonitorStateException.
Exception in thread "Thread-7" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460) at threads.lock.Account.unlock(Account.java:76) at threads.lock.TrylockDemo$BadTransferOperation.transfer(TrylockDemo.java:98) at threads.lock.TrylockDemo$BadTransferOperation.run(TrylockDemo.java:67)
Então, como isso é implementado? Abaixo encontra-se uma listagem do meu TryLockDemo baseado no meu programa DeadLockDemo.
public class TrylockDemo { private static final int NUM_ACCOUNTS = 10; private static final int NUM_THREADS = 20; private static final int NUM_ITERATIONS = 100000; private static final int LOCK_ATTEMPTS = 10000; static final Random rnd = new Random(); List<Account> accounts = new ArrayList<Account>(); public static void main(String args[]) { TrylockDemo demo = new TrylockDemo(); demo.setUp(); demo.run(); } void setUp() { for (int i = 0; i < NUM_ACCOUNTS; i++) { Account account = new Account(i, 1000); accounts.add(account); } } void run() { for (int i = 0; i < NUM_THREADS; i++) { new BadTransferOperation(i).start(); } } class BadTransferOperation extends Thread { int threadNum; BadTransferOperation(int threadNum) { this.threadNum = threadNum; } @Override public void run() { int transactionCount = 0; for (int i = 0; i < NUM_ITERATIONS; i++) { Account toAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS)); Account fromAccount = accounts.get(rnd.nextInt(NUM_ACCOUNTS)); int amount = rnd.nextInt(1000); if (!toAccount.equals(fromAccount)) { boolean successfulTransfer = false; try { successfulTransfer = transfer(fromAccount, toAccount, amount); } catch (OverdrawnException e) { successfulTransfer = true; } if (successfulTransfer) { transactionCount++; } } } System.out.println("Thread Complete: " + threadNum + " Successfully made " + transactionCount + " out of " + NUM_ITERATIONS); } private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException { boolean success = false; for (int i = 0; i < LOCK_ATTEMPTS; i++) { try { if (fromAccount.tryLock()) { try { if (toAccount.tryLock()) { success = true; fromAccount.withDrawAmount(transferAmount); toAccount.deposit(transferAmount); break; } } finally { toAccount.unlock(); } } } finally { fromAccount.unlock(); } } return success; } } }
A ideia é a mesma: eu tenho uma lista de contas de banco e vou escolher aleatoriamente duas contas e transferir um montante de uma para outra. O coração de tudo será o meu método transfer(…) atualizado conforme exibido abaixo.
private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException { boolean success = false; for (int i = 0; i < LOCK_ATTEMPTS; i++) { try { if (fromAccount.tryLock()) { try { if (toAccount.tryLock()) { success = true; fromAccount.withDrawAmount(transferAmount); toAccount.deposit(transferAmount); break; } } finally { toAccount.unlock(); } } } finally { fromAccount.unlock(); } } return success; }
A ideia aqui é que eu tento bloquear o fromAccount e depois o toAccount. Se isso funcionar, então eu faço a transferência antes de me lembrar de desbloquear ambas as contas. Se as contas já estiverem bloqueadas, então meu método tryLock() irá falhar e e todo o processo entra em loop e reinicia novamente. Depois de 10 mil tentativas de bloqueamento, a thread desiste e ignora a transferência. Eu imagino que em um aplicativo do mundo real você irá querer que essa falha entre em algum tipo de fila de forma que seja investigada mais tarde.
Ao utilizar locking explícito, é preciso considerar se ele funciona bem, portanto dê uma olhada nos resultados abaixo…
Thread Complete: 17 Successfully made 58142 out of 100000 Thread Complete: 12 Successfully made 57627 out of 100000 Thread Complete: 9 Successfully made 57901 out of 100000 Thread Complete: 16 Successfully made 56754 out of 100000 Thread Complete: 3 Successfully made 56914 out of 100000 Thread Complete: 14 Successfully made 57048 out of 100000 Thread Complete: 8 Successfully made 56817 out of 100000 Thread Complete: 4 Successfully made 57134 out of 100000 Thread Complete: 15 Successfully made 56636 out of 100000 Thread Complete: 19 Successfully made 56399 out of 100000 Thread Complete: 2 Successfully made 56603 out of 100000 Thread Complete: 13 Successfully made 56889 out of 100000 Thread Complete: 0 Successfully made 56904 out of 100000 Thread Complete: 5 Successfully made 57119 out of 100000 Thread Complete: 7 Successfully made 56776 out of 100000 Thread Complete: 6 Successfully made 57076 out of 100000 Thread Complete: 10 Successfully made 56871 out of 100000 Thread Complete: 11 Successfully made 56863 out of 100000 Thread Complete: 18 Successfully made 56916 out of 100000 Thread Complete: 1 Successfully made 57304 out of 100000
Isso mostra que, apesar de o programa não ter travado (deadlock) indefinidamente, ele apenas conseguiu fazer a transferência em pouco mais da metade dos pedidos. Isso significa desperdiçar muito poder de processamento em loops, loops e mais loops – o que não é muito eficiente como um todo. Além disso, eu disse segundos atrás que o programa não ficou esperando indefinidamente, o que não é exatamente verdade. Se você pensar no que está acontecendo, então irá perceber que o programa está de fato entrando em deadlock, o que confirma essa condição.
A segunda versão do meu código de demonstração de locking explícito utiliza o tryLock(long time,TimeUnit unit) mencionado acima.
private boolean transfer(Account fromAccount, Account toAccount, int transferAmount) throws OverdrawnException { boolean success = false; try { if (fromAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) { try { if (toAccount.tryLock(LOCK_TIMEOUT, TimeUnit.MILLISECONDS)) { success = true; fromAccount.withDrawAmount(transferAmount); toAccount.deposit(transferAmount); } } finally { toAccount.unlock(); } } } catch (InterruptedException e) { e.printStackTrace(); } finally { fromAccount.unlock(); } return success; }
Nesse código, eu substituí o loop for por um tryLock(…) tempo limite de 1 milissegundo. Isso significa que quando o tryLock(…) é chamado e não puder obter o bloqueamento, ele irá esperar 1 ms antes de tentar novamente e desistir.
Thread Complete: 0 Successfully made 26637 out of 100000 Thread Complete: 14 Successfully made 26516 out of 100000 Thread Complete: 3 Successfully made 26552 out of 100000 Thread Complete: 11 Successfully made 26653 out of 100000 Thread Complete: 7 Successfully made 26399 out of 100000 Thread Complete: 1 Successfully made 26602 out of 100000 Thread Complete: 18 Successfully made 26606 out of 100000 Thread Complete: 17 Successfully made 26358 out of 100000 Thread Complete: 19 Successfully made 26407 out of 100000 Thread Complete: 16 Successfully made 26312 out of 100000 Thread Complete: 15 Successfully made 26449 out of 100000 Thread Complete: 5 Successfully made 26388 out of 100000 Thread Complete: 8 Successfully made 26613 out of 100000 Thread Complete: 2 Successfully made 26504 out of 100000 Thread Complete: 6 Successfully made 26420 out of 100000 Thread Complete: 4 Successfully made 26452 out of 100000 Thread Complete: 9 Successfully made 26287 out of 100000 Thread Complete: 12 Successfully made 26507 out of 100000 Thread Complete: 10 Successfully made 26660 out of 100000 Thread Complete: 13 Successfully made 26523 out of 100000
O resultado acima demonstra que, ao utilizar o timer, a taxa de sucesso na transferência cai ainda mais, para pouco mais de 25%. Apesar de que agora ele não está desperdiçando tanto tempo do processador, continua ainda altamente ineficiente.
Eu poderia brincar mais com ambos os exemplos de código e escolher variáveis que ajustam o programa e melhoram o desempenho, mas, no final do dia, não há um substituto real para conseguir uma forma de bloqueio correta. Pessoalmente, eu prefiro utilizar a velha palavra-chave synchronized e bloquear implicitamente onde for possível e reservar o locking explícito para as poucas situações em que o código de deadlocking é antigo, confuso, indecifrável; eu já tentei todo o resto para fazer funcionar e já é tarde e é hora de ir para casa…
***
Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.captaindebug.com/2012/11/investigating-deadlocks-part-5-using.html#.UUtUG3E-uoO