Monitores em Java - Synchronize it!
Por Caio Paes
(caio.paes@ccc.ufcg.edu.br)
Entenda como Java possibilita o uso de monitores para a sincronização de múltiplas threads com o uso da palavra reservada synchronized.

O conceito de monitores, para programação concorrente, foi criado por Tony Hoare e a sua primeira implementação foi feita em 1975, por Per Brinch Hansen, na primeira linguagem de programação concorrente, o Concurrent Pascal. Em Java, um monitor é um objeto que garante exclusão mútua na execução de procedimentos a ele associados. Sua utilização é justificada para evitar problemas relacionados ao acesso por múltiplas threads a regiões críticas de um programa.

Uma região crítica é um trecho de código no qual as operações sobre dados compartilhados por mais de um processo necessitam de atomicidade. Um exemplo clássico de um problema que possui uma região crítica em sua solução é o Produtor/Consumidor. Nesse problema, dois processos executam paralelamente e, enquanto o Produtor adiciona produtos em uma fila, o Consumidor os remove.

Para efetuar a sincronização das threads e garantir que apenas uma delas terá o acesso a uma região crítica, precisamos determinar qual parte do código faz parte dessa região. Essa decisão terá reflexos diretos no desempenho do programa. Após esse passo, podemos usar várias estratégias para sincronizar as threads e controlar o acesso à região. Java nos permite o uso de monitores por meio da palavra reservada synchronized. Existem duas formas de se utilizar a palavra reservada synchronized para aplicarmos ao uso de monitores: na declaração de métodos ou no início de um bloco.

Em Java, todo objeto possui um monitor associado. Quando não restringimos um trecho de código como sincronizado, múltiplas threads podem executá-lo paralelamente. O monitor não estará interferindo no controle do acesso ao objeto. Porém, com a indicação de sincronização do trecho (uso de synchronized), as threads precisarão consultar o monitor do objeto e solicitar um “passe” para ter o direito de executar a região crítica. Esse “passe” é devolvido ao objeto após o término da execução da thread naquele trecho de código. Enquanto uma thread detém o direito de executar a região crítica, as próximas que tentarem o acesso ficarão esperando a liberação do passe. É dessa forma que é implementado o conceito de monitores em Java.

Veja, a seguir, um exemplo de uso do synchronized na declaração de um método, na utilização do padrão de projeto Singleton.

public final class MySingleton {
     // Instancia estática privada que será acessada
     private static MySingleton INSTANCE;
 
     // Construtor privado. Suprime o construtor público padrao.
     private MySingleton() {
          // Operações de inicialização da classe
     }
 
     // Método público estático de acesso único ao objeto!
     public static synchronized MySingleton getInstance(){
          if (INSTANCE == null) {
                INSTANCE = new MySingleton();
          }
           return INSTANCE;
     }
}

No exemplo, o uso do synchronized na declaração de um método de classe implica que, quando uma thread tentar executar o método getInstance, essa solicitará ao monitor associado à estrutura estática da classe (que na verdade, internamente, também é um objeto) o passe para executar o corpo do método. Se outra thread tentar acessar o mesmo método, ela ficará esperando o fim da execução da primeira.

Se utilizarmos a construção em um método de instância (observe que não faz sentido no caso da utilização do Singleton), as threads que tentarem executar o método solicitarão o passe ao objeto instanciado. Assim, o controle do passe para a execução da região crítica ocorre individualmente para cada instância na memória.

O outro caso de uso da palavra reservada synchronized é aplicado ao início de um bloco. Veja no exemplo a seguir.

public class ABC {  
  ...
  public void abc() { 
          Objeto1 obj1 = new Objeto1();
          ... // código não sincronizado 
    synchronized(this) // “this” pode ser qualquer objeto 
          {  
             ... // código sincronizado  
    }  
        }
}

Nesse caso, podemos permitir que uma parte do método não tenha controle sobre a sincronização das threads. Outro detalhe é que podemos definir, também, a qual objeto a thread em execução deve solicitar o passe. Não necessariamente será uma instância do objeto que teve o método chamado. No exemplo, se após a palavra synchronized indicássemos um objeto ob1 (do tipo Objeto1), no lugar de this (do tipo ABC), as threads que executassem o método abc não seriam sincronizadas pelo monitor do objeto da classe ABC, e sim com base no passe do monitor da instância obj1.

O uso de monitores, seguindo a estratégia implementada na linguagem Java, é bastante simples e intuitivo. O principal problema, na resolução de problemas de concorrência, se dá na definição de regiões críticas e nas questões de otimização do código. Em alguns casos, é mais interessante para o programador utilizar outros recursos providos pela linguagem para a solução do seu problema de forma mais eficiente quanto ao desempenho. O tempo de execução de um método declarado com synchronized, em Java, pode ser até 100 vezes maior do que o tempo de execução do mesmo método sem a necessidade da sua sincronização.

A dica que prevalece é: não use os mecanismos de sincronização de threads quando não for realmente necessário.

Jornal PETNews - Edição: Jeymisson Oliveira - Revisão: Savyo Nóbrega e Joseana Fechine
Grupo PET Computação UFCG, 2012. All rights reserved.