JavaRush /Blog Java /Random-ES /Gestionar la volatilidad
lexmirnov
Nivel 29
Москва

Gestionar la volatilidad

Publicado en el grupo Random-ES

Directrices para el uso de variables volátiles

Por Brian Goetz 19 de junio de 2007 Original: Gestión de la volatilidad Las variables volátiles en Java se pueden denominar "luz sincronizada"; Requieren menos código que los bloques sincronizados, a menudo se ejecutan más rápido, pero solo pueden hacer una fracción de lo que hacen los bloques sincronizados. Este artículo presenta varios patrones para usar volátiles de manera efectiva y algunas advertencias sobre dónde no usarlos. Los bloqueos tienen dos características principales: exclusión mutua (mutex) y visibilidad. La exclusión mutua significa que un bloqueo solo puede ser mantenido por un subproceso a la vez, y esta propiedad se puede usar para implementar protocolos de control de acceso para recursos compartidos de modo que solo un subproceso los use a la vez. La visibilidad es una cuestión más sutil, su propósito es garantizar que los cambios realizados en los recursos públicos antes de que se libere el bloqueo sean visibles para el siguiente hilo que se haga cargo de ese bloqueo. Si la sincronización no garantizara la visibilidad, los subprocesos podrían recibir valores obsoletos o incorrectos para las variables públicas, lo que provocaría una serie de problemas graves.
Variables volátiles
Las variables volátiles tienen las propiedades de visibilidad de las sincronizadas, pero carecen de atomicidad. Esto significa que los subprocesos utilizarán automáticamente los valores más actuales de variables volátiles. Se pueden utilizar para la seguridad de subprocesos , pero en un conjunto muy limitado de casos: aquellos que no introducen relaciones entre múltiples variables o entre valores actuales y futuros de una variable. Por lo tanto, volátil por sí solo no es suficiente para implementar un contador, un mutex o cualquier clase cuyas partes inmutables estén asociadas con múltiples variables (por ejemplo, "inicio <= fin"). Puede elegir bloqueos volátiles por una de dos razones principales: simplicidad o escalabilidad. Algunas construcciones de lenguaje son más fáciles de escribir como código de programa, y ​​luego de leer y comprender, cuando usan variables volátiles en lugar de bloqueos. Además, a diferencia de los bloqueos, no pueden bloquear un hilo y, por lo tanto, son menos propensos a sufrir problemas de escalabilidad. En situaciones en las que hay muchas más lecturas que escrituras, las variables volátiles pueden proporcionar beneficios de rendimiento sobre los bloqueos.
Condiciones para el uso correcto de volátiles.
Puede reemplazar las cerraduras por otras volátiles en un número limitado de circunstancias. Para ser seguro para subprocesos, se deben cumplir ambos criterios:
  1. Lo que se escribe en una variable es independiente de su valor actual.
  2. La variable no participa en invariantes con otras variables.
