El libro "Concurrencia de Java en la práctica"

imagenHola habrozhiteli! Las transmisiones son una parte fundamental de la plataforma Java. Los procesadores multi-core son comunes, y el uso efectivo de la concurrencia se ha vuelto necesario para crear cualquier aplicación de alto rendimiento. Una máquina virtual Java mejorada, soporte para clases de alto rendimiento y un rico conjunto de bloques de construcción para tareas de paralelización fueron un gran avance en el desarrollo de aplicaciones paralelas. En Java Concurrency in Practice, los creadores de tecnología innovadora explican no solo cómo funcionan, sino que también hablan sobre patrones de diseño. Es fácil crear un programa competitivo que parece funcionar. Sin embargo, el desarrollo, las pruebas y la depuración de programas multiproceso plantean muchos problemas. El código deja de funcionar justo cuando es más importante: bajo una carga pesada.En "Concurrencia de Java en la práctica" encontrará teoría y métodos específicos para crear aplicaciones paralelas confiables, escalables y compatibles. Los autores no ofrecen una lista de API y mecanismos de paralelismo; introducen reglas de diseño, patrones y modelos que son independientes de la versión de Java y siguen siendo relevantes y efectivos durante muchos años.

Extracto. Hilo de seguridad


Puede que se sorprenda de que la programación competitiva se asocie con hilos o bloqueos (1) no más que la ingeniería civil se asocia con remaches y vigas en I. Por supuesto, la construcción de puentes requiere el uso correcto de una gran cantidad de remaches y vigas en I, y lo mismo se aplica a la construcción de programas competitivos, que requieren el uso correcto de hilos y cerraduras. Pero estos son solo mecanismos, medios para lograr el objetivo. Escribir código seguro para subprocesos es, en esencia, controlar el acceso a un estado y, en particular, a un estado mutable.

En general, el estado de un objeto son sus datos almacenados en variables de estado, como instancia y campos estáticos o campos de otros objetos dependientes. El estado del hash HashMap se almacena parcialmente en el propio HashMap, pero también en muchos objetos Map.Entry. El estado de un objeto incluye cualquier dato que pueda afectar su comportamiento.

(1) lock block, «», , . blocking. lock «», « ». lock , , , «». — . , , , . — . . .

Varios subprocesos pueden acceder a una variable compartida, mutada: cambia su valor. De hecho, estamos tratando de proteger los datos, no el código, del acceso competitivo no controlado.

La creación de un objeto seguro para subprocesos requiere sincronización para coordinar el acceso a un estado mutado, un incumplimiento que puede conducir a la corrupción de datos y otras consecuencias indeseables.

Cada vez que más de un hilo accede a una variable de estado y uno de los hilos posiblemente escribe en él, todos los hilos deben coordinar su acceso a él mediante sincronización. La sincronización en Java es proporcionada por la palabra clave sincronizada, que proporciona bloqueo exclusivo, así como variables volátiles y atómicas y bloqueos explícitos.

Resista la tentación de pensar que hay situaciones que no requieren sincronización. El programa puede funcionar y pasar sus pruebas, pero sigue funcionando mal y se bloquea en cualquier momento.

Si varios subprocesos acceden a la misma variable con un estado mutado sin una sincronización adecuada, entonces su programa no funciona correctamente. Hay tres formas de solucionarlo:

  • No comparta la variable de estado en todos los hilos
  • hacer que la variable de estado no sea mutable;
  • use la sincronización de estado cada vez que acceda a la variable de estado.

Las correcciones pueden requerir cambios de diseño significativos, por lo que es mucho más fácil diseñar una clase segura para subprocesos de inmediato que actualizarla más tarde.

