Uma introdução ao ThreadLocal em Java
1. Visão geral
Neste artigo, veremos a construçãoThreadLocal do pacotejava.lang. Isso nos permite armazenar dados individualmente para o encadeamento atual - e simplesmente agrupá-lo em um tipo especial de objeto.
2. APIThreadLocal
A construçãoTheadLocal nos permite armazenar dados que serãoaccessible only pora specific thread.
Digamos que queremos um valorInteger que será agrupado com o segmento específico:
ThreadLocal threadLocalValue = new ThreadLocal<>();
Em seguida, quando queremos usar este valor de uma thread, precisamos apenas chamar um métodoget() ouset(). Simplificando, podemos pensar queThreadLocal armazena dados dentro de um mapa - com o thread como a chave.
Devido a esse fato, quando chamamos um métodoget() emthreadLocalValue, obteremos um valorInteger para o thread solicitante:
threadLocalValue.set(1);
Integer result = threadLocalValue.get();
Podemos construir uma instância deThreadLocal usando o método estáticowithInitial() e passando um fornecedor para ele:
ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);
Para remover o valor deThreadLocal, podemos chamar um métodoremove():
threadLocal.remove();
Para ver como usarThreadLocal corretamente, primeiro, vamos olhar para um exemplo que não usaThreadLocal, depois reescreveremos nosso exemplo para alavancar essa construção.
3. Armazenamento de dados do usuário em um mapa
Vamos considerar um programa que precisa armazenar os dadosContext específicos do usuário por determinado ID de usuário:
public class Context {
private String userName;
public Context(String userName) {
this.userName = userName;
}
}
Queremos ter um segmento por ID de usuário. Vamos criar uma classeSharedMapWithUserContext que implementa uma interfaceRunnable. A implementação no métodorun() chama algum banco de dados por meio da classeUserRepository que retorna um objetoContext para um determinadouserId.
Em seguida, armazenamos esse contexto emConcurentHashMap digitado poruserId:
public class SharedMapWithUserContext implements Runnable {
public static Map userContextPerUserId
= new ConcurrentHashMap<>();
private Integer userId;
private UserRepository userRepository = new UserRepository();
@Override
public void run() {
String userName = userRepository.getUserNameForUserId(userId);
userContextPerUserId.put(userId, new Context(userName));
}
// standard constructor
}
Podemos facilmente testar nosso código criando e iniciando dois threads para doisuserIds diferentes e afirmando que temos duas entradas no mapauserContextPerUserId:
SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();
assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);
4. Armazenamento de dados do usuário emThreadLocal
Podemos reescrever nosso exemplo para armazenar a instância do usuárioContext usando umThreadLocal. Cada thread terá sua própria instânciaThreadLocal.
Ao usarThreadLocal, precisamos ter muito cuidado porque cada instância deThreadLocal está associada a um determinado thread. Em nosso exemplo, temos um encadeamento dedicado para cadauserIde esse encadeamento é criado por nós para que tenhamos controle total sobre ele.
O métodorun() buscará o contexto do usuário e o armazenará na variávelThreadLocal usando o métodoset():
public class ThreadLocalWithUserContext implements Runnable {
private static ThreadLocal userContext
= new ThreadLocal<>();
private Integer userId;
private UserRepository userRepository = new UserRepository();
@Override
public void run() {
String userName = userRepository.getUserNameForUserId(userId);
userContext.set(new Context(userName));
System.out.println("thread context for given userId: "
+ userId + " is: " + userContext.get());
}
// standard constructor
}
Podemos testá-lo iniciando dois threads que executarão a ação para um determinadouserId:
ThreadLocalWithUserContext firstUser
= new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser
= new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();
Depois de executar este código, veremos na saída padrão queThreadLocal foi definido por determinado encadeamento:
thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}
Podemos ver que cada um dos usuários tem seu próprio contexto.
5. Não useThreadLocal comExecutorService
Se quisermos usar umExecutorServicee enviar umRunnable para ele, usarThreadLocal produzirá resultados não determinísticos - porque não temos uma garantia de que todas as ações deRunnable para um determinadouserId será tratado pela mesma thread toda vez que for executado.
Por causa disso, nossoThreadLocal será compartilhado entre diferentesuserIds. É por isso que não devemos usar umTheadLocal junto comExecutorService. Ele só deve ser usado quando tivermos controle total sobre qual thread escolherá qual ação executável executar.
6. Conclusão
Neste artigo rápido, estávamos olhando para a construçãoThreadLocal. Implementamos a lógica que usaConcurrentHashMap que foi compartilhado entre os threads para armazenar o contexto associado a um determinadouserId. Em seguida, reescrevemos nosso exemplo para aproveitarThreadLocal para armazenar dados que estão associados a um particularuserId e com um determinado segmento.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.