7

Considere francotirador código de abajo:es la sincronización necesaria durante la lectura si hay contención podría ocurrir

package sync; 

public class LockQuestion { 
    private String mutable; 

    public synchronized void setMutable(String mutable) { 
     this.mutable = mutable; 
    } 

    public String getMutable() { 
     return mutable; 
    } 
} 

A la hora Time1 hilo Thread1 actualizará la variable ‘mutable’. Se necesita sincronización en setter para vaciar la memoria de la memoria caché local a la memoria principal. En el momento Time2 (Time2> Time1, no hay contención de hilos) thread Thread2 leerá el valor de mutable.

La pregunta es: ¿tengo que poner sincronizado antes de getter? Parece que esto no causará ningún problema: la memoria debe estar actualizada y la memoria caché local de Thread2 debe estar invalidada por Thread1, &, pero no estoy seguro.

+6

Enlazar en Java se define en términos de relaciones * happens-before *. No trates de pensar en términos de caches de enjuague, porque estarás equivocado. Por un lado, las optimizaciones del compilador son importantes. Incluso si el caché de lavado era un modelo preciso, si el objeto referido es mutable, el orden de las actualizaciones no estaría garantizado. (Hasta la especificación 1.5, la especificación sí usaba un modelo de cavidades de enjuague, pero no se podía implementar). –

Respuesta

4

En lugar de preguntarse, ¿por qué no utilizar las referencias atómicas en java.util.concurrent?

(y por lo que vale la pena, mi lectura de happens-before no garantiza que Thread2 vea cambios en mutables a menos que también use sincronización ... pero siempre me da dolor de cabeza esa parte de JLS, así que use el atómico referencias)

+0

Aunque las clases Atómicas son muy útiles, inevitablemente tienen una pequeña penalización en el rendimiento. Prefiero usar tipos 'simples' a menos que se requiera una atomicidad de actualización. En el caso anterior solo se requiere visibilidad de la actualización. –

+0

@Petro Semeniuk Sin uno de los objetos atómicos, sincronización y/o volátil, no hay forma de garantizar la "visibilidad" de la actualización y la lectura en relación con el otro. Esto puede o no importar. –

+0

Tenga en cuenta que el 'synchronized' /' volátil' necesita estar en el mismo objeto. –

1

¿Está absolutamente seguro de que se llamará al captador solo después de que se haya llamado al colocador? Si es así, no necesita que el getter esté sincronizado, ya que las lecturas concurrentes no necesitan sincronizarse.

Si existe la posibilidad de que se puedan invocar get y set al mismo tiempo, definitivamente debe sincronizar los dos.

+1

Esto no es verdad. Sin un bloqueo sincronizado en el captador, no ocurre nada, antes de la garantía. El colocador puede disparar, terminar y luego el eliminador dispara en un hilo diferente - * pero * lee el valor anterior. Esto puede o no importar. Para un acceso único, 'volátil 'sería" equivalente "aquí, pero se pierde en las implicaciones de sincronización más grandes. –

+0

OP escribió que se llama al setter en Time1, getter se llama en Time2 y Time2> Time1, por lo que es posible que el thread 2 solo se inicie después de que complete el thread 1 que da una garantía de "sucede antes" (por ejemplo, thread1. Start(), thread1.Join(), thread2.Start()) – SpeksETC

+2

No veo nada relacionado con los hilos que se detienen o unen. T2> T1 no es suficiente para happen-before - el almacenamiento en caché arroja esta suposición por la ventana. –

4

Será bien si haces detalles mutables volátil, en la "cheap read-write lock"

+0

Gracias. Mis pensamientos sobre volátiles son similares a los de las clases Atomic *: son baratos pero no son gratuitos. Idealmente quiero forzar 'sucede antes' pero sin penalización/bloqueo en las lecturas. –

+0

@Petro Semeniuk No se trata de ser "libre", se trata de "tener siempre la semántica deseada" (por ejemplo, no dejar que ingrese una extraña falla de sincronización). Sin embargo, * su programa * puede funcionar bien de esa manera sin un "bloqueo costoso" (que, enfrentándolo, es muy barato a menos que sea muy disputado), pero eso es solo para analizar en una imagen más amplia. Mejor simplemente hacer "lo correcto" y no preocuparse por tontas micro optimizaciones. –

+0

Aunque en la práctica 'volátil' rara vez es útil. –

0

Esto probablemente nunca dará lugar a un comportamiento incorrecto, pero a menos que también se garantiza el orden en que los hilos de inicio en, no se puede necesariamente garantía que el compilador no reordenó la lectura en Thread2 antes de la escritura en Thread1. Más específicamente, the entire Java runtime only has to guarantee that threads execute as if they were run in serial. Por lo tanto, siempre que el hilo tenga el mismo resultado ejecutándose en serie bajo optimizaciones, toda la pila de idiomas (compilador, hardware, tiempo de ejecución del lenguaje) puede hacer prácticamente todo lo que quiera. Incluyendo permitir que Thread2 guarde en caché el resultado de LockQuestion.getMutable().

En la práctica, me sorprendería mucho si eso ocurriera. Si desea garantizar que esto no ocurra, haga que LockQuestion.mutable se declare como final y se inicialice en el constructor. Or use the following idiom:

private static class LazySomethingHolder { 
    public static Something something = new Something(); 
} 

public static Something getInstance() { 
    return LazySomethingHolder.something; 
} 
1

Si usted se preocupa tanto por el rendimiento en el hilo de la lectura, a continuación, lo que se hace es leer el valor de una vez usando una correcta sincronización o referencias volátiles o atómicas. Luego asigna el valor a una vieja variable simple.

La asignación a la variable plain se garantiza después de la lectura atómica (porque ¿de qué otro modo podría obtener el valor?) Y si el valor nunca volverá a escribirse en otra cadena, ya está todo configurado.

1

Creo que deberías comenzar con algo que sea correcto y optimizar luego cuando sabes que tienes un problema. Solo usaría AtomicReference a menos que unos nano segundos sea demasiado largo.;)

public static void main(String... args) { 
    AtomicReference<String> ars = new AtomicReference<String>(); 
    ars.set("hello"); 
    long start = System.nanoTime(); 
    int runs = 1000* 1000 * 1000; 
    int length = test(ars, runs); 
    long time = System.nanoTime() - start; 
    System.out.printf("get() costs " + 1000*time/runs + " ps."); 
} 

private static int test(AtomicReference<String> ars, int runs) { 
    int len = 0; 
    for (int i = 0; i < runs; i++) 
     len = ars.get().length(); 
    return len; 
} 

Prints

get() costs 1219 ps. 

ps es un pico-segundos, con es 1 millonésima parte de un micro-segundo.

+0

Gracias por proporcionar las métricas de perf. Por cierto, si haces lo mismo con String simple, verás que el acceso constante a AtomicReference duplica el costo de recuperación. Probablemente la manera más eficiente de hacerlo es asignarle valor a la variable local (método de prueba interno) –

+0

El resultado es solo una lectura volátil, declararlo volátil tendrá el mismo efecto. –

Cuestiones relacionadas