Nesta lição, falaremos de maneira geral sobre como trabalhar com a classe java.lang.ThreadLocal<> e como usá-la em um ambiente multithread.

A classe ThreadLocal é usada para armazenar variáveis. Uma característica distintiva dessa classe é que ela mantém uma cópia separada e independente de um valor para cada thread que a utiliza.

Aprofundando o funcionamento da classe, podemos imaginar um Map que mapeia threads para valores, de onde a thread atual pega o valor apropriado quando precisa utilizá-lo.

Construtor da classe ThreadLocal

Construtor Ação
ThreadLocal() Cria uma variável vazia em Java

Métodos

Método Ação
pegar() Retorna o valor da variável local do thread atual
definir() Define o valor da variável local para o thread atual
remover() Remove o valor da variável local do thread atual
ThreadLocal.withInitial() Método de fábrica adicional que define o valor inicial

prepare-se()

Vamos escrever um exemplo onde criamos dois contadores. A primeira, uma variável comum, servirá para contar o número de threads. O segundo vamos envolver em um ThreadLocal . E veremos como eles funcionam juntos. Primeiro, vamos escrever uma classe ThreadDemo que herda Runnable e contém nossos dados e o importantíssimo método run() . Também adicionaremos um método para exibir os contadores na tela:


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();

    public void run() {
        counter++;

        if(threadLocalCounter.get() != null) {
            threadLocalCounter.set(threadLocalCounter.get() + 1);
        } else {
            threadLocalCounter.set(0);
        }
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

A cada corrida da nossa aula, aumentamos ocontadorvariável chame o método get() para obter dados da variável ThreadLocal . Se o novo thread não tiver dados, iremos defini-lo como 0. Se houver dados, iremos aumentá-lo em um. E vamos escrever nosso método principal :


public static void main(String[] args) {
    ThreadDemo threadDemo = new ThreadDemo();

    Thread t1 = new Thread(threadDemo);
    Thread t2 = new Thread(threadDemo);
    Thread t3 = new Thread(threadDemo);

    t1.start();
    t2.start();
    t3.start();

}

Rodando nossa classe, vemos que a variável ThreadLocal permanece a mesma independente da thread que a acessa, mas o número de threads cresce.

Contador: 1
Contador: 2
Contador: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

Processo concluído com código de saída 0

remover()

Para entender como funciona o método remove , vamos apenas alterar um pouco o código da classe ThreadDemo :


if(threadLocalCounter.get() != null) {
      threadLocalCounter.set(threadLocalCounter.get() + 1);
  } else {
      if (counter % 2 == 0) {
          threadLocalCounter.remove();
      } else {
          threadLocalCounter.set(0);
      }
  }

Nesse código, se o contador de threads for um número par, chamaremos o método remove() em nossa variável ThreadLocal . Resultado:

Contador: 3
threadLocalCounter: 0
Contador: 2
threadLocalCounter: nulo
Contador: 1
threadLocalCounter: 0

Processo concluído com código de saída 0

E aqui vemos facilmente que a variável ThreadLocal no segundo thread é null .

ThreadLocal.withInitial()

Este método cria uma variável local de thread.

Implementação da classe ThreadDemo :


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 1);

    public void run() {
        counter++;
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

E podemos ver o resultado do nosso código:

Contador: 1
Contador: 2
Contador: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

Processo concluído com código de saída 0

Por que devemos usar tais variáveis?

ThreadLocal fornece uma abstração sobre variáveis ​​locais em relação ao thread de execução java.lang.Thread .

As variáveis ​​ThreadLocal diferem das comuns porque cada thread tem sua própria instância inicializada individualmente da variável, que é acessada por meio dosmétodos get() e set() .

Cada thread, ou seja, instância da classe Thread , possui um mapa de variáveis ​​ThreadLocal associadas a ela. As chaves do mapa são referências a objetos ThreadLocal e os valores são referências a variáveis ​​ThreadLocal "adquiridas" .

Por que a classe Random não é adequada para gerar números aleatórios em aplicativos multithread?

Usamos a classe Random para obter números aleatórios. Mas funciona tão bem em um ambiente multithread? Na verdade não. Random não é adequado para ambientes multithread, porque quando vários threads acessam uma classe ao mesmo tempo, o desempenho é prejudicado.

Para resolver esse problema, o JDK 7 introduziu a classe java.util.concurrent.ThreadLocalRandom para gerar números aleatórios em um ambiente multithread. Consiste em duas classes: ThreadLocal e Random .

Os números aleatórios recebidos por um encadeamento são independentes de outros encadeamentos, mas java.util.Random fornece números aleatórios globalmente. Além disso, ao contrário de Random , ThreadLocalRandom não oferece suporte à propagação explícita. Em vez disso, ele substitui o método setSeed() herdado de Random , para que sempre gere uma UnsupportedOperationException quando chamado.

Vejamos os métodos da classe ThreadLocalRandom :

Método Ação
ThreadLocalRandom current() Retorna o ThreadLocalRandom do thread atual.
int próximo(int bits) Gera o próximo número pseudoaleatório.
double nextDouble(menos duplo, limite duplo) Retorna um número pseudoaleatório de uma distribuição uniforme entre o mínimo (inclusivo) e o limite (exclusivo).
int nextInt(int pelo menos, int limite) Retorna um número pseudoaleatório de uma distribuição uniforme entre o mínimo (inclusivo) e o limite (exclusivo).
longo próximoLong(long n) Retorna um número pseudoaleatório de uma distribuição uniforme entre 0 (inclusivo) e o valor especificado (exclusivo).
long nextLong(long less, long bound) Retorna um número pseudoaleatório de uma distribuição uniforme entre o mínimo (inclusivo) e o limite (exclusivo).
void setSeed(semente longa) Lança UnsupportedOperationException . Este gerador não suporta semeadura.

Obtendo números aleatórios usando ThreadLocalRandom.current()

ThreadLocalRandom é uma combinação das classes ThreadLocal e Random . Ele obtém melhor desempenho em um ambiente multithread simplesmente evitando qualquer acesso simultâneo a instâncias da classe Random .

Vamos implementar um exemplo envolvendo múltiplas threads e ver como nossa aplicação faz com a classe ThreadLocalRandom :


import java.util.concurrent.ThreadLocalRandom;

class RandomNumbers extends Thread {

    public void run() {
        try {
            int bound = 100;
            int result = ThreadLocalRandom.current().nextInt(bound);
            System.out.println("Thread " + Thread.currentThread().getId() + " generated " + result);
        }
        catch (Exception e) {
            System.out.println("Exception");
        }
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

				for (int i = 0; i < 10; i++) {
            RandomNumbers randomNumbers = new RandomNumbers();
            randomNumbers.start();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime));
    }
}

