Conceitos Básicos de Java - Locks em Java

  1. Conceitos Fundamentais de Lock em Java

1.1 Spin Lock (Lock por Espera Ocupada)

Quando uma thread tenta adquirir um lock já ocupado por outra, ela fica em um loop contínuo verificando repetidamente se o lock foi liberado, até conseguir obtê-lo.

1.2 Otimistic Lock

Assume que não haverá conflito. Ao modificar dados, verifica se o valor lido inicialmente mudou; se mudou, lê o valor mais recente e tenta novamente a modificação.

1.3 Pessimistic Lock

Assume que conflitos ocorrerão, sincronizando todas as operações sobre os dados, bloqueando desde a leitura.

1.4 Exclusive Lock (Write Lock)

Apenas uma thread pode modificar o recurso (protegido por um lock de escrita). Outras threads não podem adquirir nenhum lock (nem leitura nem escrita).

1.5 Shared Lock (Read Lock)

Várias threads podem ler simultaneamente (lock de leitura compartilhado), mas nenhuma pode escrever enquanto houver locks de leitura ativos.

1.6 Reentrante vs Não Reentrante

Um lock reentrante permite que a mesma thread adquira o lock novamente enquanto já o possui, sem causar deadlock. Locks não reentrantes bloqueiam a própria thread se ela tentar readquirir o lock.

  1. Mecanismos Comuns de Lock em Java

Esta seção detalha as palavras-chave, interfaces e classes mais usadas: synchronized, Lock (interface) e ReentrantLock.

2.1 Detalhamento do synchronized

Para entender o synchronized, é preciso conhecer a estrutura de um objeto na heap:

  • Instance data: valores dos campos do objeto.
  • Object header: contém o Mark Word (que armazena o estado do lock), o ponteiro para a classe (Class Metadata Address) e, se o objeto for um array, o comprimento do array.
  • Padding: preenchimento para alinhamento a múltiplos de 8 bytes.

O processo de lock do synchronized concentra-se no Mark Word:

  1. A thread cria um Lock Record em sua stack (frame da JVM).
  2. Copia o valor atual do Mark Word (código hash + idade GC, no estado unlocked) para o Lock Record.
  3. Várias threads tentam realiazr uma operação CAS (Compare-and-Swap) no Mark Word: valor antigo = hash+age, novo valor = endereço do Lock Record. A thread que conseguir substituir o valor ganha o lock. As que falham entram em spin lock.
  4. Se o spin atingir um limite (ou se outra thread falhar no CAS), o lock é inflado para um monitor (weighted lock). O Mark Word passa a conter o endereço do monitor.

O monitor (ObjectMonitor) mantém:

  • owner: referência à thread que detém o lock.
  • EntryList (conjunto de espera): threads bloqueadas aguardando o lock (estado BLOCKED).
  • WaitSet: threads que chamaram wait() (estado WAITING).

Quando o owner libera o lock (final do bloco synchronized ou chamada de wait()), threads no EntryList podem competir novamente. notify() move uma thread do WaitSet para o EntryList.

2.2 Detalhamento da Interface Lock

A interface java.util.concurrent.locks.Lock define os métodos:

  • void lock() – Adquire o lock; bloqueia até conseguir.
  • boolean tryLock() – Tenta adquirir; retorna imediatamente.
  • boolean tryLock(long time, TimeUnit unit) – Tenta com tempo limite.
  • void lockInterruptibly() – Adquire o lock, mas pode ser interrompido.
  • void unlock() – Libera o lock.
  • Condition newCondition() – Cria uma condição associada a este lock.

As Condition são análogas a wait()/notify(), mas requerem Lock. Exemplo de uso correto:

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

public class ConditionDemo {
    private static Lock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            lock.lock();
            try {
                System.out.println("Thread suspensa...");
                condition.await();
                System.out.println("Thread liberada...");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
            }
        });
        t.start();

        Thread.sleep(5000);
        lock.lock();
        condition.signal();
        lock.unlock();
    }
}

Deadlock pode ocorrer se signal() for chamado antes de await(). Exemplo problemático:

// Thread A: espera 5s, depois lock() e await()
// Thread main: espera 2s, depois lock() e signal()
// Resultado: signal ocorre antes de await -> a thread A nunca acorda.

Implementação de uma fila bloqueante com Condition:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BlockingQueueCustom {
    private final List<Object> queue = new ArrayList<>();
    private final int capacity;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public BlockingQueueCustom(int capacity) {
        this.capacity = capacity;
    }

    public void put(Object item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await();
            }
            queue.add(item);
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();
            }
            Object item = queue.remove(0);
            notFull.signal();
            return item;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BlockingQueueCustom bq = new BlockingQueueCustom(4);
        new Thread(() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    bq.put("Item " + i);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }).start();

        Thread.sleep(3000);
        for (int i = 0; i < 10; i++) {
            System.out.println("Consumido: " + bq.take());
            Thread.sleep(2000);
        }
    }
}

2.3 Princípios do ReentrantLock

Internamente, ReentrantLock mantém:

  • owner: referência à thread que detém o lock.
  • count: número de aquisições reentrantes.
  • waiters: fila de threads esperando (usando LockSupport.park()).

O lock é adquirido via CAS no count. Se a thread já é dona, o count é incrementado. Se não, tenta CAS; se falhar, entra na fila de espera.

Implementação simplificada de um lock reentrante:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;

public class SimpleReentrantLock implements Lock {
    private AtomicReference<Thread> owner = new AtomicReference<>();
    private AtomicInteger count = new AtomicInteger(0);
    private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();

    @Override
    public boolean tryLock() {
        int ct = count.get();
        if (ct != 0) {
            if (Thread.currentThread() == owner.get()) {
                count.set(ct + 1);
                return true;
            }
        } else {
            if (count.compareAndSet(0, 1)) {
                owner.set(Thread.currentThread());
                return true;
            }
        }
        return false;
    }

    @Override
    public void lock() {
        if (!tryLock()) {
            waiters.offer(Thread.currentThread());
            while (true) {
                Thread head = waiters.peek();
                if (head == Thread.currentThread()) {
                    if (tryLock()) {
                        waiters.poll();
                        return;
                    }
                }
                LockSupport.park();
            }
        }
    }

    private boolean tryUnlock() {
        if (Thread.currentThread() != owner.get()) {
            throw new IllegalMonitorStateException();
        }
        int newCount = count.get() - 1;
        count.set(newCount);
        if (newCount == 0) {
            owner.compareAndSet(Thread.currentThread(), null);
            return true;
        }
        return false;
    }

    @Override
    public void unlock() {
        if (!tryUnlock()) {
            Thread head = waiters.peek();
            if (head != null) {
                LockSupport.unpark(head);
            }
        }
    }

    @Override public void lockInterruptibly() throws InterruptedException { }
    @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; }
    @Override public Condition newCondition() { return null; }
}

2.4 ReadWriteLock (ReentrantReadWriteLock)

O ReentrantReadWriteLock mantém um lock de leitura (compartilhado) e um lock de escrita (exclusivo). É útil quando as operações de leitura são muito mais frequentes que as de escrita.

Exemplo de uso para tornar um HashMap thread‑safe:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ThreadSafeHashMap {
    private final Map<String, Object> map = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    public Object put(String key, Object value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    public Object get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public void clear() {
        writeLock.lock();
        try {
            map.clear();
        } finally {
            writeLock.unlock();
        }
    }
}

Princípio de funcionamento: O lock de leitura só pode ser adquirido se o writeCount for zero. Se houver uma thread com lock de escrita, outras threads de leitura/bloqueio entram na fila. O lock de escrita só é concedido se tanto readCount quanto writeCount forem zero. Lock downgrade é possível: uma thread que possui o lock de escrita pode obter o lock de leitura sem liberar o de escrita (útil para fazer operações atômicas).

  1. Comparação entre os Mecanismos de Lock

Mecanismo Vantagens Desvantagens
synchronized Sintaxe simples, gerenciado pela JVM, otimizações automáticas (lock coarsening, lock elimination, biased locking), liberação automática do lock, menor risco de deadlock. Não oferece recursos avançados como fair lock, try‑lock, timeout, lock interruptível, read/write lock.
Lock (interface) Oferece todos os recursos que synchronized não tem. Exige liberação manual do lock (risco de esquecer).
ReentrantLock Reentrante, suporta fair lock, permite try‑lock e lock interruptível, maior flexibilidade. Mais complexo que synchronized, liberação manual, pode ter maior consumo de recursos.
ReentrantReadWriteLock Alta performance em cenários com muitos leitores e poucos escritores. Não é adequado quando leitura e escrita têm frequências similares ou quando há mais escritas.

Tags: java synchronized ReentrantLock ReadWriteLock Condition

Publicado em 6-29 16:30