La clase ThreadLocal de Java y usos prácticos

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente Comentarios

En Java existe una clase con la que podemos asociar un dato al hilo o thread que ejecuta el código. Podemos usar esta clase para evitar incluir en cada método el parámetro de ese dato si es usado en multitud de métodos simplificando en gran medida el código. En las aplicaciones web este dato puede ser el usuario que se ha autenticado, el dominio por el que se ha accedido a la aplicación, el dispositivo móvil, el idioma del usuario o cualquier otra información que queramos esté disponible de forma global en el hilo de ejecución.

Java

En algunos casos nos encontramos con que un determinado dato lo pasamos como parámetro en sucesivas llamadas a métodos, obligándonos a declararlo en cada uno de ellos. En una aplicación web el dato podría ser el usuario autenticado, el dominio por el que se ha accedido a la aplicación, el dispositivo móvil, el idioma del usuario, su preferencia de divisa o cualquier otra información no relativa al usuario pero que igualmente la aplicación necesita en muchos sitios y usa esta información contextual para variar su funcionalidad. En una aplicación web podemos obtener el dominio por el que es accedido la aplicación con la clase Request, asociando el dato como un atributo de la petición lo tendremos disponible de forma global en la capa de presentación. Sin embargo, este objeto request solo estará disponible en la capa de presentación de la aplicación con la intención de que la capa de lógica de negocio sea independiente de la tecnología o framework web. Para hacer que algún dato global también esté disponible en la capa de lógica de negocio podemos usar la clase ThreadLocal.

La clase ThreadLocal básicamente asocia un dato con el Thread que ejecuta el código, teniendo una variable global en la aplicación con la referencia a la clase ThreadLocal podemos acceder al dato desde cualquier punto de la aplicación. Si necesitásemos varios datos tendríamos varias instancias globales de ThreadLocal.

Las variables globales es algo a evitar pero para algunos datos podemos hacer una excepción ya que entendemos que las ventajas son mayores que las desventajas, uno de los mayores peligros de las variables globales es la concurrencia si varios hilos modifican el dato global, como el dato asociado a ThreadLocal es local al hilo no hay peligro, simplemente deberemos asegurarnos de que una vez el hilo de ejecución termine limpiar el dato para que la siguiente petición en una aplicación web que procese ese hilo no use una dato anterior de otra petición.

La clase ThreadLocal es bastante simple tiene un método para establecer el dato con set, para obtenerlo con get, para eliminarlo con remove y desde Java 8 para establecer el valor inicial con una interfaz funcional Supplier con el método withInitial.

Veamos un ejemplo en el que crearemos un filtro que detecte el dominio por el que se accede a la aplicación y lo deje disponible en una variable ThreadLocal de modo que la aplicación lo tenga disponible desde cualquier capa de la aplicación. Primero crearemos una clase con variables estáticas globales que contendrá la referencia a la instancia ThreadLocal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package io.github.picodotdev.plugintapestry.misc;

public class Globals {

    public static ThreadLocal<String> HOST;

    static {
        HOST = new ThreadLocal<>();
    }
}
Globals.java

A continuación el filtro que obtiene el dominio de la request y lo deja en el ThreadLocal de forma global. Al usar una clase ThreadLocal es importante limpiar el dato correctamente, en este caso usando un bloque try con su finally para que aunque se produzca una excepción el dato acabe desasociado del thread al finalizar la petición, de lo contrario tendremos una fuga de memoria.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package io.github.picodotdev.plugintapestry.misc;

import javax.servlet.*;
import java.io.IOException;

public class AppFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            Globals.HOST.set(request.getServerName());

            chain.doFilter(request, response);
        } finally {
            Globals.HOST.remove();
        }
    }

    @Override
    public void destroy() {
    }
}
AppFilter.java

Finalmente, podemos acceder al dato desde cualquier punto de la aplicación, en este caso desde la capa de presentación en la página Index de una aplicación usando Apache Tapestry y desde la capa de lógica de negocio o servicios que es independiente de la capa de presentación.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.github.picodotdev.plugintapestry.pages;

...

public class Index {

    ...
    
    void setupRender() {
		// ThreadLocal example
		System.out.printf("Host (from page): %s%n", Globals.HOST.get());
		...
	}
		
	...
}
Index.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package io.github.picodotdev.plugintapestry.services.dao;

...

public class DefaultJooqProductoDAO implements GenericDAO<Producto>, JooqProductoDAO {

    ...

    @Override
    @Transactional(readOnly = true)
    public Producto findById(Long id) {
        // ThreadLocal example
        System.out.printf("Host (from service): %s%n", Globals.HOST.get());

        return context.selectFrom(PRODUCTO).where(PRODUCTO.ID.eq(id)).fetchOneInto(Producto.class);
    }
    
    ...
}
DefaultJooqProductoDAO.java

Con el filtro y las anteriores clases en la consola se imprimirán los mensajes con el dominio por el que ha sido accedida la aplicación, en este caso localhost.

1
2
3
Host (from page): localhost
...
Host (from service): localhost
System.out

En definitiva, en ciertos casos el uso selectivo de ThreadLocal simplifica el código evitando incluir un parámetro en multitud de métodos y si el dato ha de recuperarse de la base de datos evita realizar la misma consulta varias veces haciendo el programa más eficiente.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: