In questa lezione parleremo in generale dell'utilizzo della classe java.lang.ThreadLocal<> e di come utilizzarla in un ambiente multithread.

La classe ThreadLocal viene utilizzata per memorizzare le variabili. Una caratteristica distintiva di questa classe è che mantiene una copia indipendente separata di un valore per ogni thread che lo utilizza.

Approfondendo il funzionamento della classe, possiamo immaginare una mappa che associa i thread ai valori, da cui il thread corrente prende il valore appropriato quando ha bisogno di usarlo.

Costruttore di classe ThreadLocal

Costruttore Azione
ThreadLocal() Crea una variabile vuota in Java

Metodi

Metodo Azione
Ottenere() Restituisce il valore della variabile locale del thread corrente
impostato() Imposta il valore della variabile locale per il thread corrente
rimuovere() Rimuove il valore della variabile locale del thread corrente
ThreadLocal.withInitial() Metodo factory aggiuntivo che imposta il valore iniziale

preparatevi()

Scriviamo un esempio in cui creiamo due contatori. La prima, una variabile ordinaria, servirà per contare il numero di thread. Il secondo lo avvolgeremo in un ThreadLocal . E vedremo come lavorano insieme. Innanzitutto, scriviamo una classe ThreadDemo che erediti Runnable e contenga i nostri dati e l'importantissimo metodo run() . Aggiungeremo anche un metodo per visualizzare i contatori sullo schermo:


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());
    }
}

Con ogni corsa della nostra classe, aumentiamo ilcontatorevariable chiama il metodo get() per ottenere i dati dalla variabile ThreadLocal . Se il nuovo thread non ha dati, lo imposteremo su 0. Se ci sono dati, lo aumenteremo di uno. E scriviamo il nostro metodo principale :


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();

}

Eseguendo la nostra classe, vediamo che la variabile ThreadLocal rimane la stessa indipendentemente dal thread che vi accede, ma il numero di thread cresce.

Contatore: 1
Contatore: 2
Contatore: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

Processo terminato con codice di uscita 0

rimuovere()

Per capire come funziona il metodo remove , cambieremo solo leggermente il codice nella classe ThreadDemo :


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

In questo codice, se il contatore di thread è un numero pari, chiameremo il metodo remove() sulla nostra variabile ThreadLocal . Risultato:

Contatore: 3
threadLocalCounter: 0
Contatore: 2
threadLocalCounter: null
Contatore: 1
threadLocalCounter: 0

Processo terminato con codice di uscita 0

E qui vediamo facilmente che la variabile ThreadLocal nel secondo thread è null .

ThreadLocal.withInitial()

Questo metodo crea una variabile locale del thread.

Implementazione della 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 possiamo guardare il risultato del nostro codice:

Contatore: 1
Contatore: 2
Contatore: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

Processo terminato con codice di uscita 0

Perché dovremmo usare tali variabili?

ThreadLocal fornisce un'astrazione sulle variabili locali in relazione al thread di esecuzione java.lang.Thread .

Le variabili ThreadLocal differiscono da quelle ordinarie in quanto ogni thread ha la propria istanza della variabile inizializzata individualmente, a cui si accede tramite imetodi get() e set() .

Ad ogni thread, cioè istanza della classe Thread , è associata una mappa di variabili ThreadLocal . Le chiavi della mappa sono riferimenti a oggetti ThreadLocal e i valori sono riferimenti a variabili ThreadLocal "acquisite" .

Perché la classe Random non è adatta per generare numeri casuali in applicazioni multithread?

Usiamo la classe Random per ottenere numeri casuali. Ma funziona altrettanto bene in un ambiente multithread? In realtà no. Random non è adatto per ambienti multithread, perché quando più thread accedono a una classe contemporaneamente, le prestazioni ne risentono.

Per risolvere questo problema, JDK 7 ha introdotto la classe java.util.concurrent.ThreadLocalRandom per generare numeri casuali in un ambiente multithread. Consiste di due classi: ThreadLocal e Random .

I numeri casuali ricevuti da un thread sono indipendenti da altri thread, ma java.util.Random fornisce numeri casuali globali. Inoltre, a differenza di Random , ThreadLocalRandom non supporta il seeding esplicito. Al contrario, sovrascrive il metodo setSeed() ereditato da Random , in modo che generi sempre un'eccezione UnsupportedOperationException quando viene chiamato.

Diamo un'occhiata ai metodi della classe ThreadLocalRandom :

Metodo Azione
ThreadLocalRandom corrente() Restituisce il ThreadLocalRandom del thread corrente.
int successivo(int bit) Genera il prossimo numero pseudo-casuale.
double nextDouble(doppio minimo, doppio limite) Restituisce un numero pseudocasuale da una distribuzione uniforme tra minimo (incluso) e limite (escluso).
int nextInt(int minimo, int limitato) Restituisce un numero pseudocasuale da una distribuzione uniforme tra minimo (incluso) e limite (escluso).
lungo successivo Lungo (lungo n) Restituisce un numero pseudocasuale da una distribuzione uniforme compresa tra 0 (incluso) e il valore specificato (escluso).
long nextLong(lungo minimo, lungo limite) Restituisce un numero pseudocasuale da una distribuzione uniforme tra minimo (incluso) e limite (escluso).
void setSeme(seme lungo) Genera UnsupportedOperationException . Questo generatore non supporta il seeding.

Ottenere numeri casuali usando ThreadLocalRandom.current()

ThreadLocalRandom è una combinazione delle classi ThreadLocal e Random . Raggiunge prestazioni migliori in un ambiente multithread semplicemente evitando qualsiasi accesso simultaneo alle istanze della classe Random .

Implementiamo un esempio che coinvolge più thread e vediamo la nostra applicazione con la 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));
    }
}

Risultato del nostro programma:

Tempo impiegato: 1
Thread 17 generato 13
Thread 18 generato 41
Thread 16 generato 99
Thread 19 generato 25
Thread 23 generato 33
Thread 24 generato 21
Thread 15 generato 15
Thread 21 generato 28
Thread 22 generato 97
Thread 20 generato 33

E ora cambiamo la nostra classe RandomNumbers e usiamo Random in essa:


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

Prendi nota! Nei nostri test, a volte i risultati erano gli stessi ea volte erano diversi. Ma se usiamo più thread (diciamo, 100), il risultato sarà simile a questo:

Casuale — 19-25 ms
ThreadLocalRandom — 17-19 ms

Di conseguenza, maggiore è il numero di thread nella nostra applicazione, maggiore sarà il calo delle prestazioni quando si utilizza la classe Random in un ambiente multithread.

Per riassumere e ribadire le differenze tra le classi Random e ThreadLocalRandom :

Casuale ThreadLocaleCasuale
Se thread diversi utilizzano la stessa istanza di Random , si verificheranno conflitti e le prestazioni ne risentiranno. Non ci sono conflitti o problemi, perché i numeri casuali generati sono locali al thread corrente.
Utilizza una formula congruenziale lineare per modificare il valore iniziale. Il generatore di numeri casuali viene inizializzato utilizzando un seme generato internamente.
Utile nelle applicazioni in cui ogni thread utilizza il proprio set di oggetti Random . Utile nelle applicazioni in cui più thread utilizzano numeri casuali in parallelo nei pool di thread.
Questa è una classe genitore. Questa è una classe per bambini.