Skip to content

Singleton

Last Updated on 27/12/2023

Singleton Design Pattern Logo


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:

Simple Singleton UML Diagram

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:

  1. Thread A invoca o método getInstance;
  2. Thread A avalia a expressão if (instance == null) em true;
  3. Thread A vai para o estado runnable e a Thread B vai para o estado running;
  4. Thread B invoca o método getInstance;
  5. Thread B também avalia a expressão if (instance == null) em true;
  6. Thread B instancia um objeto de SystemInfo e o atribui ao campo estático instance;
  7. Thread B vai para o estado runnable e a Thread A toma seu lugar novamente;
  8. Thread A instancia outro objeto de SystemInfo e substitui o objeto atual no campo estático;
  9. 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:

Enum Singleton UML Diagram

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

2 thoughts on “Singleton”

  1. Maravilhoso!

    Sensacional o seu pensamento de cuidar de todas as opções que podem acontecer.

    Parabéns.

Leave a Reply

Your email address will not be published. Required fields are marked *