Guia para java.util.concurrent.Locks

Guia para java.util.concurrent.Locks

 

1. Visão geral

Simplificando, um bloqueio é um mecanismo de sincronização de thread mais flexível e sofisticado do que o bloco padrãosynchronized.

A interfaceLock existe desde Java 1.5. É definido dentro do pacotejava.util.concurrent.lock e fornece operações abrangentes para bloqueio.

Neste artigo, exploraremos diferentes implementações da interfaceLock e seus aplicativos.

2. Diferenças entre bloqueio e bloco sincronizado

Existem poucas diferenças entre o uso deblock sincronizado e o uso de APIsLock:

  • A synchronized block is fully contained within a method – podemos terLock API'slock() eunlock() operação em métodos separados

  • A synchronized block não suporta a justiça, qualquer thread pode adquirir o bloqueio uma vez liberado, nenhuma preferência pode ser especificada. We can achieve fairness within the Lock APIs by specifying the fairness property. Garante que o maior segmento de espera tenha acesso ao bloqueio

  • Um encadeamento é bloqueado se não conseguir acessar osblock sincronizados. The Lock API provides tryLock() method. The thread acquires lock only if it’s available and not held by any other thread. Isso reduz o tempo de bloqueio do segmento aguardando o bloqueio

  • Um thread que está em estado de “espera” para adquirir o acesso asynchronized block, não pode ser interrompido. The Lock API provides a method lockInterruptibly() which can be used to interrupt the thread when it’s waiting for the lock

3. APILock

Vamos dar uma olhada nos métodos na interfaceLock:

  • *void lock()* – adquire o bloqueio se estiver disponível; se o bloqueio não estiver disponível, um thread será bloqueado até que o bloqueio seja liberado

  • void lockInterruptibly() - é semelhante aolock(),, mas permite que o thread bloqueado seja interrompido e reinicie a execução por meio de umjava.lang.InterruptedException lançado

  • boolean tryLock() - esta é uma versão sem bloqueio do métodolock(); ele tenta obter o bloqueio imediatamente, retorna verdadeiro se o bloqueio for bem-sucedido

  • *boolean tryLock(long timeout, TimeUnit timeUnit)* – é semelhante atryLock(),, exceto que aguarda o tempo limite fornecido antes de desistir de tentar adquirir oLock

  • void unlock() - desbloqueia a instânciaLock

Uma instância bloqueada sempre deve ser desbloqueada para evitar a condição de conflito. Um bloco de código recomendado para usar o bloqueio deve conter um blocotry/catchefinally:

Lock lock = ...;
lock.lock();
try {
    // access to the shared resource
} finally {
    lock.unlock();
}

Além da interfaceLock,, temos uma interfaceReadWriteLock que mantém um par de bloqueios, um para operações somente leitura e outro para a operação de gravação. O bloqueio de leitura pode ser mantido simultaneamente por vários threads, desde que não haja gravação.

ReadWriteLock declara métodos para adquirir bloqueios de leitura ou gravação:

  • *Lock readLock()* – retorna o bloqueio que é usado para leitura

  • Lock writeLock() - retorna o bloqueio que é usado para escrever

4. Implementações de bloqueio

4.1. ReentrantLock

A classeReentrantLock implementa a interfaceLock. Ele oferece a mesma simultaneidade e semântica de memória, como o bloqueio de monitor implícito acessado usando métodos e instruçõessynchronized, com recursos estendidos.

Vamos ver como podemos usar a sincronizaçãoReenrtantLock for:

public class SharedObject {
    //...
    ReentrantLock lock = new ReentrantLock();
    int counter = 0;

    public void perform() {
        lock.lock();
        try {
            // Critical section here
            count++;
        } finally {
            lock.unlock();
        }
    }
    //...
}

Precisamos ter certeza de que estamos envolvendo as chamadaslock () eunlock() no blocotry-finally para evitar situações de deadlock.

Vamos ver como funciona otryLock():

public void performTryLock(){
    //...
    boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);

    if(isLockAcquired) {
        try {
            //Critical section here
        } finally {
            lock.unlock();
        }
    }
    //...
}

Nesse caso, o thread que chamatryLock(), aguardará um segundo e desistirá de esperar se o bloqueio não estiver disponível.

4.2. ReentrantReadWriteLock

A classeReentrantReadWriteLock implementa a interfaceReadWriteLock.