Es difícil averiguar si múltiples hilos accederán a esta o aquella variable. Afortunadamente, las soluciones técnicas orientadas a objetos que ayudan a crear clases bien organizadas y fáciles de mantener, como encapsular y ocultar datos, también ayudan a crear clases seguras para subprocesos. Cuantos menos subprocesos tengan acceso a una variable en particular, más fácil será garantizar la sincronización y establecer las condiciones bajo las cuales se puede acceder a esta variable. El lenguaje Java no lo obliga a encapsular el estado; es perfectamente aceptable almacenar el estado en campos públicos (incluso campos estáticos públicos) o publicar un enlace a un objeto que de otro modo sería interno, pero cuanto mejor esté encapsulado el estado de su programa,cuanto más fácil sea hacer que su programa sea seguro y ayudar a los encargados a mantenerlo así.

Al diseñar clases seguras para subprocesos, sus buenos asistentes serán soluciones técnicas orientadas a objetos: encapsulación, mutabilidad y una especificación clara de invariantes.

Si las buenas soluciones técnicas de diseño orientado a objetos difieren de las necesidades del desarrollador, debe sacrificar las reglas del buen diseño por el bien del rendimiento o la compatibilidad con el código heredado. A veces, la abstracción y la encapsulación están en desacuerdo con el rendimiento, aunque no tan a menudo como creen muchos desarrolladores, pero la mejor práctica es hacer que el código sea correcto primero y luego rápido. Intente utilizar la optimización solo si las mediciones de productividad y necesidades indican que debe hacerlo (2) .

(2)En el código competitivo, debe cumplir con esta práctica incluso más de lo habitual. Dado que los errores competitivos son extremadamente difíciles de reproducir y no son fáciles de depurar, la ventaja de una pequeña ganancia de rendimiento en algunas ramas de código raramente utilizadas puede ser bastante insignificante en comparación con el riesgo de que el programa se bloquee en condiciones operativas.

Si decide que necesita romper la encapsulación, no todo está perdido. Su programa aún puede hacerse seguro para subprocesos, pero el proceso será más complicado y más costoso, y el resultado será poco confiable. El Capítulo 4 describe las condiciones bajo las cuales se puede mitigar de forma segura la encapsulación de variables de estado.

Hasta ahora, hemos usado los términos "clase segura para subprocesos" y "programa seguro para subprocesos" casi indistintamente. ¿Es un programa seguro para subprocesos construido completamente a partir de clases seguras para subprocesos? Opcional: un programa que consiste completamente en clases seguras para subprocesos puede no ser seguro para subprocesos y un programa seguro para subprocesos puede contener clases que no son seguras para subprocesos. Los problemas relacionados con el diseño de clases seguras para subprocesos también se analizan en el Capítulo 4. En cualquier caso, el concepto de una clase segura para subprocesos tiene sentido solo si la clase encapsula su propio estado. El término "seguridad de subprocesos" se puede aplicar al código, pero habla del estado y solo se puede aplicar a esa matriz de código que encapsula su estado (puede ser un objeto o todo el programa).

2.1. ¿Qué es la seguridad del hilo?


Definir la seguridad del hilo no es fácil. Una búsqueda rápida en Google le ofrece numerosas opciones como estas:

... se puede llamar desde múltiples hilos de programa sin interacciones no deseadas entre hilos.

... puede ser llamado por dos o más hilos al mismo tiempo, sin requerir ninguna otra acción de la persona que llama.

Dadas tales definiciones, ¡no es sorprendente que la seguridad de los hilos sea confusa! ¿Cómo distinguir una clase segura para subprocesos de una clase insegura? ¿Qué queremos decir con la palabra "seguro"?

En el corazón de cualquier definición razonable de seguridad del hilo está la noción de corrección.

La corrección significa que la clase se ajusta a su especificación. La especificación define invariantes que limitan el estado de un objeto y las condiciones posteriores que describen los efectos de las operaciones. ¿Cómo sabes que las especificaciones para las clases son correctas? De ninguna manera, pero esto no nos impide usarlos después de habernos convencido de que el código funciona. Así que supongamos que la corrección de un solo subproceso es algo visible. Ahora podemos suponer que la clase segura para subprocesos se comporta correctamente durante el acceso desde múltiples subprocesos.

