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