Dominando o Singleton em Java: Princípios, Aplicações e Melhores Práticas

O Singleton é um design pattern bastante utilizado, e bem simples de ser implementado, ele é um padrão de design do tipo criacional, seu uso nos permite limitar o número de instâncias de um objeto criado a apenas um.

Esse tipo de abordagem é bem útil quando você identifica que apenas uma instância é o suficiente para processar ações do sistema inteiro. Essa abordagem também é útil para reduzir o número de consumo de memória, já que criar várias instâncias de um mesmo objeto pode consumir muita memória, ao processar uma simples requisição.

Esse padrão também não deve ser utilizado com exageros, já que sua implementação em determinadas situações pode limitar o processamento em paralelo da sua aplicação, desta forma é bom identificar os pontos corretos de aplicar esse design.

Implementação em Java

Uma das formas mais simples de implementar esse padrão em Java é dessa forma.

public class Singleton {

	private static Singleton instance;
	
	private void Sigleton() {
		// Construtor privado
	}
	
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
	
	public void someMethod() {
    System.out.println(this.toString());
	}
	
}
Singleton.java

Vamos explicar nossa implementação, você pode observar que temos um atributo chamado instance dentro da nossa própria classe, esse atributo será o responsável por armazenar a única instância dessa classe, e para recuperarmos essa instância criamos o método getInstance encapsulando a nossa única instancia desse objeto.

E por último como o construtor dessa classe é privado, a única classe que pode construir uma instância dela é ela mesmo, como você pode notar é feito uma verificação se já existe uma instância antes de retornar a que possuímos, caso ela seja nula porque não foi criada ainda, ela é instanciada.

Testando nossa implementação.

public class TestSingleton {

	public static void main(String[] args) {
		Singleton instance = Singleton.getInstance();
		instance.someMethod();
		
		Singleton instance2 = Singleton.getInstance();
		instance2.someMethod();
	}
}
TestSingleton.java

Observe no nosso teste, que mesmo executando o “getInstance” mais de uma vez, é sempre nos dado a mesma instância podemos verificar isso através da impressão do endereço de memória quando estamos executando o teste.

O padrão é bem simples de ser implementado, mas esse método que utilizamos não é thread safe, ou seja se você está trabalhando utilizando recursos multi thread esse método pode falhar fazendo com que mais de uma instância seja criada, mas como fazer uma implementação thread safe? Vamos mostrar o exemplo a seguir.

Implementação do Singleton thread safe

Um dos melhores recursos quando se está criando uma aplicação em Java é utilizar multi threads para o processamento, então devemos pensar nisso quando estamos colocando em prática o padrão, agora vamos ensinar a implementar ele de forma thread safe.

public class Singleton {

	private static volatile Singleton instance;
	
	public static Singleton getInstance() {
		if(instance == null) {
			synchronized(Singleton.class) {
				if(instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
	
	public void someMethod() {
		System.out.println(this.toString());
	}
}
Singleton.java

Perceba que a implementação é bem parecida com a anterior, com algumas pequenas modificações, a adição da palavra reservada “volatile que vai nos garantir que as alterações feitas por uma thread seja visíveis em outras, e a verificação dupla dentro do “getInstance” é para minimizar a sincronização utilizando o método “synchronized“, pois é um recurso que tem um custo elevado, mas com a verificação dupla diminuímos esse custo uma vez que a instância é criada. Vamos testar nossa implementação.

public class TestSingleton {

	public static void main(String[] args) {
		
		for(int i=0; i<10; i++) {
			Thread thread = new Thread(() -> {
				Singleton instance = Singleton.getInstance();
				instance.someMethod();
			});
			
			thread.run();
		}
	}
}
TestSingleton.java

No nosso teste criamos 10 threads para validar o funcionamento thread safety, se executar o teste vai notar que o mesmo endereço de memória é impresso no terminal.

Embora essa seja uma forma bem eficiente de aplicar esse padrão, a partir do Java 5 com a introdução de enum, podemos ter um método mais eficiente ainda.

public enum Singleton {

	INSTANCE;
	
	public void someMethod() {
		System.out.println(this.toString();
	}
}
Singleton.java

Nesse exemplo criamos a partir de uma enum, que já são thread safe, ela vai nos garantir que haverá apenas uma instância, e dentro dela podemos criar os métodos que precisamos.

Quando utilizar

É interessante analisar bem onde será implementado esse tipo de padrão, para não impactar sua aplicação conforme já mencionamos antes, mas aqui vai algumas sugestões de pontos de uma aplicação que pode ser interessante aplicar o padrão:

  • Acesso global: Quando você precisa ter uma classe que deve conter exatamente a mesma instância para coordenar ações do sistema, por exemplo um gerenciador de configurações ou um pool de conexões com um banco de dados.
  • Controle central: Em casos que é necessário um ponto central de controle da aplicação, que ajuda a controlar as operações como um gerenciador de recursos ou um serviço de log.
  • Cache: Quando você precisa de um cache global, que armazene os dados ou recursos que são mais utilizados, esse padrão pode garantir que somente uma instância do cache exista.
  • Outros padrões: Esse padrão é bastante utilizado para apoiar outros padrões de projeto, como o Factory por exemplo, com ele é possível garantir que você tenha apenas uma instância da fábrica, não há necessidade de ter múltiplas instâncias de fábricas se elas não participaram do processamento de fato.
  • Conexões externas: Em sistemas que lidam com conexões com recursos externos, como um serviço de mensageria com o Kafka por exemplo ou banco de dados, esse padrão pode ajudar a gerenciar a conexão ou o pool de conexões.
  • Recursos compartilhados: Quando você precisa que uma informação seja compartilhada com várias instâncias de objetos, apenas o compartilhamento de informação não necessita que uma instância seja criada toda vez que você precisa da informação.

Vale lembrar que o seu uso excessivo pode trazer diversas complicações para sua aplicação, como acidentalmente criar um estado global para a aplicação, que pode tornar o código complexo demais para dar qualquer tipo de manutenção, ainda pode implicar em dificuldades de realizar testes, alocação prematura de uma instância que nunca será utilizada ou dificuldades de substituição da implementação.

Como todo recurso ou padrão deve ser bem avaliado onde será utilizado, para que uma solução não se torne um problema usá-lo da maneira correta pode diminuir muito o custo de processamento da sua aplicação e ter ganhos com o compartilhamento de informação dentro da aplicação assim como a centralização de determinados recursos.

Links uteis

Mauricio Lima
Mauricio Lima

Bacharel em Ciência da Computação, profissional dedicado ao desenvolvimento de software e entusiasta da tecnologia.

Artigos: 65