Una clase es segura para subprocesos si se comporta correctamente durante el acceso desde múltiples subprocesos, independientemente de cómo estos subprocesos están programados o intercalados por el entorno de trabajo, y sin sincronización adicional u otra coordinación por parte del código de llamada.

Un programa multiproceso no puede ser seguro para subprocesos si no es correcto incluso en un entorno de subproceso único (3) . Si el objeto se implementa correctamente, entonces ninguna secuencia de operaciones (acceso a métodos públicos y lectura o escritura en campos públicos) debería violar sus invariantes o condiciones posteriores. Ningún conjunto de operaciones realizadas de forma secuencial o competitiva en instancias de una clase segura para subprocesos puede hacer que una instancia esté en un estado no válido.

(3) Si el uso suelto del término corrección le molesta aquí, entonces puede pensar en una clase segura para subprocesos como una clase que es defectuosa en un entorno competitivo, así como en un entorno de un solo subproceso.

Las clases seguras para subprocesos encapsulan cualquier sincronización necesaria y no necesitan la ayuda de un cliente.

2.1.1 Ejemplo: servlet sin soporte de estado interno


En el Capítulo 1, hemos enumerado las estructuras que crean hilos y llaman componentes de los cuales usted es responsable de la seguridad de los hilos. Ahora tenemos la intención de desarrollar un servicio de factorización de servlets y expandir gradualmente su funcionalidad mientras se mantiene la seguridad del hilo.

El Listado 2.1 muestra un servlet simple que descomprime un número de una consulta, lo factoriza y envuelve los resultados en respuesta.

Listado 2.1. Servlet sin soporte interno del estado

@ThreadSafe
public class StatelessFactorizer implements Servlet {
      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            encodeIntoResponse(resp, factors);
      }
}

La clase StatelessFactorizer, como la mayoría de los servlets, no tiene estado interno: no contiene campos y no hace referencia a campos de otras clases. El estado para un cálculo particular existe solo en las variables locales que se almacenan en la pila de flujo y están disponibles solo para el flujo en ejecución. Un subproceso que accede a StatelessFactorizer no puede afectar el resultado de que otro subproceso haga lo mismo, porque estos subprocesos no comparten el estado.

Los objetos sin soporte de estado interno siempre son seguros para subprocesos.

El hecho de que la mayoría de los servlets se pueden implementar sin soporte interno del estado reduce significativamente la carga de enhebrar los servlets mismos. Y solo cuando los servlets necesitan recordar algo, aumentan los requisitos para la seguridad de su hilo.

2.2. Atomicidad


¿Qué sucede cuando un elemento de estado se agrega a un objeto sin soporte interno de estado? Supongamos que queremos agregar un contador de visitas que mida el número de solicitudes procesadas. Puede agregar un campo de tipo largo al servlet e incrementarlo con cada solicitud, como se muestra en UnsafeCountingFactorizer en el Listado 2.2.

Listado 2.2. Un servlet que cuenta las solicitudes sin la sincronización necesaria. Esto no debe hacerse.

imagen

@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
      private long count = 0;

      public long getCount() { return count; }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            ++count;
            encodeIntoResponse(resp, factors);
      }
}

Desafortunadamente, la clase UnsafeCountingFactorizer no es segura para subprocesos, incluso si funciona bien en un entorno de subproceso único. Al igual que UnsafeSequence, es propenso a perder actualizaciones. Aunque el recuento de la operación incremental ++ tiene una sintaxis compacta, no es atómica, es decir, indivisible, sino una secuencia de tres operaciones: entregar el valor actual, agregarle uno y volver a escribir el nuevo valor. En las operaciones de "lectura, cambio, escritura", el estado resultante se deriva del anterior.