Resultado do nosso programa:

Tempo gasto: 1
Tópico 17 gerado 13
Tópico 18 gerado 41
Tópico 16 gerado 99
Tópico 19 gerado 25
Tópico 23 gerado 33
Tópico 24 gerado 21
Tópico 15 gerado 15
Tópico 21 gerado 28
Tópico 22 gerado 97
Tópico 20 gerado 33

E agora vamos mudar nossa classe RandomNumbers e usar Random nela:


int result = new Random().nextInt(bound);
Tempo gasto: 5
Tópico 20 gerado 48
Tópico 19 gerado 57
Tópico 18 gerado 90
Tópico 22 gerado 43
Tópico 24 gerado 7
Tópico 23 gerado 63
Tópico 15 gerado 2
Tópico 16 gerado 40
Tópico 17 gerado 29
Tópico 21 gerado 12

Tome nota! Em nossos testes, às vezes os resultados eram os mesmos e às vezes eram diferentes. Mas se usarmos mais threads (digamos, 100), o resultado ficará assim:

Aleatório — 19-25 ms
ThreadLocalRandom — 17-19 ms

Da mesma forma, quanto mais threads houver em nosso aplicativo, maior será o impacto no desempenho ao usar a classe Random em um ambiente multithread.

Para resumir e reiterar as diferenças entre as classes Random e ThreadLocalRandom :

Aleatório TópicoLocalAleatório
Se threads diferentes usarem a mesma instância de Random , haverá conflitos e o desempenho será prejudicado. Não há conflitos ou problemas, porque os números aleatórios gerados são locais para o thread atual.
Usa uma fórmula congruente linear para alterar o valor inicial. O gerador de números aleatórios é inicializado usando uma semente gerada internamente.
Útil em aplicações onde cada thread usa seu próprio conjunto de objetos Random . Útil em aplicativos onde vários threads usam números aleatórios em paralelo em pools de threads.
Esta é uma classe pai. Esta é uma classe filha.