quarta-feira, 8 de agosto de 2012

Construindo um Singleton thread-safety em Java

O que é um Singleton?
Singleton é um Design Pattern que explora a necessidade de se ter uma única instância de determinado objeto num sistema. Todos os outros objetos precisam de apenas uma instância desse objeto. Imagine a situação de um sistema de logs. Uma única instância deste componente poderia ser criada para que todo o sistema acesse seus recursos de log. O padrão Singleton garante que uma classe só seja iniciada uma vez.

Principais características 
1. Um Singleton deve ter uma variável private static que referência a própria classe
2. Um Singleton deve ter seu constructor private ou protected  
3. Deve ter um método de acesso que retorna uma única instância do objeto

O código abaixo constitui um exemplo de um componente que cria uma Singleton de log fazendo com que o mesmo seja lazy initialization (ou seja, inicialização preguiçosa, que só é instanciada quando for necessária).

public class LogSingleton {
  private static LogSingleton log;
  private LogSingleton() {}
  public LogSingleton getInstance() {
    if ( log == null ) log = new LogSingleton();
    return log;
  }
  public void append(String message) {
    System.out.println( "[LOG] " + message + " [LOG]" );
  }
}

Observe que neste exemplo, os três itens são atendidos. Porém, este exemplo não é thread-safety. Isso significa que não é garantido o bom funcionamento da Singleton num sistema onde se trabalhe com multithreading. Imagine que existam duas threads A e B. A thread A fez uma chamada à getInstance(). Ela veifica se log é nulo. Como foi a primeira a acessar o objeto, o resultado é verdadeiro. Por conseqüência a thread A passa para a segunda faze da condição – instanciar o objeto LogSingleton. Porém, neste exato momento, a thread B fez uma chamada a getInstance() e executa a regra condicional ( log == null ). Como a thread A ainda não instanciou o objeto log, a thread B encontra um resultado positivo na condição, fazendo com que ela também tente instanciar log. Em seguida a thread A instancia log e depois a thread B instancia mais uma vez log. O padrão Singleton deveria ser capaz de ser instanciado uma única vez, portanto este exemplo não é thread-safety.

Utilizando a técnica double-check
Uma solução para o código anterior é torná-lo synchronized:

public LogSingleton getInstance() {
  synchronized (LogSingleton.class) {
    if ( log == null ) log = new LogSingleton();
  }
  return log;
}

Porém, a cada chamada de getInstance(), seria necessário recorer ao recurso synchronized que é muito lento. Existe a técnica de double-check que faz com que apenas a criação do objeto seja sincronizada.

public LogSingleton getInstance() {
  if ( log == null ) {
    synchronized (LogSingleton.class) {
      if ( log == null ) log = new LogSingleton();
    }
  }
  return log;
}

Neste caso, imagine a situação: a thread A faz uma chamada a getInstance() que, como log é nulo, entra no escopo sincronizado. Neste ponto, nenhuma outra thread poderá instanciar o objeto. A thread B chama getInstance() e, como log ainda não foi inicializado, entra no início do escopo sincronizado, porém aguarda até que a thread A termine seu serviço. Até aqui tudo bem. Neste ponto, nós temos a segurança. O delay gerado pelo synchronized é necessário e filtrado pelo primeiro IF. A thread A está no processo de instanciar o log. O Virtual Machine ainda está levantando este objeto na memória. Contudo, a thread C faz uma chamada a getInstance() que, encontra algo diferente de null em log, porém log ainda não foi totalmente criado. A thread C obteve a instância (inacabada) de log e em suas atividades fez uma chamada ao método append() (mostrado no código completo). Como log ainda não é verdadeiramente um LogSingleton, ele não tem capacidade de chamar append(). Aqui encontramos uma situação perigosa e não thread-safety. Portanto, a técnica double-ckeck não funciona, pois o log == null não pode garantir sincronização na condição, proporcionando situações como esta.

Singleton thread-safety sem a lazy initialization
Primeira opção é abandonar a lazy initialization:

public class LogSingleton {
  private static LogSingleton log = new LogSingleton();
  private LogSingleton() {}
  public LogSingleton getInstance() {
    return log;
  }
  public void append(String message) {
    System.out.println( "[LOG] " + message + " [LOG]" );
  }
}

Neste caso, nosso sistema teria que obrigatoriamente instanciar log, mesmo que este nunca seja utilizado. Este caso é thread-safety.

Singleton thread-safety com getInstance() sincronizado

public class LogSingleton {
  private static LogSingleton log;
  private LogSingleton() {}
  public LogSingleton getInstance() {
    synchronized (LogSingleton.class) {
      if ( log == null ) log = new LogSingleton();
    }
    return log;
  }
  public void append(String message) {
    System.out.println( "[LOG] " + message + " [LOG]" );
  }
}

Como já foi mostrado, este caso é thread-safety, mas é reconhecida uma perda de performace só pelo fato de entrarmos num escopo sincronizado. A cada chamada a getInstance(), nosso sistema teria que passar por esse recurso.

Singleton thread-safety com técnica initialize-on-demand holder class
Esta opção é a mais elegante. Observe o código:

public class LogSingleton {
  private static class LogHolder {
    private static final LogSingleton log = new LogSingleton();
  }
  private LogSingleton() {}
  public LogSingleton getInstance() {
    return LogHolder.log;
  }
  public void append(String message) {
    System.out.println( "[LOG] " + message + " [LOG]" );
  }
}

Esta técnica é interessante porque log não é instanciado até que o método getInstance() seja chamado. Supondo que log utilize muitos recursos, o sistema não seria obrigado a criar sua instância até que o mesmo seja realmente necessário. A desvantagem deste caso é que o objeto log é estático. Esta técnica não funciona com objetos de instância. Quando alguma thread chamar getInstance(), uma referência ao atributo log de LogHolder é realizada. Consequentemente a classe interna é instanciada na memória. Este método é thread-safety e só instancia LogSingleton pela real necessidade.

Qual técnica utilizar (conclusão)?
Certamente a double-check deve ser descartada, pois apresenta falhas. Se você possui um Singleton que necessite ter uma instância não estática (lazy initialization), você terá que sincronizar getInstance() - opção thread-safety, porém muito lenta. Se uma inicialização estática do objeto Singleton resolver o seu problema (como em nosso exemplo de log), a initialize-on-demand é a melhor situação, pois só criará o objeto quando o mesmo for necessário. Em alguns casos , você pode entender que o Singleton será sempre necessário – como um caso de Singleton que faça as conexões com o banco de dados, por exemplo. Neste caso, você poderia simplesmente utilizar a prática de criá-lo como atributo estático na própria classe, o método mais simples.
Cada caso requer um estudo para verificar a melhor opção a ser utilizada. Se estiver com dúvidas de qual caso utilizar, proponho que use a initialize-on-demand.

"Faça ou não faça. A tentativa não existe." Yoda
Obrigado pela leitura.
Guilherme Pontes

Nenhum comentário:

Postar um comentário