podría ser un poco tarde al juego en esto, pero una implementación básica sería algo como esto:
public class MySingleton {
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
...
}
Aquí tenemos la clase MySingleton que tiene un miembro estático privada llamada INSTANCIA y un método público estático llamado getInstance(). La primera vez que se invoca getInstance(), el miembro INSTANCE es nulo. El flujo caerá entonces en la condición de creación y creará una nueva instancia de la clase MySingleton. Las llamadas posteriores a getInstance() encontrarán que la variable INSTANCE ya está configurada y, por lo tanto, no crearán otra instancia de MySingleton. Esto asegura que solo hay una instancia de MySingleton que se comparte entre todas las personas que llaman de getInstance().
Pero esta implementación tiene un problema. Las aplicaciones de subprocesos múltiples tendrán una condición de carrera en la creación de la instancia única. Si varios hilos de ejecución golpean el método getInstance() en (o alrededor) al mismo tiempo, cada uno verá al miembro INSTANCE como nulo. Esto dará como resultado que cada hilo cree una nueva instancia de MySingleton y posteriormente establezca el miembro de INSTANCE.
private static MySingleton INSTANCE;
public static synchronized MySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new MySingleton();
}
return INSTANCE;
}
Aquí hemos utilizado la palabra clave sincronizada en la firma del método para sincronizar el método getInstance(). Esto sin duda arreglará nuestra condición de carrera. Los hilos ahora se bloquearán e ingresarán el método uno a la vez. Pero también crea un problema de rendimiento. Esta implementación no solo sincroniza la creación de la instancia única, sino que sincroniza todas las llamadas a getInstance(), incluidas las lecturas. Las lecturas no necesitan sincronizarse ya que simplemente devuelven el valor de INSTANCE. Dado que las lecturas constituirán la mayor parte de nuestras llamadas (recuerde, la creación de instancias solo ocurre en la primera llamada), incurriremos en un golpe de rendimiento innecesario al sincronizar todo el método.
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronize(MySingleton.class) {
INSTANCE = new MySingleton();
}
}
return INSTANCE;
}
Aquí hemos movido la sincronización desde la firma del método, a un bloque sincronizado que envuelve la creación de la instancia MySingleton. Pero, ¿resuelve esto nuestro problema? Bueno, ya no estamos bloqueando las lecturas, pero también hemos dado un paso atrás. Múltiples hilos golpearán el método getInstance() en o cerca del mismo tiempo y todos verán al miembro INSTANCE como nulo. Luego golpearán el bloque sincronizado donde uno obtendrá el bloqueo y creará la instancia. Cuando ese hilo sale del bloque, los otros hilos contendrán por el bloqueo, y uno a uno cada hilo caerá a través del bloque y creará una nueva instancia de nuestra clase. Así que estamos de vuelta donde comenzamos.
private static MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
Aquí emitir otro cheque desde el interior del bloque. Si el miembro INSTANCE ya se ha configurado, omitiremos la inicialización. Esto se llama bloqueo con doble verificación.
Esto resuelve nuestro problema de creación de instancias múltiples. Pero una vez más, nuestra solución ha presentado otro desafío. Otros hilos podrían no "ver" que el miembro INSTANCE se haya actualizado. Esto se debe a la forma en que Java optimiza las operaciones de memoria. Los hilos copian los valores originales de las variables de la memoria principal en la memoria caché de la CPU. Los cambios en los valores se escriben y se leen desde ese caché. Esta es una característica de Java diseñada para optimizar el rendimiento. Pero esto crea un problema para nuestra implementación singleton. Un segundo subproceso - procesado por una CPU o núcleo diferente, que utiliza una memoria caché diferente - no verá los cambios realizados por el primero. Esto hará que el segundo subproceso vea al miembro INSTANCE como nulo forzando una nueva instancia de nuestro singleton para ser creado.
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
if (INSTANCE == null) {
synchronized(MySingleton.class) {
if (INSTANCE == null) {
INSTANCE = createInstance();
}
}
}
return INSTANCE;
}
resolvemos esto usando la palabra clave volátiles en la declaración del miembro de instancia. Esto le indicará al compilador que siempre lea y escriba en la memoria principal, y no en la memoria caché de la CPU.
Pero este cambio simple tiene un costo. Debido a que estamos pasando por alto el caché de la CPU, tomaremos un golpe de rendimiento cada vez que operamos en el miembro INSTANCE volátil, lo cual hacemos 4 veces. Comprobamos la existencia (1 y 2), establecemos el valor (3) y luego devolvemos el valor (4). Se podría argumentar que este camino es el caso marginal, ya que solo creamos la instancia durante la primera llamada del método. Quizás un golpe de rendimiento en la creación sea tolerable. Pero incluso nuestro caso de uso principal, dice, operará en el miembro volátil dos veces. Una vez para verificar la existencia, y nuevamente para devolver su valor.
private static volatile MySingleton INSTANCE;
public static MySingleton getInstance() {
MySingleton result = INSTANCE;
if (result == null) {
synchronized(MySingleton.class) {
result = INSTANCE;
if (result == null) {
INSTANCE = result = createInstance();
}
}
}
return result;
}
Dado que el impacto en el rendimiento se debe a que opera directamente sobre el elemento volátil, vamos a establecer una variable local con el valor de la volátil y operan sobre la variable local. Esto disminuirá la cantidad de veces que operamos en el volátil, recuperando parte de nuestro rendimiento perdido. Tenga en cuenta que tenemos que establecer nuestra variable local nuevamente cuando ingresemos al bloque sincronizado. Esto garantiza que esté actualizado con los cambios que ocurrieron mientras esperábamos el bloqueo.
Escribí un artículo sobre esto recientemente. Deconstructing The Singleton. Puede encontrar más información sobre estos ejemplos y un ejemplo del patrón "titular" allí. También hay un ejemplo del mundo real que muestra el enfoque volátil doblemente verificado. Espero que esto ayude.
Esto es equivalente a 'static Myclass myclass = new MyClass()' – pjp
mejor pregunta, ¿Cuántas implementaciones rotas/con errores del patrón singleton podemos recopilar en un hilo? – james