Sincronização de Threads para Formar Moléculas de Água

Este problema aborda a sincronização de threads para simular a formação de moléculas de água (H2O). Temos dois tipos de threads: hidrogênio ('H') e oxigênio ('O'). O objetivo é garantir que as threads se combinem corretamente para formar moléculas de água, com dois átomos de hidrogênio e um de oxigênio, respeitando as restrições de concorrência.

Cada thread, ao chegar a um ponto de sincronização (barreira), deve aguardar até que os componentes necessários para formar uma molécula de água estejam disponíveis. Se um thread de oxigênio chegar e não houver dois threads de hidrogênio esperando, ele deve aguardar. Similarmente, se um thread de hidrogênio chegar e não houver um oxigênio e outro hidrogênio disponíveis, ele também deve esperar.

A regra principal é que a formação de uma molécula de água deve ser concluída antes que a próxima possa começar. Isso implica que os contadores de hidrogênio e oxigênio devem ser resetados após cada formação bem-sucedida.

Exemplo 1:

Entrada: "HOH" Saída: "HHO"

Exemplo 2:

Entrada: "OOHHHH" Saída: "HHOHHO"

Restrições:

  • O comprimento total da string de entrada é 3*n*, onde 1 ≤ *n* ≤ 50.
  • O número total de 'H' na entrada é 2*n*.
  • O número total de 'O' na entrada é *n*.

Solução Utilizando Locks e Conditions

Uma abordagem eficiente para resolver este problema é utilizando locks (ReentrantLock) e conditions (Condition) do pacote java.util.concurrent.locks. Isso oferece um controle mais granular sobre a sincronização em comparação com os métodos wait(), notify() e notifyAll() da classe Object.

Manteremos contadores para os átomos de hidrogênio e oxigênio que estão aguardando para formar uma molécula. Usaremos um lock para proteger o acesso a esses contadores e a lógica de formação de moléculas.

Para cada tipo de átomo, utilizaremos uma Condition associada ao lock. A thread de hidrogênio esperará na hydrogenCondition se já houver dois hidrogênios aguardando. A thread de oxigênio esperará na oxygenCondition se já houver um oxigênio aguardando.

Quando um átomo é liberado (ou seja, seu método run() é chamado), seu respectivo contador é incrementado. Em seguida, verificamos se a condição para formar uma molécula de água (2 hidrogênios e 1 oxigênio) foi atendida. Se sim, resetamos os contadores e sinalizamos (usando signalAll()) ambas as condições. Isso acorda todas as threads que estavam esperando, permitindo que elas reavaliem suas condições de espera.

O uso de volatile para os contadores garante que as alterações feitas por uma thread sejam visíveis para as outras imediatamente.

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

class H2O {
    private final Lock moleculeLock = new ReentrantLock();
    private final Condition hydrogenWait = moleculeLock.newCondition();
    private final Condition oxygenWait = moleculeLock.newCondition();

    // Contadores voláteis para garantir visibilidade entre threads
    private volatile int pendingHydrogen = 0;
    private volatile int pendingOxygen = 0;

    public H2O() {
        // Construtor vazio
    }

    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
        moleculeLock.lock();
        try {
            // Aguarda se já existirem 2 átomos de hidrogênio aguardando
            while (pendingHydrogen == 2) {
                hydrogenWait.await();
            }
            
            // Libera o átomo de hidrogênio e incrementa o contador
            releaseHydrogen.run();
            pendingHydrogen++;
            
            // Verifica se uma molécula pode ser formada
            checkAndFormMolecule();
        } finally {
            moleculeLock.unlock();
        }
    }

    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
        moleculeLock.lock();
        try {
            // Aguarda se já existir 1 átomo de oxigênio aguardando
            while (pendingOxygen == 1) {
                oxygenWait.await();
            }
            
            // Libera o átomo de oxigênio e incrementa o contador
            releaseOxygen.run();
            pendingOxygen++;
            
            // Verifica se uma molécula pode ser formada
            checkAndFormMolecule();
        } finally {
            moleculeLock.unlock();
        }
    }
    
    private void checkAndFormMolecule() {
        // Se a combinação para uma molécula de água for completa
        if (pendingHydrogen == 2 && pendingOxygen == 1) {
            // Reseta os contadores para a próxima molécula
            pendingHydrogen = 0;
            pendingOxygen = 0;
            
            // Sinaliza todas as threads esperando para que possam tentar formar a próxima molécula
            hydrogenWait.signalAll();
            oxygenWait.signalAll();
        }
    }
}

Solução Utilizando Sincroinzação Padrão (synchronized, wait, notifyAll)

Uma alternativa é utilizar os mecanismos de sincronização intrínsecos do Java: o bloco synchronized e os métodos wait(), notifyAll(). Embora menos flexível que locks e conditions, esta abordagem também pode resolver o problema corretamente.

O princípio é semelhante: usamos um objeto this como monitor para o bloco synchronized. Dentro dele, verificamos as contagens de hidrogênio e oxigênio. Se as condições de espera forem atendidas (por exemplo, se já houver dois hidrogênios esperando), a thread chama wait() para liberar o monitor e entrar em estado de espera. Caso contrário, a thread executa sua ação (run()), icnrementa o contador arpopriado e, se uma molécula for formada, reseta os contadores e chama notifyAll() para acordar todas as threads em espera.

class H2O {
    private int hydrogenCount = 0;
    private int oxygenCount = 0;

    public H2O() {
        // Construtor vazio
    }

    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
        synchronized (this) {
            // Aguarda se já houver 2 hidrogênios esperando
            while (hydrogenCount == 2) {
                wait();
            }
            
            // Libera o hidrogênio e incrementa o contador
            releaseHydrogen.run();
            hydrogenCount++;
            
            // Verifica se uma molécula completa foi formada
            if (hydrogenCount == 2 && oxygenCount == 1) {
                // Reseta contadores e notifica outras threads
                hydrogenCount = 0;
                oxygenCount = 0;
            }
            
            // Notifica todas as threads em espera
            notifyAll();
        }
    }

    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
        synchronized (this) {
            // Aguarda se já houver 1 oxigênio esperando
            while (oxygenCount == 1) {
                wait();
            }
            
            // Libera o oxigênio e incrementa o contador
            releaseOxygen.run();
            oxygenCount++;
            
            // Verifica se uma molécula completa foi formada
            if (hydrogenCount == 2 && oxygenCount == 1) {
                // Reseta contadores e notifica outras threads
                hydrogenCount = 0;
                oxygenCount = 0;
            }
            
            // Notifica todas as threads em espera
            notifyAll();
        }
    }
}

Tags: java Concorrência Sincronização threads Locks

Publicado em 10-21 11:46