Diseño para la seguridad de los hilos

Hace seis meses comencé una serie de artículos sobre el diseño de clases y objetos. En la columna de Técnicas de diseño de este mes , continuaré esa serie analizando los principios de diseño relacionados con la seguridad de los hilos. Este artículo le dice qué es la seguridad de subprocesos, por qué la necesita, cuándo la necesita y cómo conseguirla.

¿Qué es la seguridad de subprocesos?

La seguridad de subprocesos simplemente significa que los campos de un objeto o clase siempre mantienen un estado válido, como lo observan otros objetos y clases, incluso cuando se utilizan simultáneamente por varios subprocesos.

Una de las primeras pautas que propuse en esta columna (consulte "Diseño de inicialización de objetos") es que debe diseñar clases de manera que los objetos mantengan un estado válido, desde el comienzo de su vida útil hasta el final. Si sigue este consejo y crea objetos cuyas variables de instancia son todas privadas y cuyos métodos solo realizan las transiciones de estado adecuadas en esas variables de instancia, está en buena forma en un entorno de un solo subproceso. Pero puede meterse en problemas cuando surjan más hilos.

Varios subprocesos pueden significar problemas para su objeto porque, a menudo, mientras un método está en proceso de ejecución, el estado de su objeto puede ser temporalmente inválido. Cuando solo un subproceso invoca los métodos del objeto, solo se ejecutará un método a la vez, y cada método podrá finalizar antes de que se invoque otro método. Por lo tanto, en un entorno de un solo subproceso, a cada método se le dará la oportunidad de asegurarse de que cualquier estado temporalmente no válido se cambie a un estado válido antes de que el método regrese.

Sin embargo, una vez que introduce varios subprocesos, la JVM puede interrumpir el subproceso que ejecuta un método mientras las variables de instancia del objeto todavía se encuentran en un estado temporalmente no válido. La JVM podría entonces dar una oportunidad de ejecución a un subproceso diferente, y ese subproceso podría llamar a un método en el mismo objeto. Todo su arduo trabajo para hacer que sus variables de instancia sean privadas y sus métodos realicen solo transformaciones de estado válidas no será suficiente para evitar que este segundo hilo observe el objeto en un estado no válido.

Un objeto de este tipo no sería seguro para subprocesos, porque en un entorno de subprocesos múltiples, el objeto podría dañarse u observarse que tiene un estado no válido. Un objeto seguro para subprocesos es aquel que siempre mantiene un estado válido, como lo observan otras clases y objetos, incluso en un entorno multiproceso.

¿Por qué preocuparse por la seguridad de los hilos?

Hay dos grandes razones por las que necesita pensar en la seguridad de los subprocesos cuando diseña clases y objetos en Java:

  1. El soporte para múltiples subprocesos está integrado en el lenguaje Java y la API

  2. Todos los subprocesos dentro de una máquina virtual Java (JVM) comparten el mismo área de montón y método

Debido a que el subproceso múltiple está integrado en Java, es posible que cualquier clase que diseñe eventualmente pueda ser utilizada simultáneamente por varios subprocesos. No necesita (y no debe) hacer que todas las clases que diseñe sean seguras para subprocesos, porque la seguridad para subprocesos no es gratuita. Pero al menos debería pensar en la seguridad de los subprocesos cada vez que diseñe una clase Java. Encontrará una discusión sobre los costos de la seguridad de subprocesos y pautas sobre cuándo hacer que las clases sean seguras para subprocesos más adelante en este artículo.

Dada la arquitectura de la JVM, solo debe preocuparse por las variables de instancia y clase cuando se preocupe por la seguridad de los subprocesos. Debido a que todos los subprocesos comparten el mismo montón, y el montón es donde se almacenan todas las variables de instancia, varios subprocesos pueden intentar utilizar las variables de instancia del mismo objeto al mismo tiempo. Del mismo modo, debido a que todos los subprocesos comparten la misma área de método, y el área de método es donde se almacenan todas las variables de clase, varios subprocesos pueden intentar utilizar las mismas variables de clase al mismo tiempo. Cuando elige hacer una clase segura para subprocesos, su objetivo es garantizar la integridad, en un entorno multiproceso, de las variables de instancia y clase declaradas en esa clase.

No necesita preocuparse por el acceso multiproceso a variables locales, parámetros de métodos y valores de retorno, porque estas variables residen en la pila de Java. En la JVM, cada hilo recibe su propia pila de Java. Ningún hilo puede ver o utilizar variables locales, valores de retorno o parámetros que pertenezcan a otro hilo.

Dada la estructura de la JVM, las variables locales, los parámetros de método y los valores de retorno son inherentemente "seguros para subprocesos". Pero las variables de instancia y las variables de clase solo serán seguras para subprocesos si diseña su clase de manera adecuada.

RGBColor # 1: listo para un solo hilo

Como ejemplo de una clase que no es segura para subprocesos, considere la RGBColorclase que se muestra a continuación. Las instancias de esta clase representan un color almacenada en tres variables de instancia privadas: r, g, y b. Dada la clase que se muestra a continuación, un RGBColorobjeto comenzaría su vida en un estado válido y solo experimentaría transiciones de estado válido, desde el comienzo de su vida hasta el final, pero solo en un entorno de un solo subproceso.

// En subprocesos de archivos / ex1 / RGBColor.java // Las instancias de esta clase NO son seguras para subprocesos. public class RGBColor {private int r; privado int g; privado int b; public RGBColor (int r, int g, int b) {checkRGBVals (r, g, b); esto.r = r; this.g = g; this.b = b; } public void setColor (int r, int g, int b) {checkRGBVals (r, g, b); esto.r = r; this.g = g; this.b = b; } / ** * devuelve el color en una matriz de tres entradas: R, G y B * / public int [] getColor () {int [] retVal = new int [3]; retVal [0] = r; retVal [1] = g; retVal [2] = b; return retVal; } public void invert () {r = 255 - r; g = 255 - g; b = 255 - b; } checkRGBVals vacío estático privado (int r, int g, int b) {if (r 255 || g 255 || b <0 || b> 255) {lanzar una nueva IllegalArgumentException (); }}}

Debido a que las tres variables de instancia, ints r, gy b, son privadas, la única forma en que otras clases y objetos pueden acceder o influir en los valores de estas variables es a través RGBColordel constructor y los métodos. El diseño del constructor y los métodos garantizan que:

  1. RGBColorEl constructor siempre dará a las variables valores iniciales adecuados.

  2. Métodos setColor()y invert()siempre realizará transformaciones de estado válidas en estas variables

  3. El método getColor()siempre devolverá una vista válida de estas variables

Note that if bad data is passed to the constructor or the setColor() method, they will complete abruptly with an InvalidArgumentException. The checkRGBVals() method, which throws this exception, in effect defines what it means for an RGBColor object to be valid: the values of all three variables, r, g, and b, must be between 0 and 255, inclusive. In addition, in order to be valid, the color represented by these variables must be the most recent color either passed to the constructor or setColor() method, or produced by the invert() method.

If, in a single-threaded environment, you invoke setColor() and pass in blue, the RGBColor object will be blue when setColor() returns. If you then invoke getColor() on the same object, you'll get blue. In a single-threaded society, instances of this RGBColor class are well-behaved.

Throwing a concurrent wrench into the works

Unfortunately, this happy picture of a well-behaved RGBColor object can turn scary when other threads enter the picture. In a multithreaded environment, instances of the RGBColor class defined above are susceptible to two kinds of bad behavior: write/write conflicts and read/write conflicts.

Write/write conflicts

Imagine you have two threads, one thread named "red" and another named "blue." Both threads are trying to set the color of the same RGBColor object: The red thread is trying to set the color to red; the blue thread is trying to set the color to blue.

Both of these threads are trying to write to the same object's instance variables concurrently. If the thread scheduler interleaves these two threads in just the right way, the two threads will inadvertently interfere with each other, yielding a write/write conflict. In the process, the two threads will corrupt the object's state.

The Unsynchronized RGBColor applet

El siguiente subprograma, denominado RGBColor no sincronizado , muestra una secuencia de eventos que podrían resultar en un RGBColorobjeto dañado . El hilo rojo intenta inocentemente poner el color en rojo mientras que el hilo azul intenta inocentemente poner el color en azul. Al final, el RGBColorobjeto no representa ni rojo ni azul, sino el inquietante color, el magenta.

Por alguna razón, su navegador no le permitirá ver este genial subprograma Java.

Para recorrer la secuencia de eventos que conducen a un RGBColorobjeto dañado , presione el botón Paso del subprograma. Presione Atrás para retroceder un paso y Restablecer para retroceder al principio. A medida que avanza, una línea de texto en la parte inferior del subprograma explicará lo que sucede durante cada paso.

For those of you who can't run the applet, here's a table that shows the sequence of events demonstrated by the applet:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes setColor(255, 0, 0) 0 0 0  
red checkRGBVals(255, 0, 0); 0 0 0  
red this.r = 255; 0 0 0  
red this.g = 0; 255 0 0  
red this.b = 0; 255 0 0  
red red thread returns 255 0 0  
blue later, blue thread continues 255 0 0  
blue this.b = 255 255 0 0  
blue blue thread returns 255 0 255  
none object represents magenta 255 0 255  

As you can see from this applet and table, the RGBColor is corrupted because the thread scheduler interrupts the blue thread while the object is still in a temporarily invalid state. When the red thread comes in and paints the object red, the blue thread is only partially finished painting the object blue. When the blue thread returns to finish the job, it inadvertently corrupts the object.

Read/write conflicts

Another kind of misbehavior that may be exhibited in a multithreaded environment by instances of this RGBColor class is read/write conflicts. This kind of conflict arises when an object's state is read and used while in a temporarily invalid state due to the unfinished work of another thread.

For example, note that during the blue thread's execution of the setColor() method above, the object at one point finds itself in the temporarily invalid state of black. Here, black is a temporarily invalid state because:

  1. It is temporary: Eventually, the blue thread intends to set the color to blue.

  2. It is invalid: No one asked for a black RGBColor object. The blue thread is supposed to turn a green object into blue.

If the blue thread is preempted at the moment the object represents black by a thread that invokes getColor() on the same object, that second thread would observe the RGBColor object's value to be black.

Here's a table that shows a sequence of events that could lead to just such a read/write conflict:

Thread Statement r g b Color
none object represents green 0 255 0  
blue blue thread invokes setColor(0, 0, 255) 0 255 0  
blue checkRGBVals(0, 0, 255); 0 255 0  
blue this.r = 0; 0 255 0  
blue this.g = 0; 0 255 0  
blue blue gets preempted 0 0 0  
red red thread invokes getColor() 0 0 0  
red int[] retVal = new int[3]; 0 0 0  
red retVal[0] = 0; 0 0 0  
red retVal[1] = 0; 0 0 0  
red retVal[2] = 0; 0 0 0  
red return retVal; 0 0 0  
red red thread returns black 0 0 0  
blue later, blue thread continues 0 0 0  
blue this.b = 255 0 0 0  
blue blue thread returns 0 0 255  
none object represents blue 0 0 255  

As you can see from this table, the trouble begins when the blue thread is interrupted when it has only partially finished painting the object blue. At this point the object is in a temporarily invalid state of black, which is exactly what the red thread sees when it invokes getColor() on the object.

Three ways to make an object thread-safe

There are basically three approaches you can take to make an object such as RGBThread thread-safe:

  1. Synchronize critical sections
  2. Make it immutable
  3. Use a thread-safe wrapper

Approach 1: Synchronizing the critical sections

The most straightforward way to correct the unruly behavior exhibited by objects such as RGBColor when placed in a multithreaded context is to synchronize the object's critical sections. An object's critical sections are those methods or blocks of code within methods that must be executed by only one thread at a time. Put another way, a critical section is a method or block of code that must be executed atomically, as a single, indivisible operation. By using Java's synchronized keyword, you can guarantee that only one thread at a time will ever execute the object's critical sections.

To take this approach to making your object thread-safe, you must follow two steps: you must make all relevant fields private, and you must identify and synchronize all the critical sections.

Step 1: Make fields private

La sincronización significa que solo un hilo a la vez podrá ejecutar un poco de código (una sección crítica). Entonces, aunque es campos a los que desea coordinar el acceso entre múltiples subprocesos, el mecanismo de Java para hacerlo realmente coordina el acceso al código. Esto significa que solo si hace que los datos sean privados, podrá controlar el acceso a esos datos controlando el acceso al código que manipula los datos.