domingo, 15 de julho de 2012

Atenção para o Singleton - ambientes multi-thread

Ultimamente estou dando uma "relida" no livro Head First Design Patterns (o qual eu recomendo para programadores Java pela didática e exemplos de código).
Passando pelo capítulo que fala sobre o design do Singleton, acabei criando uns testes legais no meu PC que mostram como é possível cair em armadilhas achando que está criando uma classe corretamente.

 O design do Singleton é muito simples e acredito que isso motiva muitos programadores a não ir mais a fundo e acabam achando que são mestres em Design Patterns (e vendem isso como facinho - isso que é desmerecer o próprio trabalho).

Bom, a definição formal do Singleton é a seguinte (conforme a definição do GoF):
"O Singleton garante que uma classe tenha apenas uma instância e fornece um ponto global para acessar esta instância".

O problema em questão é o seguinte. Ao usar um singleton é possível ter a ilusão de que sempre haverá uma única instância de uma classe por toda eternidade enquanto o sistema estiver executando.

Geralmente, a definição geral para uma classe Singleton é a seguinte:

public class Singleton {
  private static Singleton instance;
  
  private Singleton() {    
  }
  
  public static Singleton getInstance() {
    if (instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}
Escrever uma classe da forma acima não garante que teremos uma única instância. Pensando no todo, devemos considerar esta classe sendo utilizada em um ambiente multi-thread.

A maioria dos projetos em que se trabalha com Java, são projetos para WEB, e como todos sabemos, para cada requisição realizada no servidor, uma nova thread é criada. Portanto, com esta classe Singleton que escrevi acima, não podemos afirmar que existirá apenas uma instância. Pode haver uma instância para cada thread.

Para simular esta situação, criei uma classe muito parecida com a do livro do HF (Head First) e criei outra classe que cria threads para utilização da primeira. Abaixo segue o código da primeira classe (O Singleton que queremos utilizar no projeto).

ChocolateBoiler.java

package br.com.testes.patterns.singleton;

public class ChocolateBoiler {
 private boolean empty;
 private boolean boiled;
 private static ChocolateBoiler instance;
 
 private ChocolateBoiler() {
  empty = true;
  boiled = false;
  System.out.println("criando instancia");
 }
 
 public static ChocolateBoiler getInstance() {
  if (instance == null) {
   instance = new ChocolateBoiler();
  }
  return instance;
 }
 
 public synchronized void fill() {
  if (isEmpty()) {
   System.out.println("filling the choco tank");
   empty = false;
   boiled = false;
  }
 }
 
 public synchronized void drain() {
  if (!isEmpty() && isBoiled()) {
   System.out.println("draining the chocolate");
   empty = true;
  }
 }
 
 public synchronized  void boil() {
  if (!isEmpty() && !isBoiled()) {
   System.out.println("boiling the chocolate");
   boiled = true;
  }
 }

 public boolean isEmpty() {
  return empty;
 }

 public boolean isBoiled() {
  return boiled;
 }

}
Agora a classe abaixo monta o cenário multi-threaded que queremos testar.

 ChocoHolic.java

package br.com.testes.patterns.singleton;

public class ChocoHolic {
 public static void main(String[] args) {
  
  Runnable task = new Runnable() {
   @Override
   public void run() {
    ChocolateBoiler boiler = ChocolateBoiler.getInstance();
    boiler.fill();
    
    try {
     Thread.sleep((long) (Math.random() * 3 * 1000));
    } catch (InterruptedException e) { }
    
    boiler.boil();
    boiler.drain();
   }
  };
  
  new Thread(task).start();
  new Thread(task).start();
  new Thread(task).start();
  
 }
}

Ao executar a classe ChocoHolic acima, um dos resultados que tive foi o seguinte:

criando instancia
filling the choco tank
criando instancia
filling the choco tank
criando instancia
filling the choco tank
boiling the chocolate
draining the chocolate
boiling the chocolate
draining the chocolate
boiling the chocolate
draining the chocolate

Repare na saída acima, como a frase "criando instancia" foi impressa 3 vezes. Logo, podemos concluir que foram criadas 3 instâncias =/ Além disso, uma confusão geral aconteceu por ali. O tanque de chocolate foi preenchido mais de uma vez (derramando tudo pela sala...). A imagem abaixo mostra as threads que temos em execução ao realizar um debug no eclipse.


Agora imagine que cada thread seja trocada a tempo de cada uma alcançar a instrução que cria a instância de ChocolateBoiler como mostrado no screenshot do debug abaixo:


O resultado é o que tivemos na saída mostrada como exemplo após executar o código.

Mas como corrigir isso?

Uma das formas de corrigir este problema, é fazer com que o método estático que retorna a instância da classe, seja sincronizado. Desta forma, apenas uma thread por vez poderá solicitar a criação do objeto.
Agora, temos o novo código da classe ChocolateBoiler sincronizando o método getInstance().

package br.com.testes.patterns.singleton;

public class ChocolateBoiler {
 private boolean empty;
 private boolean boiled;
 private static ChocolateBoiler instance;
 
 private ChocolateBoiler() {
  empty = true;
  boiled = false;
  System.out.println("criando instancia");
 }
 
 public synchronized static ChocolateBoiler getInstance() {
  if (instance == null) {
   instance = new ChocolateBoiler();
  }
  return instance;
 }
 
 public synchronized void fill() {
  if (isEmpty()) {
   System.out.println("filling the choco tank");
   empty = false;
   boiled = false;
  }
 }
 
 public synchronized void drain() {
  if (!isEmpty() && isBoiled()) {
   System.out.println("draining the chocolate");
   empty = true;
  }
 }
 
 public synchronized  void boil() {
  if (!isEmpty() && !isBoiled()) {
   System.out.println("boiling the chocolate");
   boiled = true;
  }
 }

 public boolean isEmpty() {
  return empty;
 }

 public boolean isBoiled() {
  return boiled;
 }

}

Executando o código novamente, tive a seguinte saída:

criando instancia
filling the choco tank
boiling the chocolate
draining the chocolate

Agora veja como a execução foi totalmente controlada. Nada de várias instâncias, e nada de chocolate derramado... Existem outras formas de corrigir este problema sem sincronizar o método getInstance, uma vez que chamar métodos sincronizados tem um custo muito alto em uma aplicação real. Uma alternativa citada pelo livro, é a de criar a instância diretamente na declaração do atributo da classe (not-lazily created instance).

public class ChocolateBoiler {
 private boolean empty;
 private boolean boiled;
 private static ChocolateBoiler instance = new ChocolateBoiler();
 
 private ChocolateBoiler() {
  empty = true;
  boiled = false;
  System.out.println("criando instancia");
 }
 
 public static ChocolateBoiler getInstance() {
  return instance;
 }
 ...
 ...

Outra alternativa que achei interessante foi a de só sincronizar a chamada quando a instância ainda não existir usando o conceito de double-checked locking.

package br.com.testes.patterns.singleton;

public class ChocolateBoiler {
 private boolean empty;
 private boolean boiled;
 private volatile static ChocolateBoiler instance;
 
 private ChocolateBoiler() {
  empty = true;
  boiled = false;
  System.out.println("criando instancia");
 }
 
 public static ChocolateBoiler getInstance() {
  
  if (instance == null) {
   synchronized (ChocolateBoiler.class) {
    if (instance == null) {
     instance = new ChocolateBoiler();
    }
   }
  }
  
  return instance;
 }
 
  ...
  ...
  // o resto do codigo continua igual

Bom... a idéia aqui foi alertar quanto a utilização do Singleton.
Até a próxima.

2 comentários:

  1. O seu teste do singleton criou varias instancias porque ao acessar a variável de memoria todas as suas Threads acessaram ao mesmo tempo, fazendo que com que o espaço em memoria reservado para sua variável INSTANCE estivesse null, se você fizer um getInstance e depois iniciar as threads, seu singleton não devera mais criar instancias, faça um teste ;)

    ResponderExcluir
    Respostas
    1. Boa observação skull.
      Porém a idéia do post foi mostrar exatamente o singleton rodando em um ambiente multithreads. Considerando aplicações WEB onde as threads são criadas pelo próprio container, o singleton será criado dentro da thread (a qual nem nos preocupamos em saber que ela existe) assim como no exemplo.
      Mas caso tivessemos uma aplicação Desktop onde o código acima estivesse sendo utilizado, aí realmente seria mais fácil não utilizar o synchronize e evitariamos o overhead no programa.

      Excluir