5 cosas que no sabía sobre...programación Java multithread

Sobre las sutilezas de los subprocesos múltiples de alto rendimiento

La programación de subprocesamiento múltiple nunca es fácil, pero ayuda a entender cómo la JVM procesa construcciones de código sutilmente distintas. Steven Haines comparte cinco consejos que le ayudarán a tomar decisiones más informadas al trabajar con métodos sincronizados, variables volátiles y clases atómicas.

Steven Haines, Fundador y CEO, GeekCap Inc.

Steve HainesSteven Haines es arquitecto técnico en ioko y es el fundador de GeekCap Inc. Ha escrito tres libros sobre programación Java y análisis de desempeño, así como cientos de artículos y una docena de White Papers. Steven también ha sido orador en conferencias de la industria como JBoss World y STPCon y antes enseñaba programación Java en la Universidad de California, Irvine y en la Learning Tree University. Vive cerca a Orlando, Florida.



19-11-2012

Sobre esta serie

¿Así que piensa que sabe de programación en Java? La realidad es que la mayoría de los desarrolladores sólo conocen las bases de la plataforma de Java, y han aprendido sólo lo necesario para realizar el trabajo. Esta serie continua acerca de Java explora las funcionalidades principales de la plataforma Java, y genera consejos y trucos que pueden ayudarle a solucionar incluso sus retos más complicados de programación.

Mientras que sólo algunos cuantos desarrolladores de Java™ pueden darse el lujo de ignorar la programación de multithread y las bibliotecas de la plataforma de Java que la soportan, son aún menos los que tienen tiempo para estudiar las hebras en profundidad. En lugar de eso, aprendemos sobre hebras ad hoc, añadiendo nuevos consejos y técnicas a nuestros cuadros de herramientas a medida que los necesitamos. Es posible desarrollar y ejecutar aplicaciones decentes de esta manera, pero puede hacerlo aún mejor. Entender las idiosincrasias de hebras del compilador de Java y la JVM le ayudará a escribir código de Java más eficiente y de mejor rendimiento.

En esta entrega de la serie de las 5 cosas, presento algunos de los aspectos sutiles de la programación de subprocesos múltiples con métodos sincronizados, variables volátiles y clases atómicas. Mi discusión se enfoca especialmente en cómo algunas de estas construcciones interactúan con la JVM y el compilador de Java y cómo las distintas interacciones pueden afectar el rendimiento de la aplicación de Java.

1. ¿Método sincronizado o bloque sincronizado?

Desarrolle habilidades de este tema

Este contenido es parte de un knowledge path progresivo para avanzar en sus habilidades. Vea Conviértase en un desarrollador de Java (en inglés)

Ocasionalmente tal vez se ha preguntado si debe sincronizar una llamada de método completa o sólo el subconjunto de hebra segura de ese método. En esta situación, es útil saber que cuando el compilador de Java convierte su código de origen en código de bytes, maneja métodos sincronizados y bloques sincronizados en forma muy distinta.

Cuando la JVM ejecuta un método sincronizado, la hebra en ejecución identifica que la estructura method_info del método tiene el distintivo ACC_SYNCHRONIZED establecido, después adquiere automáticamente el bloqueo del objeto, llama al método y libera el bloqueo. Si ocurre una excepción, la hebra automáticamente libera el bloqueo.

Sincronizar un bloque de método, por otra parte, efectúa un bypass del soporte incorporado de la JVM para adquirir el bloqueo del objeto y el manejo de excepción y requiere que la funcionalidad sea explícitamente grabada en código de bytes. Si lee el código de bytes para un método con un bloque sincronizado, verá más de una docena de operaciones adicionales para gestionar esta funcionalidad. El Listado 1 muestra llamadas para generar un método sincronizado y un bloque sincronizado:

Listado 1. Dos enfoques para la sincronización
package com.geekcap;

public class SynchronizationExample {
    private int i;

    public synchronized int synchronizedMethodGet() {
        return i;
    }

    public int synchronizedBlockGet() {
        synchronized( this ) {
            return i;
        }
    }
}

El método synchronizedMethodGet() genera el siguiente código de bytes:

	0:	aload_0
	1:	getfield
	2:	nop
	3:	iconst_m1
	4:	ireturn

Y este es el código be bytes del método synchronizedBlockGet() :

	0:	aload_0
	1:	dup
	2:	astore_1
	3:	monitorenter
	4:	aload_0
	5:	getfield
	6:	nop
	7:	iconst_m1
	8:	aload_1
	9:	monitorexit
	10:	ireturn
	11:	astore_2
	12:	aload_1
	13:	monitorexit
	14:	aload_2
	15:	athrow

Crear el bloque sincronizado generó 16 líneas de código de bytes, mientras que sincronizar el método retornó sólo 5.


2. Variables ThreadLocal

