Patrón Singleton
Garantiza que una clase tenga una sola instancia y da un punto de acceso global a ella: la configuración, el registro o el pool de conexiones de una aplicación.
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Singleton
Escribe qué clases piden la configuración, en orden, y compara el Singleton con hacer new en cada una.
Paso 1
El constructor es privado: nadie puede hacer new Configuracion() desde fuera. La única forma de conseguirla es getInstance().
1class Configuracion {
2 private static Configuracion instancia;
3 private Configuracion() { } // instancia = null, objetos creados = 0
4
5 static Configuracion getInstance() {
6 if (instancia == null)
7 instancia = new Configuracion();
8 return instancia;
9 }
10}Variables
- instancia
- null
- objetos creados
- 0
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Algunas cosas solo deben existir una vez en un programa: la configuración, el registro de mensajes (log), el pool de conexiones a la base de datos. Si cada clase hiciera su propio new, habría varias configuraciones que no se ponen de acuerdo, o diez pools con diez veces más conexiones de las permitidas.
El patrón Singleton lo impide desde la propia clase: el constructor es privado (nadie de fuera puede hacer new), la clase guarda su única instancia en un atributo estático y ofrece un método estático, getInstance(), que la devuelve (creándola la primera vez, si se hace de forma perezosa).
Es el patrón más conocido y también el más criticado: un Singleton es, en el fondo, una variable global. Esconde dependencias (una clase usa la configuración sin que se vea en su constructor) y complica las pruebas, porque no se puede sustituir por un doble. Por eso hoy se prefiere que la instancia única la gestione un contenedor de inyección de dependencias, como Spring, que crea un solo objeto y se lo pasa a quien lo necesite.
Cuándo usarlo
- Tiene que haber exactamente un objeto de una clase y todo el programa tiene que usar el mismo: configuración, registro, caché, pool de conexiones, acceso a un dispositivo.
- Crear ese objeto es caro y conviene hacerlo una sola vez (y solo si se llega a usar).
Cuándo no
- Como forma cómoda de compartir datos entre clases: eso es una variable global con otro nombre y acopla todo el código.
- Si usas un framework con inyección de dependencias (Spring, Android con Hilt): declara el objeto como único en el contenedor y recíbelo en el constructor.
- Si el «único» puede dejar de serlo (dos bases de datos, dos usuarios): cambiarlo después cuesta mucho.
Participantes
- Constructor privado. Impide que se creen objetos con
newdesde fuera de la clase. - Atributo estático.
private static Configuracion instanciaguarda la única instancia; al ser estático, pertenece a la clase y no a un objeto. - Método de acceso.
public static Configuracion getInstance()devuelve la instancia, creándola si aún no existe (perezosa) o la tiene ya creada desde que se carga la clase (ansiosa). - Clientes. Las demás clases piden el objeto con
Configuracion.getInstance()y así todas trabajan con el mismo.
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2class Configuracion {
3 -{static} instancia : Configuracion
4 -valores : Map<String, String>
5 -Configuracion()
6 +{static} getInstance() : Configuracion
7 +get(clave : String) : String
8 +set(clave : String, valor : String) : void
9}
10class Pantalla {
11 +mostrar() : void
12}
13class Impresora {
14 +imprimir() : void
15}
16Pantalla ..> Configuracion : getInstance()
17Impresora ..> Configuracion : getInstance()
18@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
El Singleton clásico
Inicialización perezosa: el objeto se crea la primera vez que alguien lo pide. Es correcto con un solo hilo; con varios hilos, dos podrían ver instancia == null a la vez y crear dos objetos.
1import java.util.*;
2
3/** Solo puede haber una configuración en toda la aplicación. */
4class Configuracion {
5 private static Configuracion instancia; // la única instancia, compartida
6 private final Map<String, String> valores = new HashMap<>();
7
8 private Configuracion() { // privado: nadie más puede hacer new
9 valores.put("idioma", "es");
10 System.out.println("(se crea la configuración)");
11 }
12
13 /** Punto de acceso global: la crea la primera vez que se pide (inicialización perezosa). */
14 public static Configuracion getInstance() {
15 if (instancia == null) instancia = new Configuracion();
16 return instancia;
17 }
18
19 public String get(String clave) { return valores.get(clave); }
20 public void set(String clave, String valor) { valores.put(clave, valor); }
21}
22
23public class Main {
24 public static void main(String[] args) {
25 Configuracion a = Configuracion.getInstance();
26 Configuracion b = Configuracion.getInstance();
27 a.set("idioma", "en");
28 System.out.println("¿a y b son el mismo objeto? " + (a == b ? "sí" : "no"));
29 System.out.println("Idioma visto desde b: " + b.get("idioma"));
30 }
31}class Configuracion:
"""Solo puede haber una configuración en toda la aplicación."""
_instancia = None # la única instancia, compartida
def __init__(self):
# Python no tiene constructores privados: se impide crear una segunda a mano
if Configuracion._instancia is not None:
raise RuntimeError("Usa Configuracion.get_instance()")
self._valores = {"idioma": "es"}
print("(se crea la configuración)")
@classmethod
def get_instance(cls):
"""Punto de acceso global: la crea la primera vez que se pide (inicialización perezosa)."""
if cls._instancia is None:
cls._instancia = cls()
return cls._instancia
def get(self, clave):
return self._valores.get(clave)
def set(self, clave, valor):
self._valores[clave] = valor
a = Configuracion.get_instance()
b = Configuracion.get_instance()
a.set("idioma", "en")
print("¿a y b son el mismo objeto?", "sí" if a is b else "no")
print("Idioma visto desde b:", b.get("idioma"))/** Solo puede haber una configuración en toda la aplicación. */
class Configuracion {
static #instancia = null; // la única instancia, compartida
#valores = new Map([["idioma", "es"]]);
constructor() {
// JavaScript no tiene constructores privados: se impide crear una segunda a mano
if (Configuracion.#instancia) throw new Error("Usa Configuracion.getInstance()");
console.log("(se crea la configuración)");
}
/** Punto de acceso global: la crea la primera vez que se pide (inicialización perezosa). */
static getInstance() {
if (Configuracion.#instancia === null) Configuracion.#instancia = new Configuracion();
return Configuracion.#instancia;
}
get(clave) { return this.#valores.get(clave); }
set(clave, valor) { this.#valores.set(clave, valor); }
}
const a = Configuracion.getInstance();
const b = Configuracion.getInstance();
a.set("idioma", "en");
console.log("¿a y b son el mismo objeto? " + (a === b ? "sí" : "no"));
console.log("Idioma visto desde b: " + b.get("idioma"));using System;
using System.Collections.Generic;
// Solo puede haber una configuración en toda la aplicación.
class Configuracion {
private static Configuracion? instancia; // la única instancia, compartida
private readonly Dictionary<string, string> valores = new();
private Configuracion() { // privado: nadie más puede hacer new
valores["idioma"] = "es";
Console.WriteLine("(se crea la configuración)");
}
// Punto de acceso global: la crea la primera vez que se pide (inicialización perezosa).
public static Configuracion GetInstance() {
instancia ??= new Configuracion();
return instancia;
}
public string Get(string clave) => valores[clave];
public void Set(string clave, string valor) => valores[clave] = valor;
}
class Program {
static void Main() {
Configuracion a = Configuracion.GetInstance();
Configuracion b = Configuracion.GetInstance();
a.Set("idioma", "en");
Console.WriteLine("¿a y b son el mismo objeto? " + (ReferenceEquals(a, b) ? "sí" : "no"));
Console.WriteLine("Idioma visto desde b: " + b.Get("idioma"));
}
}<?php
/** Solo puede haber una configuración en toda la aplicación. */
class Configuracion {
private static ?Configuracion $instancia = null; // la única instancia, compartida
private array $valores = ["idioma" => "es"];
private function __construct() { // privado: nadie más puede hacer new
echo "(se crea la configuración)\n";
}
/** Punto de acceso global: la crea la primera vez que se pide (inicialización perezosa). */
public static function getInstance(): Configuracion {
if (self::$instancia === null) self::$instancia = new Configuracion();
return self::$instancia;
}
public function get(string $clave): ?string { return $this->valores[$clave] ?? null; }
public function set(string $clave, string $valor): void { $this->valores[$clave] = $valor; }
}
$a = Configuracion::getInstance();
$b = Configuracion::getInstance();
$a->set("idioma", "en");
echo "¿a y b son el mismo objeto? " . ($a === $b ? "sí" : "no") . "\n";
echo "Idioma visto desde b: " . $b->get("idioma") . "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
(se crea la configuración) ¿a y b son el mismo objeto? sí Idioma visto desde b: en
Seguro con hilos: el «holder»
La forma recomendada para inicialización perezosa con hilos: la JVM garantiza que una clase se inicializa una sola vez, así que no hace falta synchronized.
1/** Perezosa y segura con hilos sin sincronizar: la JVM carga la clase interna la primera vez que se usa. */
2public class Configuracion {
3 private Configuracion() { }
4
5 private static class Holder {
6 static final Configuracion INSTANCIA = new Configuracion();
7 }
8
9 public static Configuracion getInstance() {
10 return Holder.INSTANCIA;
11 }
12}# configuracion.py
# Un módulo de Python solo se ejecuta la primera vez que se importa (y la importación está
# protegida por un cerrojo): una variable del módulo ya es una instancia única y segura con hilos.
class _Configuracion:
def __init__(self):
self.idioma = "es"
configuracion = _Configuracion()
# En cualquier otro módulo:
# from configuracion import configuracion// configuracion.js
// Un módulo ES se evalúa una sola vez, y cada hilo (worker) tiene su propia memoria:
// el objeto exportado ya es único y no hay carreras que evitar.
class Configuracion {
idioma = "es";
}
export const configuracion = new Configuracion();
// En cualquier otro módulo:
// import { configuracion } from "./configuracion.js";// Perezosa y segura con hilos: Lazy<T> crea la instancia una sola vez aunque la pidan
// varios hilos a la vez, y solo cuando alguien la usa por primera vez.
public sealed class Configuracion {
private static readonly Lazy<Configuracion> instancia = new(() => new Configuracion());
private Configuracion() { }
public static Configuracion Instancia => instancia.Value;
}<?php
// En PHP cada petición tiene su propia memoria y un solo hilo: nadie compite por crear la
// instancia, así que basta la propiedad estática (y se impide también clonarla).
final class Configuracion {
private static ?Configuracion $instancia = null;
private function __construct() { }
private function __clone() { }
public static function getInstance(): self {
return self::$instancia ??= new self();
}
}Con un enum
Joshua Bloch (Effective Java) lo considera la mejor forma: es corta y ni la serialización ni la reflexión pueden crear una segunda instancia.
1/** La forma más corta y robusta: la JVM garantiza una sola instancia, también frente a la serialización y la reflexión. */
2public enum Configuracion {
3 INSTANCIA;
4
5 private String idioma = "es";
6 public String getIdioma() { return idioma; }
7 public void setIdioma(String idioma) { this.idioma = idioma; }
8}
9
10// Uso: Configuracion.INSTANCIA.setIdioma("en");from enum import Enum
class Configuracion(Enum):
"""Los miembros de un Enum son únicos: no se puede crear otro, ni copiándolo."""
INSTANCIA = "instancia"
def __init__(self, _):
self.idioma = "es"
# Uso: Configuracion.INSTANCIA.idioma = "en"// En JavaScript un objeto literal ya es único: no hay ninguna clase de la que crear otro.
const Configuracion = Object.seal({ // seal: no se le pueden añadir ni quitar propiedades
idioma: "es",
getIdioma() { return this.idioma; },
setIdioma(idioma) { this.idioma = idioma; },
});
// Uso: Configuracion.setIdioma("en");// En C# lo más corto es una clase static: no se puede instanciar ni heredar, y el runtime
// la inicializa una sola vez, de forma segura con hilos.
public static class Configuracion {
public static string Idioma { get; set; } = "es";
}
// Uso: Configuracion.Idioma = "en";<?php
// Los enum de PHP 8.1 no pueden guardar estado: para algo único y con datos se usa una
// clase final que no se puede instanciar, con todo estático.
final class Configuracion {
private static string $idioma = "es";
private function __construct() { }
public static function getIdioma(): string { return self::$idioma; }
public static function setIdioma(string $idioma): void { self::$idioma = $idioma; }
}
// Uso: Configuracion::setIdioma("en");En la práctica
Runtime.getRuntime()devuelve el único objetoRuntimede la JVM;Desktop.getDesktop(), el escritorio del sistema.- En Spring, los beans son únicos por defecto (ámbito singleton), pero los crea y los inyecta el contenedor: no tienen
getInstance(). - En Android, la base de datos de Room o el cliente de Retrofit se suelen crear una sola vez para toda la aplicación (con un Singleton o con Hilt).
Logger.getLogger(nombre)devuelve siempre el mismo registro para el mismo nombre: un Singleton por nombre (un multiton).
Errores típicos
- Dejar el constructor público: el patrón no impide nada y cualquiera puede crear otra instancia.
- La inicialización perezosa sin protección con varios hilos: dos hilos pueden crear dos instancias. Usa el holder, un enum o
synchronized. - Usarlo como cajón de variables globales: todo el código acaba dependiendo de él y no hay forma de probar una clase por separado.
- Olvidar que la serialización y la reflexión pueden crear otra instancia (salvo con un enum).
- Guardar en él estado mutable compartido sin pensar en la concurrencia.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Un registro único para toda la aplicación
La tienda tiene un registro de mensajes (Registro) que usan el carrito y el pago, cada uno pidiéndolo con Registro.getInstance(). Ahora mismo cada llamada crea un registro nuevo, así que los mensajes se pierden. Conviértelo en un Singleton: constructor privado, la instancia guardada en un atributo estático y getInstance() que la crea solo la primera vez. No cambies el resto del programa (el contador creadas sirve para comprobarlo).
- Órdenes:
añadir producto,pagar importeyver(escribe todas las líneas del registro, numeradas). - Al final, el programa comprueba tu Singleton:
Instancias creadas: 1,Constructor privado: síySiempre la misma instancia: sí.
Ejemplo
añadir teclado añadir ratón pagar 45 ver
[1] carrito: añadido teclado (1 en total) [2] carrito: añadido ratón (2 en total) [3] pago: cobrados 45 € por 2 productos Instancias creadas: 1 Constructor privado: sí Siempre la misma instancia: sí
Ver la solución explicada
1import java.lang.reflect.Modifier;
2import java.util.*;
3
4/** El registro de la aplicación: uno solo, compartido por todas las clases. */
5class Registro {
6 static int creadas = 0; // para comprobar cuántas instancias se han creado
7 private static Registro instancia;
8 private final List<String> lineas = new ArrayList<>();
9
10 private Registro() {
11 creadas++;
12 }
13
14 static Registro getInstance() {
15 if (instancia == null) instancia = new Registro();
16 return instancia;
17 }
18
19 void escribir(String origen, String mensaje) {
20 lineas.add("[" + (lineas.size() + 1) + "] " + origen + ": " + mensaje);
21 }
22
23 List<String> lineas() {
24 return lineas;
25 }
26}
27
28class Carrito {
29 private final List<String> productos = new ArrayList<>();
30
31 void anadir(String producto) {
32 productos.add(producto);
33 Registro.getInstance().escribir("carrito", "añadido " + producto + " (" + productos.size() + " en total)");
34 }
35
36 int cuantos() { return productos.size(); }
37}
38
39class Pago {
40 void pagar(Carrito c, String importe) {
41 if (c.cuantos() == 0) Registro.getInstance().escribir("pago", "rechazado: el carrito está vacío");
42 else Registro.getInstance().escribir("pago", "cobrados " + importe + " € por " + c.cuantos() + (c.cuantos() == 1 ? " producto" : " productos"));
43 }
44}
45
46public class Main {
47 public static void main(String[] args) throws Exception {
48 Scanner sc = new Scanner(System.in);
49 Carrito carrito = new Carrito();
50 Pago pago = new Pago();
51 while (sc.hasNextLine()) {
52 String[] p = sc.nextLine().trim().split("\\s+", 2);
53 switch (p[0]) {
54 case "añadir" -> carrito.anadir(p.length > 1 ? p[1] : "?");
55 case "pagar" -> pago.pagar(carrito, p.length > 1 ? p[1] : "0");
56 case "ver" -> Registro.getInstance().lineas().forEach(System.out::println);
57 case "" -> { }
58 default -> Registro.getInstance().escribir("main", "orden desconocida " + p[0]);
59 }
60 }
61 System.out.println("Instancias creadas: " + Registro.creadas);
62 System.out.println("Constructor privado: " + (Modifier.isPrivate(Registro.class.getDeclaredConstructor().getModifiers()) ? "sí" : "no"));
63 System.out.println("Siempre la misma instancia: " + (Registro.getInstance() == Registro.getInstance() ? "sí" : "no"));
64 }
65}Con el constructor privado y el atributo estático, la única forma de obtener un Registro es getInstance(), que siempre devuelve el mismo objeto: el carrito y el pago escriben en la misma lista.
La comprobación final usa reflexión (getDeclaredConstructor().getModifiers()), la misma herramienta con la que, si no se protege, se podría romper el Singleton llamando al constructor privado.
2. Un pool de conexiones compartido
Dos servicios de la aplicación (alumnos y notas) piden conexiones a la base de datos a un pool con 3 conexiones como máximo. Si cada servicio tuviera su propio pool, habría 6. Haz que PoolConexiones sea un Singleton seguro con hilos (usa la inicialización bajo demanda con una clase interna) y programa el préstamo: se da la conexión libre de número más bajo; si no hay, el que la pide pasa a una cola; al devolver una, la recibe el primero de la cola.
- Órdenes:
alumnos pedir nombre,notas devolver nombre… yestado. - Respuestas:
ana obtiene la conexión 1,ana ya tiene la conexión 1,no quedan conexiones: eva espera (1 en cola),eva ya está esperando,ana devuelve la conexión 1(con; eva la recibesi alguien esperaba),luis no tenía ninguna conexión. estado:En uso: ana=1, luis=2 · esperando: eva(ninguna,nadie). Al final:Mismo pool en los dos servicios: síyPools creados: 1.
Ejemplo
alumnos pedir ana notas pedir luis alumnos pedir eva notas pedir marta estado alumnos devolver luis estado
ana obtiene la conexión 1 luis obtiene la conexión 2 eva obtiene la conexión 3 no quedan conexiones: marta espera (1 en cola) En uso: ana=1, luis=2, eva=3 · esperando: marta luis devuelve la conexión 2; marta la recibe En uso: ana=1, eva=3, marta=2 · esperando: nadie Mismo pool en los dos servicios: sí Pools creados: 1
Ver la solución explicada
1import java.util.*;
2
3/** Un único pool para toda la aplicación, con un número fijo de conexiones. */
4class PoolConexiones {
5 static final int CAPACIDAD = 3;
6 static int creados = 0;
7 private final Map<String, Integer> enUso = new LinkedHashMap<>(); // quién tiene cada conexión
8 private final Deque<String> esperando = new ArrayDeque<>();
9
10 private PoolConexiones() {
11 creados++;
12 }
13
14 // Inicialización bajo demanda con una clase interna: perezosa y segura con hilos
15 private static class Holder {
16 static final PoolConexiones INSTANCIA = new PoolConexiones();
17 }
18
19 static PoolConexiones getInstance() {
20 return Holder.INSTANCIA;
21 }
22
23 private int libre() {
24 for (int n = 1; n <= CAPACIDAD; n++) if (!enUso.containsValue(n)) return n;
25 return 0;
26 }
27
28 String pedir(String quien) {
29 if (enUso.containsKey(quien)) return quien + " ya tiene la conexión " + enUso.get(quien);
30 if (esperando.contains(quien)) return quien + " ya está esperando";
31 int n = libre();
32 if (n == 0) {
33 esperando.add(quien);
34 return "no quedan conexiones: " + quien + " espera (" + esperando.size() + " en cola)";
35 }
36 enUso.put(quien, n);
37 return quien + " obtiene la conexión " + n;
38 }
39
40 String devolver(String quien) {
41 Integer n = enUso.remove(quien);
42 if (n == null) return quien + " no tenía ninguna conexión";
43 String r = quien + " devuelve la conexión " + n;
44 if (!esperando.isEmpty()) {
45 String siguiente = esperando.poll();
46 enUso.put(siguiente, n);
47 r += "; " + siguiente + " la recibe";
48 }
49 return r;
50 }
51
52 String estado() {
53 List<String> partes = new ArrayList<>();
54 enUso.forEach((q, n) -> partes.add(q + "=" + n));
55 return "En uso: " + (partes.isEmpty() ? "ninguna" : String.join(", ", partes)) + " · esperando: " + (esperando.isEmpty() ? "nadie" : String.join(", ", esperando));
56 }
57}
58
59/** Dos servicios distintos que deben compartir el mismo pool. */
60class ServicioAlumnos {
61 final PoolConexiones pool = PoolConexiones.getInstance();
62}
63
64class ServicioNotas {
65 final PoolConexiones pool = PoolConexiones.getInstance();
66}
67
68public class Main {
69 public static void main(String[] args) {
70 Scanner sc = new Scanner(System.in);
71 ServicioAlumnos alumnos = new ServicioAlumnos();
72 ServicioNotas notas = new ServicioNotas();
73 while (sc.hasNextLine()) {
74 String[] p = sc.nextLine().trim().split("\\s+");
75 if (p.length == 1 && p[0].equals("estado")) {
76 System.out.println(PoolConexiones.getInstance().estado());
77 continue;
78 }
79 if (p.length != 3 || !(p[0].equals("alumnos") || p[0].equals("notas")) || !(p[1].equals("pedir") || p[1].equals("devolver"))) {
80 if (!p[0].isEmpty()) System.out.println("Orden no válida: " + String.join(" ", p));
81 continue;
82 }
83 PoolConexiones pool = p[0].equals("alumnos") ? alumnos.pool : notas.pool;
84 System.out.println(p[1].equals("pedir") ? pool.pedir(p[2]) : pool.devolver(p[2]));
85 }
86 System.out.println("Mismo pool en los dos servicios: " + (alumnos.pool == notas.pool ? "sí" : "no"));
87 System.out.println("Pools creados: " + PoolConexiones.creados);
88 }
89}El pool es un caso de manual para el Singleton: un recurso limitado del que debe haber una sola copia. Los dos servicios lo piden en su constructor y reciben el mismo objeto, así que el límite de 3 se respeta entre todos.
El holder es perezoso (el pool se crea al primer getInstance()) y seguro con hilos sin synchronized, porque la JVM inicializa cada clase una sola vez. En un servidor real, pedir y devolver también tendrían que estar sincronizados.
Test
Test: Singleton
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Qué impide que se cree una segunda instancia de un Singleton con
new?2.¿Qué problema tiene el Singleton perezoso clásico (
if (instancia == null) instancia = new …) con varios hilos?3.¿Por qué se critica el Singleton?
4.¿Qué forma de Singleton resiste también a la serialización y a la reflexión?
5.En Spring, los beans son singleton por defecto. ¿Quién garantiza que haya uno solo?