En pocas palabras, estas condiciones significan que los valores válidos que se pueden escribir en una variable volátil son independientes de cualquier otro estado del programa, incluido el estado actual de la variable. La primera condición excluye el uso de variables volátiles como contadores seguros para subprocesos. Aunque incrementar (x++) parece una sola operación, en realidad es una secuencia completa de operaciones de lectura, modificación y escritura que deben realizarse de forma atómica, algo que volátil no proporciona. Una operación válida requeriría que el valor de x permanezca igual durante toda la operación, lo que no se puede lograr usando volátiles. (Sin embargo, si puede asegurarse de que el valor se escriba desde un solo hilo, se puede omitir la primera condición). En la mayoría de las situaciones, se violará la primera o la segunda condición, lo que hace que las variables volátiles sean un enfoque menos utilizado para lograr la seguridad de los subprocesos que las sincronizadas. El Listado 1 muestra una clase no segura para subprocesos con un rango de números. Contiene una invariante: el límite inferior siempre es menor o igual que el superior. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Dado que las variables de estado de rango están limitadas de esta manera, no será suficiente hacer volátiles los campos inferior y superior para garantizar que la clase sea segura para subprocesos; La sincronización seguirá siendo necesaria. De lo contrario, tarde o temprano tendrá mala suerte y dos subprocesos que ejecuten setLower() y setUpper() con valores inapropiados pueden llevar el rango a un estado inconsistente. Por ejemplo, si el valor inicial es (0, 5), el subproceso A llama a setLower(4) y al mismo tiempo el subproceso B llama a setUpper(3), estas operaciones entrelazadas darán como resultado un error, aunque ambas pasarán la verificación. que se supone que protege el invariante. Como resultado, el rango será (4, 3): valores incorrectos. Necesitamos hacer que setLower() y setUpper() sean atómicos para otras operaciones de rango, y hacer que los campos sean volátiles no logrará eso.
Consideraciones de rendimiento
La primera razón para utilizar volátiles es la simplicidad. En algunas situaciones, usar una variable de este tipo es simplemente más fácil que usar el bloqueo asociado a ella. La segunda razón es el rendimiento; a veces, los volátiles funcionarán más rápido que los bloqueos. Es extremadamente difícil hacer declaraciones precisas y abarcadoras como "X siempre es más rápido que Y", especialmente cuando se trata de las operaciones internas de la Máquina Virtual Java. (Por ejemplo, la JVM puede liberar el bloqueo por completo en algunas situaciones, lo que dificulta discutir los costos de volátil versus sincronización de manera abstracta). Sin embargo, en la mayoría de las arquitecturas de procesadores modernas, el costo de leer variables volátiles no es muy diferente del costo de leer variables regulares. El costo de escribir variables volátiles es significativamente mayor que escribir variables regulares debido a la barrera de memoria requerida para la visibilidad, pero generalmente es más económico que establecer bloqueos.
Patrones para el uso adecuado de volátiles.
Muchos expertos en concurrencia tienden a evitar el uso de variables volátiles porque son más difíciles de usar correctamente que los bloqueos. Sin embargo, existen algunos patrones bien definidos que, si se siguen cuidadosamente, pueden utilizarse de forma segura en una amplia variedad de situaciones. Respete siempre las limitaciones de los volátiles: utilice únicamente volátiles que sean independientes de cualquier otra cosa en el programa, y ​​esto debería evitar que entre en territorio peligroso con estos patrones.
Patrón n.º 1: banderas de estado
Quizás el uso canónico de variables mutables sean simples indicadores de estado booleanos que indiquen que ha ocurrido un evento único e importante del ciclo de vida, como la finalización de la inicialización o una solicitud de apagado. Muchas aplicaciones incluyen una construcción de control de la forma: "hasta que estemos listos para cerrar, siga ejecutando", como se muestra en el Listado 2: Es volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } probable que el método Shutdown() sea llamado desde algún lugar fuera del bucle, en otro hilo. por lo que se requiere sincronización para garantizar la visibilidad variable correcta. apagadoSolicitado. (Se puede llamar desde un oyente JMX, un oyente de acción en un hilo de evento de GUI, a través de RMI, a través de un servicio web, etc.). Sin embargo, un bucle con bloques sincronizados será mucho más engorroso que un bucle con un indicador de estado volátil como en el Listado 2. Debido a que volátil facilita la escritura de código y el indicador de estado no depende de ningún otro estado del programa, este es un ejemplo de un Buen uso de volátiles. La característica de tales banderas de estatus es que normalmente hay sólo una transición de estado; el indicador ShutdownRequested pasa de falso a verdadero y luego el programa se cierra. Este patrón se puede extender a indicadores estatales que pueden cambiar hacia adelante y hacia atrás, pero solo si el ciclo de transición (de falso a verdadero y a falso) ocurre sin intervención externa. De lo contrario, se necesita algún tipo de mecanismo de transición atómica, como variables atómicas.
Patrón n.º 2: publicación segura única
Los errores de visibilidad que son posibles cuando no hay sincronización pueden convertirse en un problema aún más difícil al escribir referencias de objetos en lugar de valores primitivos. Sin sincronización, puede ver el valor actual de una referencia de objeto escrita por otro hilo y aún ver valores de estado obsoleto para ese objeto. (Esta amenaza está en la raíz del problema con el infame bloqueo de doble verificación, donde la referencia de un objeto se lee sin sincronización y se corre el riesgo de ver la referencia real pero obtener un objeto parcialmente construido a través de ella). Una forma de publicar de forma segura un object es hacer una referencia a un objeto volátil. El Listado 3 muestra un ejemplo en el que, durante el inicio, un subproceso en segundo plano carga algunos datos de la base de datos. Otro código que podría intentar utilizar estos datos comprueba si se han publicado antes de intentar utilizarlos. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Si la referencia a theFlooble no fuera volátil, el código en doWork() correría el riesgo de ver un Flooble parcialmente construido al intentar hacer referencia a theFlooble. El requisito clave para este patrón es que el objeto publicado debe ser seguro para subprocesos o efectivamente inmutable (efectivamente inmutable significa que su estado nunca cambia después de su publicación). Un enlace volátil puede garantizar que un objeto sea visible en su forma publicada, pero si el estado del objeto cambia después de la publicación, se requiere sincronización adicional.
Patrón #3: Observaciones independientes
Otro ejemplo simple de uso seguro de volátil es cuando las observaciones se "publican" periódicamente para su uso dentro de un programa. Por ejemplo, hay un sensor ambiental que detecta la temperatura actual. El hilo de fondo puede leer este sensor cada pocos segundos y actualizar una variable volátil que contiene la temperatura actual. Luego, otros hilos pueden leer esta variable, sabiendo que el valor que contiene siempre está actualizado. Otro uso de este patrón es la recopilación de estadísticas sobre el programa. El Listado 4 muestra cómo el mecanismo de autenticación puede recordar el nombre del último usuario que inició sesión. La referencia de lastUser se reutilizará para publicar el valor para que lo utilice el resto del programa. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Este patrón amplía el anterior; el valor se publica para su uso en otras partes del programa, pero la publicación no es un evento único, sino una serie de eventos independientes. Este patrón requiere que el valor publicado sea efectivamente inmutable, es decir, que su estado no cambie después de la publicación. El código que utiliza el valor debe tener en cuenta que puede cambiar en cualquier momento.
Patrón n.º 4: patrón de “frijol volátil”
El patrón "bean volátil" es aplicable en marcos que utilizan JavaBeans como "estructuras glorificadas". El patrón "bean volátil" utiliza un JavaBean como contenedor para un grupo de propiedades independientes con captadores y/o definidores. El fundamento del patrón "bean volátil" es que muchos marcos proporcionan contenedores para titulares de datos mutables (como HttpSession), pero los objetos colocados en estos contenedores deben ser seguros para subprocesos. En el patrón de bean volátil, todos los elementos de datos de JavaBean son volátiles, y los captadores y definidores deben ser triviales: no deben contener ninguna lógica más que obtener o establecer la propiedad correspondiente. Además, para los miembros de datos que son referencias a objetos, dichos objetos deben ser efectivamente inmutables. (Esto no permite campos de referencia de matriz, ya que cuando una referencia de matriz se declara volátil, sólo esa referencia, y no los elementos mismos, tiene la propiedad volátil). Como ocurre con cualquier variable volátil, no puede haber invariantes ni restricciones asociadas con las propiedades de JavaBeans. . En el Listado 5 se muestra un ejemplo de un JavaBean escrito utilizando el patrón “volatile bean”: @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Patrones volátiles más complejos
Los patrones de la sección anterior cubren la mayoría de los casos comunes en los que el uso de volátiles es razonable y obvio. Esta sección analiza un patrón más complejo en el que la volatilidad puede proporcionar un beneficio de rendimiento o escalabilidad. Los patrones volátiles más avanzados pueden ser extremadamente frágiles. Es fundamental que sus suposiciones estén cuidadosamente documentadas y que estos patrones estén fuertemente encapsulados, ¡porque incluso los cambios más pequeños pueden romper su código! Además, dado que la razón principal para los casos de uso volátiles más complejos es el rendimiento, asegúrese de tener una necesidad clara de obtener la ganancia de rendimiento prevista antes de usarlos. Estos patrones son compromisos que sacrifican la legibilidad o la facilidad de mantenimiento por posibles ganancias de rendimiento: si no necesita la mejora del rendimiento (o no puede demostrar que la necesita con un programa de medición riguroso), entonces probablemente sea un mal negocio porque eso estás renunciando a algo valioso y obteniendo algo menos a cambio.
Patrón n.º 5: bloqueo de lectura y escritura económico
A estas alturas ya deberías ser consciente de que la volátil es demasiado débil para implementar un contador. Dado que ++x es esencialmente una reducción de tres operaciones (leer, agregar, almacenar), si algo sale mal, perderá el valor actualizado si varios subprocesos intentan incrementar el contador volátil al mismo tiempo. Sin embargo, si hay muchas más lecturas que cambios, puede combinar el bloqueo intrínseco y las variables volátiles para reducir la sobrecarga general de la ruta del código. El Listado 6 muestra un contador seguro para subprocesos que utiliza sincronizado para garantizar que la operación de incremento sea atómica y utiliza volátil para garantizar que el resultado actual sea visible. Si las actualizaciones son poco frecuentes, este enfoque puede mejorar el rendimiento, ya que los costos de lectura se limitan a lecturas volátiles, que generalmente son más económicas que adquirir un bloqueo no conflictivo. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } La razón por la que este método se denomina "bloqueo de lectura y escritura barato" es porque utiliza diferentes mecanismos de sincronización para lecturas y escrituras. Debido a que las operaciones de escritura en este caso violan la primera condición de uso de volátil, no puede usar volátil para implementar un contador de manera segura; debe usar un bloqueo. Sin embargo, puede usar volátil para hacer visible el valor actual al leer, por lo que usa un bloqueo para todas las operaciones de modificación y volátil para operaciones de solo lectura. Si un bloqueo solo permite que un hilo a la vez acceda a un valor, las lecturas volátiles permiten más de uno, por lo que cuando usa volátil para proteger la lectura, obtiene un mayor nivel de intercambio que si usa un bloqueo en todo el código: y lee y graba. Sin embargo, tenga en cuenta la fragilidad de este patrón: con dos mecanismos de sincronización en competencia, puede volverse muy complejo si va más allá de la aplicación más básica de este patrón.
Resumen
Las variables volátiles son una forma de sincronización más simple pero más débil que el bloqueo, que en algunos casos proporciona un mejor rendimiento o escalabilidad que el bloqueo intrínseco. Si cumple con las condiciones para el uso seguro de volátil (una variable es verdaderamente independiente tanto de otras variables como de sus propios valores anteriores), a veces puede simplificar el código reemplazando sincronizado con volátil. Sin embargo, el código que utiliza volátiles suele ser más frágil que el código que utiliza bloqueo. Los patrones sugeridos aquí cubren los casos más comunes en los que la volatilidad es una alternativa razonable a la sincronización. Siguiendo estos patrones, y teniendo cuidado de no llevarlos más allá de sus propios límites, puede utilizar volatile de forma segura en los casos en que proporcionen beneficios.
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION