Supongamos que tengo un campo al que se accede simultáneamente y se lee muchas veces y rara vez se escribe.
public Object myRef = new Object();
Digamos que un subproceso T1 establecerá myRef en otro valor, una vez por minuto, mientras que otros subprocesos leerán myRef miles de millones de veces de forma continua y simultánea. Solo necesito que myRef sea finalmente visible para todos los hilos.
Una solución simple sería usar una referencia atómica o simplemente volátil como este:
public volatile Object myRef = new Object();
Sin embargo, las lecturas volátiles afaik incurren en un costo de rendimiento. Sé que es minúsculo, esto se parece más a algo que me pregunto que a algo que realmente necesito. Así que no nos preocupemos por el rendimiento y asumamos que esta es una pregunta puramente teórica.
Entonces, la pregunta se reduce a: ¿Hay alguna forma de evitar con seguridad las lecturas volátiles para referencias que rara vez se escriben, haciendo algo en el sitio de escritura?
Después de leer un poco, parece que las barreras de memoria podrían ser lo que necesito. Entonces, si existiera una construcción como esta, mi problema se resolvería:
- Escribir
- Invocar barrera (sincronización)
- Todo está sincronizado y todos los hilos verán el nuevo valor. (sin un costo permanente en los sitios de lectura, puede ser obsoleto o incurrir en un costo único a medida que se sincronizan los cachés, pero después de eso todo vuelve al campo normal hasta la próxima escritura).
¿Existe tal construcción en Java, o en general? En este punto no puedo evitar pensar que si existiera algo así, ya habría sido incorporado a los paquetes atómicos por las personas mucho más inteligentes que los mantienen. (¿La lectura vs escritura desproporcionadamente frecuente podría no haber sido un caso para cuidar?) Entonces, ¿tal vez hay algo mal en mi pensamiento y tal construcción no es posible en absoluto?
He visto que algunos ejemplos de código usan 'volátil' para un propósito similar, explotando su contrato anterior. Hay un campo de sincronización separado, por ejemplo:
public Object myRef = new Object();
public volatile int sync = 0;
y al escribir hilo / sitio:
myRef = new Object();
sync += 1 //volatile write to emulate barrier
No estoy seguro de que esto funcione, y algunos argumentan que esto funciona solo en la arquitectura x86. Después de leer secciones relacionadas en JMS, creo que solo está garantizado que funcione si esa escritura volátil se combina con una lectura volátil de los hilos que necesitan ver el nuevo valor de myRef. (Entonces no se deshace de la lectura volátil).
Volviendo a mi pregunta original; ¿Es esto posible? ¿Es posible en Java? ¿Es posible en una de las nuevas API en Java 9 VarHandles?
fuente
sync += 1;
y los hilos de su lector leen elsync
valor, también verán lamyRef
actualización. Debido a que solo necesita que los lectores vean la actualización eventualmente , podría usar esto para su ventaja para leer solo la sincronización en cada 1000 iteración del hilo del lector, o algo similar. Pero también puede hacer un truco similarvolatile
: simplemente guardemyRef
en caché el campo en los lectores durante 1000 iteraciones, luegosync
campo, no, los lectores no tocarían elsync
campo en cada iteración, lo harían de manera oportunista, cuando quieran verificar si ha habido una actualización. Dicho esto, una solución más simple sería almacenar en cachémyRef
durante 1000 rondas, luego volver a leerlo ...Respuestas:
Entonces, básicamente, desea la semántica de un
volatile
sin el costo de tiempo de ejecución.No creo que sea posible.
El problema es que el costo de tiempo de ejecución de
volatile
se debe a las instrucciones que implementan las barreras de memoria en el escritor y el código del lector. Si "optimiza" al lector al deshacerse de su barrera de memoria, ya no tiene la garantía de que el lector verá el nuevo valor "rara vez escrito" cuando realmente se escriba.FWIW, algunas versiones de la
sun.misc.Unsafe
clase proporcionan métodos y explícitosloadFence
, pero no creo que su uso proporcione ningún beneficio de rendimiento sobre el uso de a .storeFence
fullFence
volatile
Hipotéticamente ...
lo que quiere es que un procesador en un sistema multiprocesador pueda decirle a todos los otros procesadores:
Desafortunadamente, los ISA modernos no son compatibles con esto.
En la práctica, cada procesador controla su propio caché.
fuente
No estoy muy seguro de si esto es correcto, pero podría resolverlo usando una cola.
Cree una clase que envuelva un atributo ArrayBlockingQueue. La clase tiene un método de actualización y un método de lectura. El método de actualización publica el nuevo valor en la cola y elimina todos los valores, excepto el último valor. El método de lectura devuelve el resultado de una operación de espiar en la cola, es decir, leer pero no eliminar. Los hilos que miran el elemento en la parte delantera de la cola lo hacen sin obstáculos. Los hilos que actualizan la cola lo hacen limpiamente.
fuente
ReentrantReadWriteLock
que está diseñado para pocas escrituras, escenario de muchas lecturas.Puede usar el
StampedLock
que está diseñado para el mismo caso de pocas escrituras y muchas lecturas, pero también las lecturas se pueden intentar de manera optimista. Ejemplo:Haga que su estado sea inmutable y permita modificaciones controladas solo clonando el objeto existente y alterando solo las propiedades necesarias a través del constructor. Una vez que se construye el nuevo estado, lo asigna a la referencia que está siendo leída por los muchos hilos de lectura. De esta manera, la lectura de hilos tiene un costo cero .
fuente
X86 proporciona TSO; obtienes vallas [LoadLoad] [LoadStore] [StoreStore] gratis.
Una lectura volátil requiere semántica de lanzamiento.
Y como puede ver, el X86 ya lo proporciona de forma gratuita.
En su caso, la mayoría de las llamadas son de lectura y la línea de caché ya estará en el caché local.
Hay un precio que pagar por las optimizaciones a nivel de compilador, pero a nivel de hardware, una lectura volátil es tan costosa como una lectura normal.
Por otro lado, la escritura volátil es más costosa porque requiere un [StoreLoad] para garantizar la coherencia secuencial (en la JVM esto se hace usando
lock addl %(rsp),0
un MFENCE). Dado que las escrituras rara vez se encuentran en su situación, este no es un problema.Tendría cuidado con las optimizaciones en este nivel porque es muy fácil hacer que el código sea más complejo de lo que realmente se necesita. Lo mejor es guiar sus esfuerzos de desarrollo por algunos puntos de referencia, por ejemplo, utilizando JMH y preferiblemente probarlo en hardware real. También podría haber otras criaturas desagradables ocultas como el intercambio falso.
fuente