Dans cette leçon, nous parlerons généralement de l'utilisation de la classe java.lang.ThreadLocal<> et de son utilisation dans un environnement multithread.

La classe ThreadLocal est utilisée pour stocker des variables. Une caractéristique distinctive de cette classe est qu'elle conserve une copie indépendante séparée d'une valeur pour chaque thread qui l'utilise.

En approfondissant le fonctionnement de la classe, nous pouvons imaginer un Map qui mappe les threads aux valeurs, à partir desquelles le thread actuel prend la valeur appropriée lorsqu'il a besoin de l'utiliser.

Constructeur de classe ThreadLocal

Constructeur Action
ThreadLocal() Crée une variable vide en Java

Méthodes

Méthode Action
obtenir() Renvoie la valeur de la variable locale du thread courant
ensemble() Définit la valeur de la variable locale pour le thread actuel
retirer() Supprime la valeur de la variable locale du thread courant
ThreadLocal.withInitial() Méthode d'usine supplémentaire qui définit la valeur initiale

se mettre()

Écrivons un exemple où nous créons deux compteurs. La première, une variable ordinaire, servira à compter le nombre de threads. La seconde, nous envelopperons dans un ThreadLocal . Et nous verrons comment ils fonctionnent ensemble. Tout d'abord, écrivons une classe ThreadDemo qui hérite de Runnable et contient nos données et la très importante méthode run() . Nous ajouterons également une méthode pour afficher les compteurs à l'écran :


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

À chaque exécution de notre classe, nous augmentons lecomptoirappelez la méthode get() pour obtenir les données de la variable ThreadLocal . Si le nouveau thread n'a pas de données, nous le mettrons à 0. S'il y a des données, nous l'augmenterons de un. Et écrivons notre méthode 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();

}

En exécutant notre classe, nous voyons que la variable ThreadLocal reste la même quel que soit le thread qui y accède, mais le nombre de threads augmente.

Compteur : 1
Compteur : 2
Compteur : 3
threadLocalCounter : 0
threadLocalCounter : 0
threadLocalCounter : 0

Processus terminé avec le code de sortie 0

retirer()

Pour comprendre le fonctionnement de la méthode remove , nous allons juste modifier légèrement le code dans la classe ThreadDemo :


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

Dans ce code, si le compteur de thread est un nombre pair, alors nous appellerons la méthode remove() sur notre variable ThreadLocal . Résultat:

Compteur : 3
threadLocalCounter : 0
Compteur : 2
threadLocalCounter : null
Compteur : 1
threadLocalCounter : 0

Processus terminé avec le code de sortie 0

Et ici, nous voyons facilement que la variable ThreadLocal dans le deuxième thread est null .

ThreadLocal.withInitial()

Cette méthode crée une variable locale de thread.

Implémentation de la 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());
    }
}

Et nous pouvons regarder le résultat de notre code :

Compteur : 1
Compteur : 2
Compteur : 3
threadLocalCounter : 1
threadLocalCounter : 1
threadLocalCounter : 1

Processus terminé avec code de sortie 0

Pourquoi utiliser de telles variables ?

ThreadLocal fournit une abstraction sur les variables locales par rapport au thread d'exécution java.lang.Thread .

Les variables ThreadLocal diffèrent des variables ordinaires en ce que chaque thread a sa propre instance initialisée individuellement de la variable, accessible via lesméthodes get() et set() .

Chaque thread, c'est-à-dire une instance de la classe Thread , possède une carte de variables ThreadLocal qui lui est associée. Les clés de la carte sont des références à des objets ThreadLocal et les valeurs sont des références à des variables ThreadLocal "acquises" .

Pourquoi la classe Random n'est-elle pas adaptée à la génération de nombres aléatoires dans les applications multithread ?

Nous utilisons la classe Random pour obtenir des nombres aléatoires. Mais est-ce que cela fonctionne aussi bien dans un environnement multithread ? En fait non. Random n'est pas adapté aux environnements multithreads, car lorsque plusieurs threads accèdent à une classe en même temps, les performances en souffrent.

