Last Updated on 27/12/2023
Singleton é um design pattern criacional que garante que uma classe possua uma única instância e define um ponto de acesso global para ela.
Introdução
Algumas vezes faz sentido que em um sistema exista apenas uma única instância de uma certa classe, por exemplo, arquivos de configurações que leia propriedades de um único arquivo, ou para enviar arquivos à uma impressora.
Mesmo que o Singleton pareça ser um dos patterns mais populares e um dos mais fáceis de se implementar, ainda existem algumas funcionalidades do Java como concorrência e serialização que podem deixar algumas brechas na implementação do pattern, brechas estas que precisamos cobrir para garantir que nossas instâncias do Singleton são realmente, únicas.
Dito isso, este post não será apenas “mais um rápido tutorial sobre Singleton”, mas irá mostrar como escrever um código realmente aderente ao contrato do pattern, tanto usando uma classe Java comum quanto sua versão alternativa com enum
.
Estrutura do Singleton
Iremos criar uma classe chamada SystemInfo que será nossa classe Singleton, além de uma classe chamada SystemClient que utilizará nosso Singleton.
Abaixo está o diagrama UML com sua representação:
Algumas considerações importantes a respeito do diagrama:
- SystemInfo é a classe que implementa o pattern e obtém as informações de sistema que as classes clientes necessitam. Somente por dizermos que esta classe possui duas responsabilidades já podemos notar um ponto negativo neste pattern: falta um pouco de coesão.
- SystemClient é a classe cliente nesta representação, porém poderia ser qualquer classe cliente em nossos sistemas.
- Observe que os atributos e métodos sublinhados são estáticos, e para esta implementação do pattern,
eles são os únicos obrigatórios, enquanto que os restantes existem apenas para demonstração.
Implementação
Abaixo estão as classes Java que implementam o pattern. Alguns javadocs, comentários, getters, setters, construtores e sobrescritas de toString
foram omitidos para fins de legibilidade. Por favor acesse o repositório no GitHub para obter a versão completa deste código.
A Primeira Versão
Abaixo está a primeira e mais simples versão da implementação de nosso pattern, já com lazy initialisation:
package com.bgasparotto.designpatterns.singleton; public final class SystemInfo { private static SystemInfo instance; private String systemName; private String javaVersion; private SystemInfo() { systemName = System.getProperty("os.name"); javaVersion = System.getProperty("java.version"); } public static SystemInfo getInstance() { if (instance == null) { instance = new SystemInfo(); } return instance; } // Instance getters }
Essa versão do código parece fazer o trabalho. Ela espera até a primeira invocação para somente então inicializar a instância do Singleton, e garante que a mesma instância seja retornada em todas as chamadas. Adicionalmente, o construtor é privado, forçando as classes clientes a invocar o método getInstance
. Além disso, este construtor faz com que seja impossível extender essa classe devido a falta de construtores acessíveis, então, nós adicionamos o modificador final
para deixar este comportamento anti-herança mais claro. Porém, isso ainda não é o que queremos.
Concorrência: race condition
Se o nosso código atual da classe SystemInfo
for executado em uma aplicação multi-thread, eis o que pode acontecer:
- Thread A invoca o método
getInstance
; - Thread A avalia a expressão
if (instance == null)
emtrue
; - Thread A vai para o estado runnable e a Thread B vai para o estado running;
- Thread B invoca o método
getInstance
; - Thread B também avalia a expressão
if (instance == null)
emtrue
; - Thread B instancia um objeto de
SystemInfo
e o atribui ao campo estáticoinstance
; - Thread B vai para o estado runnable e a Thread A toma seu lugar novamente;
- Thread A instancia outro objeto de SystemInfo e substitui o objeto atual no campo estático;
- Nós temos agora duas instâncias de
SystemInfo
, a classe falhou em ser um singleton.
Para resolver isso, poderíamos simplesmente adicionar a palavra-chave synchronized
no método getInstance
:
public static synchronized SystemInfo getInstance() { if (instance == null) { instance = new SystemInfo(); } return instance; }
Mas será que realmente compensa sincronizar um método e assim perder desempenho, somente porque queríamos ganhar tal desempenho fazendo com que nossa classe tivesse inicialização lazy? Provavelmente não.
Outra opção seria criar uma classe privada e estática que também fosse carregada com a primeira invocação para referenciar nossa instância Singleton, deste modo, nós não perdereremos desempenho por causa de sincronização e ainda sim continuamos com nossa funcionalidade de lazy initialisation. Adicionalmente, nós podemos remover o campo instance
e o null check de SystemInfo
:
private static class SystemInfoHolder { private static final SystemInfo INSTANCE = new SystemInfo(); } public static SystemInfo getInstance() { return SystemInfoHolder.INSTANCE; }
Neste ponto, acabamos de fazer com que nossa classe seja thread-safe.
A Segunda Versão
Aqui vai a segunda e atual versão de nossas classe Singleton:
package com.bgasparotto.designpatterns.singleton; public final class SystemInfo { private String systemName; private String javaVersion; private static class SystemInfoHolder { private static final SystemInfo INSTANCE = new SystemInfo(); } private SystemInfo() { systemName = System.getProperty("os.name"); javaVersion = System.getProperty("java.version"); } public static SystemInfo getInstance() { return SystemInfoHolder.INSTANCE; } // Instance getters }
Ela garante que somente uma instância inicializada sob demanda seja obtida através do método getInstance
em ambientes multi-thread. Porém, esta garantia pode ser quebrada novamente com a adição de duas palavras em nossa classe.
Serialização
Se nós simplesmente adicionarmos as palavras implements Serializable
em nossa classe, já não será mais garantido que somente uma instância esteja disponível:
public final class SystemInfo implements Serializable { // ... }
Deste modo, uma segunda instância poderá ser criada quando o mecanismo de serialização “deserializar” o objeto através do processo de deserialização padrão ou através de uma implementação de readObject(ObjectInputStream in)
personalizada.
Para resolvermos este problema, nós precisamos prover uma implementação personalizada para outro método do mecanismo de serialização chamado readResolve()
que retorna um Object
, o qual substitui a instância privamente lida durante o processo de deserialização, fazendo com que ela já seja elegível para remoção pelo Garbage Collector. Tal implementação personalizada irá simplesmente ignorar quaisquer objetos lidos e retornar nossa instância Singleton:
public final class SystemInfo implements Serializable { private Object readResolve() throws ObjectStreamException { return SystemInfoHolder.INSTANCE; } // ... }
Adicionalmente, nós precisamos marcar todas as variáveis que referenciam outros objetos como transient
, de modo a evitar a obtenção da instância recém deserializada antes da execução do readResolve
:
private transient String systemName; private transient String javaVersion;
Agora, nossa classe singleton garante que apenas uma única instância será disponibilizada, em ambientes multi-thread e através de serialização.
A Terceira Versão
Aqui vai a terceira versão da nossa classe Singleton:
package com.bgasparotto.designpatterns.singleton; import java.io.ObjectStreamException; import java.io.Serializable; public final class SystemInfo implements Serializable { private static final long serialVersionUID = 2780033083469952344L; private transient String systemName; private transient String javaVersion; private static class SystemInfoHolder { private static final SystemInfo INSTANCE = new SystemInfo(); } private SystemInfo() { systemName = System.getProperty("os.name"); javaVersion = System.getProperty("java.version"); } private Object readResolve() throws ObjectStreamException { return SystemInfoHolder.INSTANCE; } public static SystemInfo getInstance() { return SystemInfoHolder.INSTANCE; } // Instance getters }
Estamos quase lá! Só precisamos cuidar de um último detalhe.
Reflection
Mesmo que o mecanismo básico de reflection respeite os modificadores de acesso das classes, é possível realizar um by-pass utilizando o método AccessibleObject.setAccessible(boolean flag)
para acessar elementos privados através de reflection.
O código abaixo utiliza reflection para obter acesso ao construtor privado e torná-lo acessível, possibilitando então instanciar manualmente outro objeto dessa classe:
public static void main(String[] args) throws Exception { // Obtains the singleton instance normally. SystemInfo firstInstance = SystemInfo.getInstance(); // Make the private constructor accessible and obtains another instance. Constructor<SystemInfo> c = SystemInfo.class.getDeclaredConstructor(); c.setAccessible(true); SystemInfo secondInstance = c.newInstance(); // Both instances are different. System.out.println(firstInstance == secondInstance); }
O código acima irá reproduzir o seguinte resultado:
false
Para impedir que isso aconteça, nós podemos mover nosso código de inicialização para um bloco de inicialização ou para os próprios campos como estamos prestes a fazer, além de fazer com que o construtor privado lance um AssertionError
se o campo INSTANCE
não for nulo. Esta abordagem, diferentemente dos null checks apresentados no início deste tutorial no código do método getInstance
, é thread-safe visto que sempre irá retornar false
após a classe ser carregada pela primeira vez e o campo INSTANCE
ser inicializado.
private transient String systemName = System.getProperty("os.name"); private transient String javaVersion = System.getProperty("java.version"); private SystemInfo() { if (SystemInfoHolder.INSTANCE != null) { throw new AssertionError("Can not instantiate."); } }
A Versão Final
Aqui vai nossa versão final da implementação de singleton, thread-safe e sem brechas para serialização e reflection:
package com.bgasparotto.designpatterns.singleton; import java.io.ObjectStreamException; import java.io.Serializable; public final class SystemInfo implements Serializable { private static final long serialVersionUID = 2780033083469952344L; private transient String systemName = System.getProperty("os.name"); private transient String javaVersion = System.getProperty("java.version"); private static class SystemInfoHolder { private static final SystemInfo INSTANCE = new SystemInfo(); } private SystemInfo() { if (SystemInfoHolder.INSTANCE != null) { throw new AssertionError("Can not instantiate."); } } private Object readResolve() throws ObjectStreamException { return SystemInfoHolder.INSTANCE; } public static SystemInfo getInstance() { return SystemInfoHolder.INSTANCE; } public String getSystemName() { return systemName; } public String getJavaVersion() { return javaVersion; } }
Esta é uma implementação válida de Singleton. Porém, tivemos que sujar nossas mãos para cobrir algumas possíveis brechas que acabaram deixando a nossa classe mais complicada do que esperávamos. Mas existe alguma forma mais fácil de implementar um Singleton com sucesso? Sim, existe.
O Singleton Enum
[…] Um enum de um só elemento é o melhor modo de implementar um Singleton.
– Joshua Bloch. Effective Java 2nd Edition p. 18. (Tradução livre)
Um singleton enum válido que faz exatamente a mesma coisa que a nossa classe anterior pode ser representado no diagrama UML abaixo:
Um enum
possui tudo o que é preciso para prover uma implementaçao válida de Singleton sem a necessidade de esforços adicionais como foi visto nas seções anteriores, então a não ser que você tenha um bom motivo para criar singletons na forma de classes, use um enum
.
E codificado como a seguir:
package com.bgasparotto.designpatterns.singleton; public enum SystemInfoEnum { INSTANCE; private String systemName = System.getProperty("os.name"); private String javaVersion = System.getProperty("java.version"); public String getSystemName() { return systemName; } public String getJavaVersion() { return javaVersion; } }
Agora se obtermos instâncias das duas implementações e invocarmos seus métodos, o resultado é exatamente o mesmo. Note também que a utilização dos dois é muito similar:
package com.bgasparotto.designpatterns.singleton; public class SystemClient { public static void main(String[] args) throws Exception { SystemInfo classInstance = SystemInfo.getInstance(); SystemInfoEnum enumInstance = SystemInfoEnum.INSTANCE; System.out.println("Class: " + classInstance.getSystemName()); System.out.println("Class: " + classInstance.getJavaVersion()); System.out.println("Enum: " + enumInstance.getSystemName()); System.out.println("Enum: " + enumInstance.getJavaVersion()); } }
Conclusão
Nós observamos que uma boa implementação de Singleton não é tão simples como imaginamos, pois existem algumas preocupações com a linguagem que devem ser levadas em consideração.
Tais preocupações envolvem execuções multi-thread e exploração de brechas através de reflection ou serialização, e mesmo que não seja difícil cobrir essas brechas, utilizar um enum
ao invés de uma classe deixa o trabalho muito mais fácil.
É importante ressaltar também que não podemos criar uma sub-classe de um singleton válido, logo este ponto negativo deve ser levado em consideração antes de decidir se um singleton será realmente necessário para resolver seu problema. Se você precisar criar sub-classes de Singleton, você precisará modificar o código da classe original do Singleton pelos motivos discutidos anteriormente.
Referências
- Joshua Bloch. Effective Java Second Edition. Addison-Wesley, 2008.
- GoF. Design Patterns – Elements of Reusable Object-Oriented Software. Addison-Wesley, 1994.
- Stack Overflow – extends of the class with private constructor
Maravilhoso!
Sensacional o seu pensamento de cuidar de todas as opções que podem acontecer.
Parabéns.
Obrigado pelo feedback! Tentei reunir conteúdo que aprendi de diferentes fontes pra montar uma implementação consistente. Que bom que gostou!