Exemplo de Bloqueio em Java – ReentrantLock

Bem-vindo ao tutorial de exemplo de Java Lock. Normalmente, ao trabalhar em um ambiente de múltiplas threads, usamos synchronized para segurança de thread.

Java Lock

Na maioria das vezes, a palavra-chave synchronized é a escolha ideal, mas possui algumas limitações que levaram à inclusão da API Lock no pacote de Concorrência do Java. A API de Concorrência do Java 1.5 apresentou o pacote java.util.concurrent.locks com a interface Lock e algumas classes de implementação para aprimorar o mecanismo de bloqueio de objetos. Algumas interfaces e classes importantes na API de Lock do Java são:

  1. Lock: Esta é a interface base para a API Lock. Ela fornece todas as funcionalidades da palavra-chave synchronized, com maneiras adicionais de criar diferentes Condições para bloqueio, fornecendo tempo limite para a thread esperar pelo bloqueio. Alguns dos métodos importantes são lock() para adquirir o bloqueio, unlock() para liberar o bloqueio, tryLock() para esperar pelo bloqueio por um determinado período de tempo, newCondition() para criar a Condição etc.

  2. Condição: Objetos de Condição são similares ao modelo de Espera-Notificação de Objeto com recurso adicional de criar diferentes conjuntos de espera. Um objeto de Condição é sempre criado por um objeto de Bloqueio. Alguns dos métodos importantes são await() que é similar ao wait() e signal(), signalAll() que é similar aos métodos notify() e notifyAll().

  3. Bloqueio de Leitura-Escrita: Contém um par de bloqueios associados, um para operações somente leitura e outro para escrita. O bloqueio de leitura pode ser mantido simultaneamente por múltiplas threads leitoras desde que não haja threads escritoras. O bloqueio de escrita é exclusivo.

  4. ReentrantLock: Esta é a classe de implementação mais amplamente utilizada da interface Lock. Esta classe implementa a interface Lock de forma semelhante à palavra-chave synchronized. Além da implementação da interface Lock, ReentrantLock contém alguns métodos utilitários para obter a thread que possui a trava, threads esperando para adquirir a trava etc. Os blocos synchronized são reentrantes por natureza, ou seja, se uma thread possui a trava no objeto monitor e se outro bloco synchronized requer a trava no mesmo objeto monitor, então a thread pode entrar nesse bloco de código. Acredito que este seja o motivo pelo qual o nome da classe é ReentrantLock. Vamos entender esse recurso com um exemplo simples.

    public class Test{
    
    public synchronized foo(){
        //faça algo
        bar();
      }
    
      public synchronized bar(){
        //faça algo mais
      }
    }
    

    Se uma thread entra em foo(), ela possui a trava no objeto Test, então quando tenta executar o método bar(), a thread é permitida a executar o método bar(), pois já possui a trava no objeto Test, ou seja, o mesmo que synchronized(this).

Exemplo de Lock em Java – ReentrantLock em Java

Agora vamos ver um exemplo simples onde substituiremos a palavra-chave synchronized pela API de Lock do Java. Digamos que temos uma classe Resource com algumas operações onde queremos que seja thread-safe e alguns métodos onde a segurança da thread não é necessária.

package com.journaldev.threads.lock;

public class Resource {

	public void doSomething(){
		//faça alguma operação, leitura de BD, escrita etc
	}
	
	public void doLogging(){
		//registro, não precisa de segurança de thread
	}
}

Agora, vamos supor que temos uma classe Runnable onde usaremos os métodos de Recurso.

package com.journaldev.threads.lock;

public class SynchronizedLockExample implements Runnable{

	private Resource resource;
	
	public SynchronizedLockExample(Resource r){
		this.resource = r;
	}
	
	@Override
	public void run() {
		synchronized (resource) {
			resource.doSomething();
		}
		resource.doLogging();
	}
}

Observe que estou usando um bloco sincronizado para adquirir o bloqueio no objeto de Recurso. Poderíamos ter criado um objeto fictício na classe e o usado para fins de bloqueio. Agora, vejamos como podemos usar a API de Lock do Java e reescrever o programa acima sem usar a palavra-chave synchronized. Usaremos ReentrantLock no Java.

package com.journaldev.threads.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConcurrencyLockExample implements Runnable{

	private Resource resource;
	private Lock lock;
	
	public ConcurrencyLockExample(Resource r){
		this.resource = r;
		this.lock = new ReentrantLock();
	}
	
	@Override
	public void run() {
		try {
			if(lock.tryLock(10, TimeUnit.SECONDS)){
			resource.doSomething();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}finally{
			//liberar o bloqueio
			lock.unlock();
		}
		resource.doLogging();
	}

}

Como você pode ver, estou usando o método tryLock() para garantir que minha thread espere apenas por um tempo definido e, se não conseguir o bloqueio no objeto, está apenas registrando e saindo. Outro ponto importante a ser observado é o uso do bloco try-finally para garantir que o bloqueio seja liberado mesmo se a chamada do método doSomething() lançar alguma exceção.

Java Lock vs synchronized

Com base nos detalhes e no programa acima, podemos facilmente concluir as seguintes diferenças entre o Lock do Java e a sincronização.

  1. A API de Lock do Java fornece mais visibilidade e opções para bloqueio, ao contrário da sincronização, onde uma thread pode acabar esperando indefinidamente pelo bloqueio, podemos usar tryLock() para garantir que a thread espere apenas por um tempo específico.
  2. O código de sincronização é muito mais limpo e fácil de manter, enquanto com Lock somos obrigados a ter um bloco try-finally para garantir que o Lock seja liberado mesmo se alguma exceção for lançada entre as chamadas dos métodos lock() e unlock().
  3. bloqueios ou métodos de sincronização podem abranger apenas um método, enquanto podemos adquirir o bloqueio em um método e liberá-lo em outro método com a API de Lock.
  4. A palavra-chave synchronized não fornece equidade, enquanto podemos definir a equidade como verdadeira ao criar um objeto ReentrantLock, para que a thread que espera mais tempo obtenha o bloqueio primeiro.
  5. Podemos criar diferentes condições para o Lock e threads diferentes podem aguardar() por diferentes condições.

Isso é tudo para o exemplo de Lock em Java, ReentrantLock em Java e uma análise comparativa com a palavra-chave synchronized.

Source:
https://www.digitalocean.com/community/tutorials/java-lock-example-reentrantlock