Pour résoudre ce problème, JDK 7 a introduit la classe java.util.concurrent.ThreadLocalRandom pour générer des nombres aléatoires dans un environnement multithread. Il se compose de deux classes : ThreadLocal et Random .

Les nombres aléatoires reçus par un thread sont indépendants des autres threads, mais java.util.Random fournit des nombres globalement aléatoires. De plus, contrairement à Random , ThreadLocalRandom ne prend pas en charge l'amorçage explicite. Au lieu de cela, il remplace la méthode setSeed() héritée de Random , de sorte qu'il lève toujours une UnsupportedOperationException lorsqu'il est appelé.

Regardons les méthodes de la classe ThreadLocalRandom :

Méthode Action
ThreadLocalRandom actuel() Renvoie le ThreadLocalRandom du thread actuel.
entier suivant (bits entiers) Génère le prochain nombre pseudo-aléatoire.
double nextDouble(double moindre, double lien) Renvoie un nombre pseudo-aléatoire à partir d'une distribution uniforme entre minimum (inclusif) et lié (exclusif).
int nextInt(int moins, int lié) Renvoie un nombre pseudo-aléatoire à partir d'une distribution uniforme entre minimum (inclusif) et lié (exclusif).
long suivantLong(long n) Renvoie un nombre pseudo-aléatoire à partir d'une distribution uniforme entre 0 (inclusif) et la valeur spécifiée (exclusif).
long nextLong(le plus long, le plus long) Renvoie un nombre pseudo-aléatoire à partir d'une distribution uniforme entre minimum (inclusif) et lié (exclusif).
void setSeed (graine longue) Lève UnsupportedOperationException . Ce générateur ne prend pas en charge l'ensemencement.

Obtenir des nombres aléatoires à l'aide de ThreadLocalRandom.current()

ThreadLocalRandom est une combinaison des classes ThreadLocal et Random . Il atteint de meilleures performances dans un environnement multithread en évitant simplement tout accès simultané aux instances de la classe Random .

Implémentons un exemple impliquant plusieurs threads et voyons ce que fait notre application avec 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));
    }
}

Résultat de notre programme :

Temps pris : 1
Thread 17 généré 13
Thread 18 généré 41
Thread 16 généré 99
Thread 19 généré 25
Thread 23 généré 33
Thread 24 généré 21
Thread 15 généré 15
Thread 21 généré 28
Thread 22 généré 97
Thread 20 généré 33

Et maintenant changeons notre classe RandomNumbers et utilisons Random dedans :


int result = new Random().nextInt(bound);
Temps pris : 5
Thread 20 généré 48
Thread 19 généré 57
Thread 18 généré 90
Thread 22 généré 43
Thread 24 généré 7
Thread 23 généré 63
Thread 15 généré 2
Thread 16 généré 40
Thread 17 généré 29
Thread 21 généré 12

Prendre note! Dans nos tests, parfois les résultats étaient les mêmes et parfois ils étaient différents. Mais si nous utilisons plus de threads (disons 100), le résultat ressemblera à ceci :

Aléatoire — 19-25 ms
ThreadLocalRandom — 17-19 ms

Par conséquent, plus il y a de threads dans notre application, plus les performances sont atteintes lors de l'utilisation de la classe Random dans un environnement multithread.

Pour résumer et réitérer les différences entre les classes Random et ThreadLocalRandom :

Aléatoire ThreadLocalRandom
Si différents threads utilisent la même instance de Random , il y aura des conflits et les performances en souffriront. Il n'y a pas de conflits ou de problèmes, car les nombres aléatoires générés sont locaux pour le thread actuel.
Utilise une formule congruentielle linéaire pour modifier la valeur initiale. Le générateur de nombres aléatoires est initialisé à l'aide d'une graine générée en interne.
Utile dans les applications où chaque thread utilise son propre ensemble d' objets aléatoires . Utile dans les applications où plusieurs threads utilisent des nombres aléatoires en parallèle dans des pools de threads.
Il s'agit d'une classe mère. C'est une classe enfant.