En la Fig. 1.1 se muestra lo que puede suceder si dos hilos intentan aumentar el contador al mismo tiempo, sin sincronización. Si el contador es 9, entonces, debido a una coordinación de tiempo fallida, ambos hilos verán el valor 9, agregarán uno y establecerán el valor en 10. Por lo tanto, el contador de visitas comenzará a retrasarse en uno.

Puede pensar que tener un contador de visitas ligeramente inexacto en un servicio web es una pérdida aceptable, y a veces lo es. Pero si el contador se usa para crear secuencias o identificadores únicos de objetos, entonces devolver el mismo valor de múltiples activaciones puede generar serios problemas de integridad de datos. La posibilidad de la aparición de resultados incorrectos debido a una coordinación de tiempo fallida surge en una condición de carrera.

2.2.1. Condiciones de carrera


La clase UnsafeCountingFactorizer tiene varias condiciones de carrera (4) . El tipo más común de condición de carrera es la situación de "comprobar y luego actuar", donde se usa una observación potencialmente obsoleta para decidir qué hacer a continuación.

(4) (data race). , . , , , , , . Java. , , . UnsafeCountingFactorizer . 16.

A menudo nos encontramos con una condición de carrera en la vida real. Suponga que planea encontrarse con un amigo al mediodía en Starbucks Café en Universitetskiy Prospekt. Pero descubrirás que hay dos Starbucks en University Avenue. A las 12:10 no ves a tu amigo en el café A y vas al café B, pero él tampoco está allí. O tu amigo llega tarde, o llegó al café A inmediatamente después de que te fuiste, o estaba en el café B, pero fue a buscarte y ahora se dirige al café A. Aceptaremos esto último, es decir, el peor de los casos. Ahora son las 12:15 y los dos se preguntan si su amigo cumplió su promesa. ¿Regresarás a otro café? ¿Cuántas veces irás de un lado a otro? Si no ha acordado un protocolo, puede pasar todo el día caminando por la Avenida de la Universidad en euforia con cafeína.
El problema con el enfoque de "dar un paseo y ver si él está allí" es que un paseo por la calle entre dos cafés lleva varios minutos, y durante este tiempo el estado del sistema puede cambiar.

El ejemplo con Starbucks ilustra la dependencia del resultado en la coordinación relativa del tiempo de los eventos (en cuánto tiempo espera a un amigo mientras está en un café, etc.). La observación de que no está en el café A se vuelve potencialmente inválida: tan pronto como salgas de la puerta principal, él puede entrar por la puerta trasera. La mayoría de las condiciones de carrera causan problemas como una excepción inesperada, datos sobrescritos y corrupción de archivos.

2.2.2. Ejemplo: condiciones de carrera en inicialización perezosa


Un truco común que utiliza el enfoque "comprobar y luego actuar" es la inicialización diferida (LazyInitRace). Su propósito es posponer la inicialización del objeto hasta que sea necesario, y asegurarse de que se inicialice solo una vez. En el Listado 2.3, el método getInstance verifica que el ExpensiveObject se inicializa y devuelve una instancia existente o, de lo contrario, crea una nueva instancia y la devuelve después de mantener una referencia a ella.

Listado 2.3. La condición de carrera está en una inicialización perezosa. Esto no debe hacerse.

imagen

@NotThreadSafe
public class LazyInitRace {
      private ExpensiveObject instance = null;

      public ExpensiveObject getInstance() {
            if (instance == null)
                instance = new ExpensiveObject();
            return instance;
      }
}

La clase LazyInitRace contiene condiciones de carrera. Suponga que los hilos A y B ejecutan el método getInstance al mismo tiempo. A ve que el campo de instancia es nulo y crea un nuevo objeto caro. El hilo B también verifica si el campo de instancia es el mismo nulo. La presencia de nulo en el campo en este momento depende de la coordinación del tiempo, incluidos los caprichos de la planificación y la cantidad de tiempo requerida para crear una instancia del Objeto caro y establecer el valor en el campo de la instancia. Si el campo de instancia es nulo cuando B lo verifica, dos elementos de código que llaman al método getInstance pueden obtener dos resultados diferentes, incluso si se supone que el método getInstance siempre devuelve la misma instancia.

El contador de visitas en UnsafeCountingFactorizer también contiene condiciones de carrera. El enfoque de "leer, cambiar, escribir" implica que para incrementar el contador, la secuencia debe conocer su valor anterior y asegurarse de que nadie más cambie o use este valor durante el proceso de actualización.

Como la mayoría de los errores competitivos, las condiciones de carrera no siempre conducen al fracaso: la coordinación temporal es exitosa. Pero si la clase LazyInitRace se usa para crear instancias del registro de toda la aplicación, cuando devuelva diferentes instancias de múltiples activaciones, los registros se perderán o las acciones recibirán representaciones contradictorias del conjunto de objetos registrados. O si la clase UnsafeSequence se usa para generar identificadores de entidad en una estructura de conservación de datos, entonces dos objetos diferentes pueden tener el mismo identificador, violando las restricciones de identidad.

2.2.3. Acciones compuestas


Tanto LazyInitRace como UnsafeCountingFactorizer contienen una secuencia de operaciones que deben ser atómicas. Pero para evitar una condición de carrera, debe haber un obstáculo para que otros hilos usen la variable mientras un hilo la modifica.

Las operaciones A y B son atómicas si, desde el punto de vista del hilo que realiza la operación A, la operación B fue realizada completamente por otro hilo o ni siquiera parcialmente.

La atomicidad de la operación de incremento en UnsafeSequence evitaría la condición de carrera que se muestra en la Fig. 1.1. Las operaciones "verificar y luego actuar" y "leer, cambiar, escribir" siempre deben ser atómicas. Se denominan acciones compuestas: secuencias de operaciones que deben realizarse atómicamente para mantener la seguridad de subprocesos. En la siguiente sección, consideraremos el bloqueo, un mecanismo integrado en Java que proporciona atomicidad. Mientras tanto, solucionaremos el problema de otra manera aplicando la clase segura de subprocesos existente, como se muestra en el Factorizador de conteo en el Listado 2.4.

Listado 2.4. Solicitudes de conteo de servlets usando AtomicLong

@ThreadSafe
public class CountingFactorizer implements Servlet {
      private final AtomicLong count = new AtomicLong(0);

      public long getCount() { return count.get(); }

      public void service(ServletRequest req, ServletResponse resp) {
            BigInteger i = extractFromRequest(req);
            BigInteger[] factors = factor(i);
            count.incrementAndGet();
            encodeIntoResponse(resp, factors);
      }
}

El paquete java.util.concurrent.atomic contiene variables atómicas para gestionar estados de clase. Reemplazando el tipo de contador de largo a AtomicLong, garantizamos que todas las acciones que se refieren al estado del contador son atómicas1. Dado que el estado del servlet es el estado del contador, y el contador es seguro para subprocesos, nuestro servlet se vuelve seguro para subprocesos.

Cuando se agrega un único elemento de estado a una clase que no admite el estado interno, la clase resultante será segura para subprocesos si el estado está completamente controlado por el objeto seguro para subprocesos. Pero, como veremos en la siguiente sección, la transición de una variable de estado a la siguiente no será tan simple como la transición de cero a uno.

Cuando sea conveniente, utilice objetos existentes seguros para subprocesos, como AtomicLong, para controlar el estado de su clase. Los posibles estados de los objetos seguros para subprocesos existentes y sus transiciones a otros estados son más fáciles de mantener y verificar para la seguridad de subprocesos que las variables de estado arbitrarias.

»Se puede encontrar más información sobre el libro en el sitio web del editor
» Contenido
» Extracto de

Khabrozhiteley 25% de descuento en el cupón - Java

Después del pago de la versión en papel del libro, se envía un libro electrónico por correo electrónico.

All Articles