interfaz - pasos para crear un componente en java
AtomicReferenceFieldUpdater: métodos para establecer, obtener, comparar y establecer semántica (3)
Como se explica en la documentación del paquete para atomics (en general, no los actualizadores específicamente):
Los efectos de memoria para accesos y actualizaciones de atómicos generalmente siguen las reglas para volátiles, [...]:
get
tiene los efectos de memoria de leer una variablevolatile
.set
tiene los efectos de memoria de escribir (asignar) una variablevolatile
.- [...]
compareAndSet
y todas las demás operaciones de lectura y actualización, comogetAndIncrement
tienen los efectos de memoria tanto de la lectura como de la escritura de variablesvolatile
.
¿Qué problema está tratando de resolver un compareAndSet
un atómico? ¿Por qué usar (por ejemplo) atomicInteger.compareAndSet(1,2)
lugar de if(volatileInt == 1) { volatileInt = 2; }
if(volatileInt == 1) { volatileInt = 2; }
? No se trata de resolver ningún problema con lecturas concurrentes, ya que éstas ya están a cargo de un volatile
regular. (Una lectura o escritura "volátil" es lo mismo que una lectura o escritura "atómica". Una lectura concurrente solo sería un problema si ocurriera en medio de una escritura, o si las declaraciones se reordenaran o se optimizaran de alguna manera problemática; pero volatile
ya previene esas cosas.) El único problema que compareAndSet
resuelve es que, en el enfoque volatileInt
, algún otro hilo puede aparecer con una escritura concurrente, entre cuando leemos volatileInt
( volatileInt == 1
) y cuando le escribimos ( volatileInt = 2
). compareAndSet
resuelve este problema bloqueando cualquier escritura de la competencia durante ese tiempo.
Esto es igualmente cierto en el caso específico de los "actualizadores" ( AtomicReferenceFieldUpdater
etc.): volatile
lecturas volatile
aún son duras. La única limitación de los actualizadores '' compareAndSet
'' es que, en lugar de "bloquear escrituras en competencia" como escribí anteriormente, solo bloquean escrituras en competencia desde la misma instancia de AtomicReferenceFieldUpdater
; no pueden protegerlo cuando está actualizando simultáneamente un campo volatile
(o, para el caso, cuando está utilizando simultáneamente varios AtomicReferenceFieldUpdater
s para actualizar el mismo campo volatile
). (Por cierto, dependiendo de cómo lo mires, lo mismo se AtomicReference
y su parentesco: si tuvieras que actualizar sus campos de una manera que los pasara por alto, ellos no podrían protegerte. La diferencia es que en realidad AtomicReference
una AtomicReference
posee su campo, y es private
, por lo que no es necesario advertirle de que no lo modifique de alguna manera por medios externos.)
Entonces, para responder a su pregunta: Sí, puede continuar leyendo campos volatile
con las mismas garantías de atomicidad contra lecturas parciales / inconsistentes, contra declaraciones reordenadas, etc.
Editado para agregar (6 de diciembre): cualquier persona que esté particularmente interesada en este tema probablemente estará interesada en la discusión inmediatamente a continuación. Me pidieron que actualizara la respuesta para aclarar los puntos más destacados de esa discusión:
Creo que el punto más importante para agregar es que lo anterior es mi propia interpretación de la documentación. Estoy bastante seguro de que lo he entendido correctamente y de que ninguna otra interpretación tiene sentido; y puedo, si lo desea, discutir el punto en longitud ;-); pero ni yo ni nadie más ha producido ninguna referencia a ningún documento autoritario que aborde este punto de manera más explícita que los dos documentos mencionados en la pregunta en sí (el Javadoc de la clase y Java concurrencia en la práctica ) y el documento mencionado en mi respuesta original a arriba (el Javadoc del paquete).
El siguiente punto más importante, creo, es que aunque la documentación de
AtomicReferenceUpdater
dice que no es seguro mezclarcompareAndSet
con una escritura volátil, creo que en plataformas típicas es realmente seguro. Es inseguro sólo en el caso general. Digo esto debido al siguiente comentario de la documentación del paquete:Las especificaciones de estos métodos permiten que las implementaciones empleen instrucciones atómicas eficientes a nivel de máquina que están disponibles en los procesadores contemporáneos. Sin embargo, en algunas plataformas, el soporte puede implicar algún tipo de bloqueo interno. Por lo tanto, no se garantiza estrictamente que los métodos no sean bloqueantes: un hilo puede bloquearse de forma transitoria antes de realizar la operación.
Asi que:
- En una implementación JDK típica para un procesador moderno,
AtomicReference.set
simplemente usa una escritura volátil, ya queAtomicReference.compareAndSet
usa una operación de comparación e intercambio que es atómica con respecto a las escrituras volátiles.AtomicReferenceUpdater.set
es necesariamente más complejo queAtomicReference.set
, porque tiene que usar una lógica de reflexión para actualizar un campo en otro objeto, pero sostengo que esa es la única razón por la que es más complejo. Una implementación típica llamaUnsafe.putObjectVolatile
, que es una escritura volátil por nombre más largo. - Pero no todas las plataformas admiten este enfoque, y si no lo hacen, se permite el bloqueo. A riesgo de simplificar en exceso, entiendo que esto significa que, en
compareAndSet
se puede implementar lacompareAndSet
elcompareAndSet
una clase atómica aplicando (más o menos) lasynchronized
a un método que usaget
yset
directa. Pero para que esto funcione, elset
también debe estarsynchronized
, por la razón explicada en mi respuesta original anterior; es decir, no puede ser simplemente una escritura volátil, porque entonces podría modificar el campo después de quecompareAndSet
haya llamado aget
pero antes de quecompareAndSet
set
llamadas. - No hace falta decir que el uso que hace mi respuesta original de la frase "bloqueo" no debe tomarse literalmente, ya que en una plataforma típica no se produce ninguna necesidad de bloqueo.
- En una implementación JDK típica para un procesador moderno,
En la implementación JDK 1.6.0_05 de Sun de
java.util.concurrent.ConcurrentLinkedQueue<E>
, encontramos esto:private static class Node<E> { private volatile E item; private volatile Node<E> next; private static final AtomicReferenceFieldUpdater<Node, Node> nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next"); private static final AtomicReferenceFieldUpdater<Node, Object> itemUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Object.class, "item"); Node(E x) { item = x; } Node(E x, Node<E> n) { item = x; next = n; } E getItem() { return item; } boolean casItem(E cmp, E val) { return itemUpdater.compareAndSet(this, cmp, val); } void setItem(E val) { itemUpdater.set(this, val); } Node<E> getNext() { return next; } boolean casNext(Node<E> cmp, Node<E> val) { return nextUpdater.compareAndSet(this, cmp, val); } void setNext(Node<E> val) { nextUpdater.set(this, val); } }
(nota: espacios en blanco ajustados para la compacidad), donde, una vez que se ha construido una instancia, no hay escrituras volátiles (es decir, todas las escrituras son a través de
AtomicReferenceFieldUpdater.compareAndSet
oAtomicReferenceFieldUpdater.set
, pero las lecturas volátiles parecen usarse libremente, sin llamada única aAtomicReferenceFieldUpdater.get
. Las versiones posteriores de JDK 1.6 se cambiaron para usarUnsafe
directamente (esto había sucedido con el JDK 1.6.0_27 de Oracle), pero las discusiones en la lista de correo JSR 166 atribuyen este cambio a consideraciones de rendimiento en lugar de a cualquier duda sobre la corrección de la implementación anterior.- Pero debo señalar que esto no es una autoridad a prueba de balas. Para mayor comodidad, escribo sobre la "implementación de Sun" como si hubiera sido una cosa unitaria, pero mi punto anterior anterior hace evidente que las implementaciones de JDK para diferentes plataformas pueden tener que hacer las cosas de manera diferente. El código anterior me parece haber sido escrito de una manera neutral para la plataforma, ya que evita las escrituras volátiles en favor de las llamadas a
AtomicReferenceFieldUpdater.set
; pero alguien que no acepta mi interpretación de un punto puede no aceptar mi interpretación del otro, y puede argumentar que el código anterior no está destinado a ser seguro para todas las plataformas. - Otra debilidad de esta autoridad es que, aunque
Node
parece permitir que se realicen lecturas volátiles al mismo tiempo que las llamadas aAtomicReferenceFieldUpdater.compareAndSet
, es una clase privada; y no he realizado ninguna prueba de que su propietario (ConcurrentLinkedQueue
) realmente haga esas llamadas sin sus propias precauciones. (Pero aunque no he probado la afirmación, dudo que alguien la cuestione).
- Pero debo señalar que esto no es una autoridad a prueba de balas. Para mayor comodidad, escribo sobre la "implementación de Sun" como si hubiera sido una cosa unitaria, pero mi punto anterior anterior hace evidente que las implementaciones de JDK para diferentes plataformas pueden tener que hacer las cosas de manera diferente. El código anterior me parece haber sido escrito de una manera neutral para la plataforma, ya que evita las escrituras volátiles en favor de las llamadas a
Consulte los comentarios a continuación para conocer los antecedentes de este apéndice y para obtener más información.
De la documentación de AtomicReferenceFieldUpdater
Java :
Tenga en cuenta que las garantías del método
compareAndSet
en esta clase son más débiles que en otras clases atómicas. Debido a que esta clase no puede garantizar que todos los usos del campo sean apropiados para fines de acceso atómico, puede garantizar la atomicidad y la semántica volátil solo con respecto a otras invocaciones decompareAndSet
yset
.
Esto significa que no puedo hacer escrituras volátiles normales junto con compareAndSet
, pero tengo que usar set
lugar. No menciona nada sobre get
.
¿ compareAndSet
significa que todavía puedo leer campos volátiles con las mismas garantías de atomicidad? ¿Todas las escrituras anteriores al set
o compareAndSet
son visibles para todos los que han leído el campo volátil?
¿O tengo que usar get
en AtomicReferenceFieldUpdater
lugar de lecturas volátiles en el campo?
Por favor publique referencias si las tiene.
Gracias.
EDITAR:
Desde Java Concurrency in Practice , lo único que dicen:
Las garantías de atomicidad para las clases de actualización son más débiles que para las clases atómicas regulares porque no puede garantizar que los campos subyacentes no se modifiquen directamente; los métodos compareAndSet y aritméticos garantizan la atomicidad solo con respecto a otros subprocesos que utilizan los métodos de actualización de campo atómico.
Nuevamente, no se menciona cómo los otros subprocesos deben leer estos campos volátiles.
Además, ¿tengo razón al suponer que "modificado directamente" es una escritura volátil regular?
Esta no será una respuesta exacta de la pregunta:
Ni la explicación, ni la intención se ve clara en la documentación. Si la idea fuera eludir el ordenamiento global, también conocido como escritura volátil en arquitecturas que lo permiten [como IBM Power o ARM] y simplemente exponer el comportamiento de CAS (LoadLinked / StoreCondition) SIN cercas, sería un esfuerzo sorprendente y una fuente de confusión.
El CAS de sun.misc.Unsafe no tiene ninguna especificación ni garantías de pedido (se conoce como sucede antes), pero java.util.atomic ... sí. Así que en el modelo más débil java.util.atomic impl. Requeriría las cercas necesarias para seguir la especificación de Java en este caso.
Asumiendo que las clases de Updater en realidad carecen de vallas. Si lo hacen, la lectura volátil del campo (sin usar get) devolverá el valor de actualización, es decir, claramente get()
es necesario. Dado que no habrá garantías de pedido, las tiendas anteriores podrían no propagarse (en modelos débiles). El hardware x86 / Sparc TSO garantiza la especificación de java.
Sin embargo, eso también significa que se puede reordenar CAS con las siguientes lecturas no volátiles. Hay una nota interesante de la cola java.util.concurrent.SynchronousQueue
:
// Note: item and mode fields don''t need to be volatile
// since they are always written before, and read after,
// other volatile/atomic operations.
Todas las operaciones atómicas mencionadas son exactamente CAS de AtomicReferenceFieldUpdater. Eso implicaría la falta o la grabación entre las lecturas Y escrituras normales y AtomicReferenceFieldUpdater.CAS, es decir, actuar como escritura volátil.
s.item = null; // forget item
s.waiter = null; // forget thread
//....
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);
Solo CAS, no escribe volátil.
Dada la condición anterior, concluiría que AtomicXXXFieldUpdater expone la misma semántica que sus homólogos de AtomicXXX.
Lo que esto significa es que la referencia al objeto estará garantizada, pero como puede usar cualquier objeto, es posible que los campos de ese objeto no se escriban correctamente cuando otro subproceso va a acceder al objeto.
La única forma en que se podría garantizar es si los campos fueran finales o volátiles.