Vejamos as regras para adquirirReadLock ouWriteLock por um segmento:

  • Read Lock - se nenhuma thread adquiriu o bloqueio de gravação ou solicitou por isso, então, várias threads podem adquirir o bloqueio de leitura

  • Write Lock - se nenhum segmento estiver lendo ou gravando, então apenas um segmento pode adquirir o bloqueio de gravação

Vamos ver como usarReadWriteLock:

public class SynchronizedHashMapWithReadWriteLock {

    Map syncHashMap = new HashMap<>();
    ReadWriteLock lock = new ReentrantReadWriteLock();
    // ...
    Lock writeLock = lock.writeLock();

    public void put(String key, String value) {
        try {
            writeLock.lock();
            syncHashMap.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    ...
    public String remove(String key){
        try {
            writeLock.lock();
            return syncHashMap.remove(key);
        } finally {
            writeLock.unlock();
        }
    }
    //...
}

Para ambos os métodos de gravação, precisamos cercar a seção crítica com o bloqueio de gravação, apenas um segmento pode ter acesso a ele:

Lock readLock = lock.readLock();
//...
public String get(String key){
    try {
        readLock.lock();
        return syncHashMap.get(key);
    } finally {
        readLock.unlock();
    }
}

public boolean containsKey(String key) {
    try {
        readLock.lock();
        return syncHashMap.containsKey(key);
    } finally {
        readLock.unlock();
    }
}

Para ambos os métodos de leitura, precisamos cercar a seção crítica com o bloqueio de leitura. Vários segmentos podem obter acesso a esta seção se nenhuma operação de gravação estiver em andamento.

4.3. StampedLock

StampedLock é introduzido no Java 8. Ele também suporta bloqueios de leitura e gravação. No entanto, os métodos de aquisição de bloqueio retornam um carimbo usado para liberar um bloqueio ou para verificar se o bloqueio ainda é válido:

public class StampedLockDemo {
    Map map = new HashMap<>();
    private StampedLock lock = new StampedLock();

    public void put(String key, String value){
        long stamp = lock.writeLock();
        try {
            map.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }

    public String get(String key) throws InterruptedException {
        long stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }
}

Outro recurso fornecido porStampedLock é o bloqueio otimista. Na maioria das vezes, as operações de leitura não precisam esperar pela conclusão da operação de gravação e, como resultado disso, o bloqueio de leitura completo não é necessário.

Em vez disso, podemos atualizar para ler o bloqueio:

public String readWithOptimisticLock(String key) {
    long stamp = lock.tryOptimisticRead();
    String value = map.get(key);

    if(!lock.validate(stamp)) {
        stamp = lock.readLock();
        try {
            return map.get(key);
        } finally {
            lock.unlock(stamp);
        }
    }
    return value;
}

5. Trabalhando comConditions

A classeCondition fornece a capacidade de um encadeamento aguardar a ocorrência de alguma condição enquanto executa a seção crítica.

Isso pode ocorrer quando um thread adquire acesso à seção crítica, mas não tem as condições necessárias para realizar sua operação. Por exemplo, um thread do leitor pode obter acesso ao bloqueio de uma fila compartilhada, que ainda não possui nenhum dado para consumir.

Tradicionalmente, o Java fornece métodoswait(), notify() and notifyAll() para intercomunicação de thread. Conditions têm mecanismos semelhantes, mas, além disso, podemos especificar várias condições:

public class ReentrantLockWithCondition {

    Stack stack = new Stack<>();
    int CAPACITY = 5;

    ReentrantLock lock = new ReentrantLock();
    Condition stackEmptyCondition = lock.newCondition();
    Condition stackFullCondition = lock.newCondition();

    public void pushToStack(String item){
        try {
            lock.lock();
            while(stack.size() == CAPACITY) {
                stackFullCondition.await();
            }
            stack.push(item);
            stackEmptyCondition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String popFromStack() {
        try {
            lock.lock();
            while(stack.size() == 0) {
                stackEmptyCondition.await();
            }
            return stack.pop();
        } finally {
            stackFullCondition.signalAll();
            lock.unlock();
        }
    }
}

6. Conclusão

Neste artigo, vimos diferentes implementações da interfaceLock e da classeStampedLock recém-introduzida. Também exploramos como podemos usar a classeCondition para trabalhar com várias condições.

O código completo para este tutorial está disponívelover on GitHub.