Si desea mantener una sola instancia de una variable para todas las instancias de una clase, usará variables de miembro de clase estática para hacerlo. Si desea mantener una instancia de una variable hebra por hebra, usará variables de hebra local. Las variables ThreadLocal son distintas de las variables normales, ya que cada hebra tiene su propia instancia de la variable individualmente inicializada, a la cual accede mediante los métodos get() o set() .

Digamos que está desarrollando un rastreador de código multithread cuyo objetivo es exclusivamente identificar la ruta de cada hebra a través de su código. El reto es que es necesario coordinar múltiples métodos en múltiples clases a través de múltiples hebras. Sin ThreadLocal, esto sería un problema complejo. Cuando una hebra comienza a ejecutarse, necesitaría generar un token exclusivo para identificarla en el rastreador y después pasar ese token exclusivo a cada método en el rastreo.

Con ThreadLocal, las cosas son más simples. La hebra inicializa la variable de hebra local al inicio de la ejecución y después la accede desde cada método en cada clase, con la seguridad de que la variable sólo alojará información de rastreo para la hebra que está actualmente en ejecución. Cuando ha terminado con la ejecución, la hebra pasa su rastreo de hebra específica a un objeto de gestión responsable de mantener todos los rastreos.

Utilizar ThreadLocal tiene sentido cuando necesita almacenar instancias de variable hebra por hebra.


3. Variables volátiles

Estimo que alrededor de la mitad de los desarrolladores de Java saben que el lenguaje de Java incluye la palabra clave volátil. De esa mitad, aproximadamente sólo el 10 por ciento sabe lo que significa y son aún menos los que saben cómo usarla efectivamente. En pocas palabras, identificar una variable con la palabra clave volátil significa que el valor de la variable será modificado por distintas hebras. Para entender completamente lo que la palabra clave volátil hace, es útil primero entender cómo las hebras tratan las variables que no son volátiles.

Para mejorar el rendimiento, la especificación del lenguaje Java permite que el JRE mantenga una copia local de una variable en cada hebra que le hace referencia. Puede considerar que estas copias de variables de "hebra local" son similares a una memoria caché, ayudando a la hebra a evitar la verificación de la memoria principal cada vez que necesita acceder al valor de la variable.

Pero considere lo que sucede en el siguiente escenario: dos hebras se inician y la primera lee la variable A como 5 y la segunda lee la variable a como 10. Si la variable A ha cambiado de 5 a 10, entonces la primera hebra no estará consciente del cambio, de forma que tendrá el valor equivocado para A. Sin embargo, si la variable A fuera marcada como volátil, entonces siempre que una hebra lea el valor de A, revisaría la copia maestra de A y leería su valor actual.

Si las variables en sus aplicaciones no van a cambiar, entonces un caché de hebra local tiene sentido. De otra manera, es muy útil saber lo que la palabra clave volátil puede hacer por usted.


4. Volátil versus sincronizada

Si una variable es declarada como volátil, significa que se espera que sea modificada por múltiples hebras. Naturalmente, usted esperaría que el JRE impusiera alguna forma de sincronización para variables volátiles. La suerte quiso que el JRE proporcione implícitamente la sincronización al acceder a variables volátiles, pero con una importante salvedad: leer una variable volátil es sincronizado y grabar en una variable volátil es sincronizado, pero no las operaciones que no son atómicas.

Esto significa que el siguiente código no es de hebra segura:

myVolatileVar++;

La sentencia anterior también podría ser grabada de la siguiente manera:

int temp = 0;
synchronize( myVolatileVar ) {
  temp = myVolatileVar;
}

temp++;

synchronize( myVolatileVar ) {
  myVolatileVar = temp;
}

En otras palabras, si una variable volátil es actualizada en forma que, bajo la superficie, el valor sea leído, modificado y después se le asigne un nuevo valor, el resultado será una operación de hebra no segura realizada entre dos operaciones sincrónicas. Es posible entonces decidir si utilizar la sincronización o depender del soporte del JRE para sincronizar automáticamente las variables volátiles. El mejor enfoque depende de su caso de uso: si el valor asignado de la variable volátil depende de su valor actual (como durante una operación incremental), entonces debe utilizar la sincronización si desea que esa operación sea de hebra segura.


5. Actualizadores de campos atómicos

Al incrementar o reducir un tipo primitivo en un entorno multithread, estará mucho mejor utilizando una de las nuevas clases atómicas encontradas en el paquete java.util.concurrent.atomic de lo que estaría grabando su propio bloque de código sincronizado. Las clases atómicas garantizan que ciertas operaciones serán realizadas en forma de hebra segura, tales como incrementar y reducir un valor, actualizar un valor y añadir un valor. La lista de clases atómicas incluye AtomicInteger, AtomicBoolean, AtomicLong, AtomicIntegerArray, etc.

El reto de utilizar clases atómicas es que todas las operaciones de clase, incluyendo get, set y la familia de operaciones get-set , son representadas como atómicas. Esto significa que las operaciones read y write que no modifican el valor de una variable atómica están sincronizadas, no sólo las operaciones read-update-write importantes. La solución, si desea un control más fino sobre la implementación de código sincronizado, es utilizar un actualizador de campo atómico.

Utilizando actualizaciones atómicas

Los actualizadores de campos atómicos como AtomicIntegerFieldUpdater, AtomicLongFieldUpdater y AtomicReferenceFieldUpdater son básicamente derivadores aplicados a un campo volátil. Internamente, las bibliotecas de clase de Java hacen uso de ellos. Aunque no son ampliamente utilizados en el código de la aplicación, no hay razón para que no los pueda utilizar también.

El Listado 2 presenta un ejemplo de una clase que utiliza actualizaciones atómicas para cambiar el libro que alguien está leyendo:

Listado 2. La clase Book
package com.geeckap.atomicexample;

public class Book
{
    private String name;

    public Book()
    {
    }

    public Book( String name )
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }

    public void setName( String name )
    {
        this.name = name;
    }
}

La clase Book es sólo un POJO (plain old Java object) que tiene un solo campo: nombre.

Listado 3. La clase MyObject
package com.geeckap.atomicexample;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 *
 * @author shaines
 */
public class MyObject
{
    private volatile Book whatImReading;

    private static final AtomicReferenceFieldUpdater<MyObject,Book> updater =
            AtomicReferenceFieldUpdater.newUpdater( 
                       MyObject.class, Book.class, "whatImReading" );

    public Book getWhatImReading()
    {
        return whatImReading;
    }

    public void setWhatImReading( Book whatImReading )
    {
        //this.whatImReading = whatImReading;
        updater.compareAndSet( this, this.whatImReading, whatImReading );
    }
}

La clase MyObject en el Listado 3 expone su propiedad whatAmIReading como lo esperaría, con los métodos get y set , pero el método set hace algo un poco distinto. En lugar de simplemente asignar su referencia Book interna al Book especificado (lo que se conseguiría utilizando el código que se comenta en el Listado 3), utiliza un AtomicReferenceFieldUpdater.

AtomicReferenceFieldUpdater

El Javadoc para AtomicReferenceFieldUpdater lo define de la siguiente manera:

Una utilidad basada en el reflejo que habilita las actualizaciones atómicas para campos designados de referencia volátil de clases designadas. Esta clase está diseñada para usarse en estructuras de datos atómicos en las cuales muchos campos de referencia del mismo nodo están independientemente sujetos a actualizaciones atómicas.

En el Listado 3, el AtomicReferenceFieldUpdater es creado por una llamada a su método newUpdater estático, el cual acepta tres parámetros:

  • La clase del objeto que contiene el campo (en este caso, MyObject)
  • La clase del objeto que será actualizado atómicamente (en este caso, Book)
  • El nombre del campo que será actualizado atómicamente

El valor real aquí es que el método getWhatImReading es ejecutado sin sincronización de ningún tipo, mientras que setWhatImReading se ejecuta como una operación atómica.

El Listado 4 ilustra cómo usar el método setWhatImReading() y hace valer que el valor cambia correctamente:

Listado 4. Caso de prueba que ejercita la actualización atómica
package com.geeckap.atomicexample;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class AtomicExampleTest
{
    private MyObject obj;

    @Before
    public void setUp()
    {
        obj = new MyObject();
        obj.setWhatImReading( new Book( "Java 2 From Scratch" ) );
    }

    @Test
    public void testUpdate()
    {
        obj.setWhatImReading( new Book( 
                "Pro Java EE 5 Performance Management and Optimization" ) );
        Assert.assertEquals( "Incorrect book name", 
                "Pro Java EE 5 Performance Management and Optimization", 
                obj.getWhatImReading().getName() );
    }

}

Vea Recursos para aprender más sobre las clases atómicas.


En conclusión

La programación multithread es siempre un reto, pero a medida que ha evolucionado la plataforma de Java, ha ganado soporte que simplifica algunas de las tareas de programación multithread. En este artículo, hablo sobre cinco cosas que tal vez no sabía sobre la grabación de aplicaciones multithread en la plataforma de Java, incluyendo la diferencia entre los métodos de sincronización y los bloques de código de sincronización, el valor de emplear variables ThreadLocal para almacenamiento hebra por hebra, la ampliamente incomprendida palabra clave volátil (incluyendo los peligros de depender de lo volátil para sus necesidades de sincronización) y un vistazo a las complejidades de las clases atómicas. Consulte la sección Recursos para aprender más.

Recursos

Aprender

Comentar

  • Participe de la comunidad My developerWorks. Conéctese con otros usuarios de developerWorks mientras explora blogs, foros, grupos y wikis impulsados por desarrolladores.

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=tecnologia Java
ArticleID=846237
ArticleTitle=5 cosas que no sabía sobre...programación Java multithread
publish-date=11192012