jueves, 3 de julio de 2008

"Double checked locking" por fin funciona en Java

[Actualización noviembre 2008: referencia al final del post a un nuevo artículo de Jeremy "mascando" las variables volátiles en Java]

Es de todos conocido (espero) el pernicioso o impredecible efecto en Java del uso de la optimización "double-checked locking" tan usada en otros lenguajes como C++. El problema básicamente se da por dos motivos: en entornos multi-core / multi-procesador la JVM puede decidir que los threads utilicen copias locales de los punteros e incluso de algunas variables pequeñas para mejorar el rendimiento de acceso (las copias locales de la variable que tiene cada thread pueden no darse por enteradas del cambio global en condiciones de stress), aparte de que el compilador puede cambiar el orden de ejecución de algunas sentencias... :-(

Pero, desde J2SE 1.5, existe solución al problema añadiendo la palabra clave "volatile" como modificador de la variable que se utiliza como monitor, lo que asegura que todos los threads vean la misma versión de dicha variable, sin usar copias locales. Se penaliza muy levemente el rendimiento de acceso a esas variables dependiendo de la implementación de la JVM (pero mucha menos penalización que una región de exclusión mutua). Ahí está, el double-checked locking por fin funcionando en Java:



// Works with acquire/release semantics for volatile
// Broken under Java 1.4 and earlier semantics for volatile
class Foo {
  private volatile Helper helper = null;
  public Helper getHelper() {
    if (helper == null) {
      synchronized(this) {
        if (helper == null)
          helper = new Helper();
      }
    }
    return helper;
  }

  // other functions and members...
}

En estos tres artículos está perfectamente explicado así que no añadiré mucho más:

P.D: En este artículo se explica perfectamente el modificador volatile y su comparación con synchronized: http://www.javaperformancetuning.com/news/qotm030.shtml

(...) volatile only synchronizes the value of one variable between thread memory and "main" memory, whilesynchronized synchronizes the value of all variables between thread memory and "main" memory, and locks and releases a monitor to boot. Clearly synchronized is likely to have more overhead than volatile.

Y hago referencia a un nuevo artículo divulgativo de Jeremy Manson explicando muy mascadito qué implica el keyword volatile en Java: http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html



ACTUALIZACIÓN: aunque este patrón sigue teniendo sentido en muchos escenarios, para implementar un patrón Singleton con lazy-initialization es mejor usar el idioma Initialization-on-demand holder. Para más info, véase el artículo