2012-06-15 24 views
13

El otro día Howard Lewis Barco registró una entrada de blog llamado "Things I Learned at Hacker Bed and Breakfast", uno de los puntos de la bala es:inicialización perezosa sin sincronización o palabra clave volátil

campo de instancia de Java que se asigna exactamente una vez a través de perezoso inicialización hace no tiene que estar sincronizado o ser volátil (como largo ya que puede aceptar condiciones de carrera entre subprocesos para asignar al campo ); esto es de Rich Hickey

En vista de ello, esto parece en desacuerdo con la sabiduría aceptada sobre la visibilidad de cambios en la memoria a través de hilos, y si esto está cubierto en el Java concurrencia en el libro de práctica o en el lenguaje Java espec. entonces lo he echado de menos. Pero esto fue algo que HLS obtuvo de Rich Hickey en un evento donde Brian Goetz estuvo presente, por lo que parece que debe haber algo en ello. ¿Podría alguien explicar la lógica detrás de esta declaración?

+0

no temas las lecturas volátiles. la inicialización de clases, es decir, el código modificable es la única forma portátil de hacerlo sin volátiles. La declaración es incorrecta frente a la arquitectura de la CPU que permite reordenar las escrituras.En x86 y Sparc TSO, la lectura volátil es gratuita, por lo que no tiene sentido jugar con un hacker. – bestsss

Respuesta

9

Esta afirmación suena un poco críptica. Sin embargo, supongo que HLS se refiere al caso cuando inicializa lazmente un campo de instancia y no le importa si varios hilos realizan esta inicialización más de una vez.
A modo de ejemplo, puedo señalar que el método hashCode() de String clase:

private int hashCode; 

public int hashCode() { 
    int hash = hashCode; 
    if (hash == 0) { 
     if (count == 0) { 
      return 0; 
     } 
     final int end = count + offset; 
     final char[] chars = value; 
     for (int i = offset; i < end; ++i) { 
      hash = 31*hash + chars[i]; 
     } 
     hashCode = hash; 
    } 
    return hash; 
} 

Como se puede ver el acceso al campo hashCode (que tiene un valor en caché del hash cadena computarizada) no está sincronizado y el campo no está declarado como volatile. Cualquier hilo que llame al método hashCode() seguirá recibiendo el mismo valor, aunque el campo hashCode se puede escribir más de una vez por diferentes hilos.

Esta técnica tiene una usabilidad limitada. En mi humilde opinión, se puede usar principalmente para los casos como en el ejemplo: un objeto primitivo/inmutable almacenado en caché que se calcula a partir de los otros campos finales/inmutables, pero su cálculo en el constructor es excesivo.

5

Editar:

Hrm. Mientras leo esto, es técnicamente incorrecto, pero está bien en la práctica con algunas advertencias. Solo los campos finales se pueden inicializar de forma segura una vez y acceder a ellos en múltiples hilos sin sincronización.

Los subprocesos iniciados diferidos pueden sufrir problemas de sincronización de varias formas. Por ejemplo, puede tener condiciones de carrera de constructor donde la referencia de la clase ha sido exportada sin la clase en sí se está inicializando completamente.

Creo que depende en gran medida de si tiene o no un campo primitivo o un objeto. Los campos primitivos que se pueden inicializar varias veces en los que no te importa que varios hilos hagan la inicialización funcionarían bien. Sin embargo, la inicialización de estilo HashMap de esta manera puede ser problemática. Incluso los valores long en algunas arquitecturas pueden almacenar las diferentes palabras en varias operaciones, por lo que pueden exportar la mitad del valor, aunque sospecho que un long nunca cruzaría una página de memoria, por lo que nunca sucedería.

Creo que depende en gran medida de si es o no una aplicación tiene ningún barreras de memoria - cualquier synchronized bloques o acceso a volatile campos. El diablo está ciertamente en los detalles aquí y el código que hace la inicialización perezosa puede funcionar bien en una arquitectura con un conjunto de códigos y no en un modelo de subproceso diferente o con una aplicación que se sincroniza raramente.


He aquí una buena pieza en campos finales como comparación:

http://www.javamex.com/tutorials/synchronization_final.shtml

A partir de Java 5, un uso particular de la palabra clave final es un arma muy importante ya menudo pasado por alto en su arsenal de concurrencia. Esencialmente, el final se puede usar para asegurarse de que cuando se construye un objeto, otro subproceso que accede a ese objeto no vea ese objeto en un estado parcialmente construido, como podría ocurrir de otro modo.Esto es porque cuando se usa como un atributo en las variables de un objeto, final tiene la siguiente característica importante como parte de su definición:

Ahora, incluso si el campo está marcado como final, si se trata de una clase, puede modificar los campos dentro de la clase. Este es un problema diferente y aún debe tener sincronización para esto.

5

Esto funciona bien bajo ciertas condiciones.

  • Está bien intentar configurar el campo más de una vez.
  • está bien si los subprocesos individuales ven valores diferentes.

A menudo, cuando crea un objeto que no ha cambiado, p. cargar un Propiedades desde el disco, tener más de una copia por un corto período de tiempo no es un problema.

private static Properties prop = null; 

public static Properties getProperties() { 
    if (prop == null) { 
     prop = new Properties(); 
     try { 
      prop.load(new FileReader("my.properties")); 
     } catch (IOException e) { 
      throw new AssertionError(e); 
     } 
    } 
    return prop; 
} 

En el corto plazo, esto es menos eficiente que usar el bloqueo, pero a largo plazo podría ser más eficiente. (Aunque Properties tiene un bloqueo propio, pero se entiende la idea;)

En mi humilde opinión, no es una solución que funcione en todos los casos.

Quizás el punto es que puede usar técnicas de consistencia de memoria más relajadas en algunos casos.

+2

Sin embargo, esto adolece de los problemas de condición de carrera del constructor. Puede obtener la referencia al objeto exportado a otro subproceso sin que el objeto esté completamente inicializado. – Gray

+0

@Gray mi comprensión de la inicialización lenta es que siempre se inicializa según sea necesario. No debería ser posible ver un valor no inicializado, pero podría tratar de configurarlo más de una vez. –

+0

El problema, como yo lo veo @Peter, es que si no hay sincronización, no hay barrera de memoria, existe la posibilidad de que parte de un objeto se comparta entre cachés de memoria sin que se actualice el _objeto completo de almacenamiento. Si el subproceso A ejecutándose en otro proceso tenía su propia copia de la página n. ° 1 pero no de la n. ° 2 y un objeto que tenía almacenado en cada página inicializado por el subproceso B, el subproceso A puede obtener un objeto parcialmente inicializado. – Gray

3

Creo que la declaración no es cierta. Otro subproceso puede ver un objeto parcialmente inicializado, por lo que la referencia puede ser visible para otro subproceso aunque el constructor no haya terminado de ejecutarse. Esto se cubre en Java concurrencia en la práctica, la sección 3.5.1:

public class Holder { 

    private int n; 

    public Holder (int n) { this.n = n; } 

    public void assertSanity() { 
     if (n != n) 
      throw new AssertionError("This statement is false."); 
    } 

} 

Esta clase no es seguro para subprocesos.

Si el objeto es visible inmutable, entonces usted está bien, debido a la semántica de los campos finales significa que no verá hasta que su constructor ha terminado de ejecutarse (sección 3.5.2).

+0

+1 para referencia :-) – dacwe

+0

@dacwe Exijo mi voto popular para escribir todo eso en :-) – artbristol

Cuestiones relacionadas