Sécurité des threads en Java

La sécurité des threads en Java est un sujet très important. Java fournit un support pour les environnements multi-threadés en utilisant les Threads Java, nous savons que plusieurs threads créés à partir du même objet partagent les variables d’objet et cela peut entraîner une incohérence des données lorsque les threads sont utilisés pour lire et mettre à jour les données partagées.

Sécurité des threads

La raison de l’incohérence des données est que la mise à jour de la valeur de n’importe quel champ n’est pas un processus atomique, cela nécessite trois étapes ; d’abord lire la valeur actuelle, ensuite effectuer les opérations nécessaires pour obtenir la valeur mise à jour et enfin attribuer la valeur mise à jour à la référence du champ. Vérifions cela avec un programme simple où plusieurs threads mettent à jour les données partagées.

package com.journaldev.threads;

public class ThreadSafety {

    public static void main(String[] args) throws InterruptedException {
    
        ProcessingThread pt = new ProcessingThread();
        Thread t1 = new Thread(pt, "t1");
        t1.start();
        Thread t2 = new Thread(pt, "t2");
        t2.start();
        //attendre que les threads aient terminé le traitement
        t1.join();
        t2.join();
        System.out.println("Processing count="+pt.getCount());
    }

}

class ProcessingThread implements Runnable{
    private int count;
    
    @Override
    public void run() {
        for(int i=1; i < 5; i++){
            processSomething(i);
        	count++;
        }
    }

    public int getCount() {
        return this.count;
    }

    private void processSomething(int i) {
        // traitement d'un travail quelconque
        try {
            Thread.sleep(i*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

Dans le programme ci-dessus, la boucle for, count est incrémenté de 1 quatre fois et comme nous avons deux threads, sa valeur devrait être de 8 après que les deux threads ont fini d’exécuter. Mais lorsque vous exécutez le programme ci-dessus plusieurs fois, vous remarquerez que la valeur de count varie entre 6, 7, 8. Cela se produit parce que même si count++ semble être une opération atomique, elle ne l’est PAS et cause une corruption des données.

Sûreté du thread en Java

La sûreté du thread en Java est le processus permettant de rendre notre programme sécurisé pour une utilisation en environnement multithread. Il existe différentes façons de rendre notre programme thread-safe.

  • La synchronisation est l’outil le plus simple et le plus largement utilisé pour assurer la sûreté du thread en Java.
  • Utilisation des classes d’enveloppe atomique du package java.util.concurrent.atomic. Par exemple, AtomicInteger
  • Utilisation de verrous du package java.util.concurrent.locks.
  • Utilisation de classes de collection thread-safe, consultez cet article pour l’utilisation de ConcurrentHashMap pour assurer la sûreté du thread.
  • Utilisation du mot-clé volatile avec des variables pour faire en sorte que chaque thread lise les données depuis la mémoire, et non depuis le cache du thread.

Java synchronisé

La synchronisation est l’outil grâce auquel nous pouvons atteindre la sûreté du thread. JVM garantit que le code synchronisé sera exécuté par un seul thread à la fois. Le mot-clé synchronisé de Java est utilisé pour créer du code synchronisé, et il utilise internement des verrous sur l’objet ou la classe pour s’assurer qu’un seul thread exécute le code synchronisé.

  • La synchronisation Java fonctionne sur le verrouillage et le déverrouillage de la ressource avant que n’importe quel thread n’entre dans le code synchronisé. Il doit acquérir le verrou sur l’objet et, lorsque l’exécution du code se termine, il libère la ressource qui peut être verrouillée par d’autres threads. Pendant ce temps, d’autres threads sont en attente pour verrouiller la ressource synchronisée.
  • Nous pouvons utiliser le mot-clé synchronized de deux manières : en rendant une méthode complètement synchronisée ou en créant un bloc synchronisé.
  • Lorsqu’une méthode est synchronisée, elle verrouille l’Object ; si la méthode est statique, elle verrouille la Classe. Il est toujours préférable d’utiliser le bloc synchronisé pour verrouiller uniquement les sections de la méthode qui nécessitent une synchronisation.
  • En créant un bloc synchronisé, nous devons spécifier la ressource sur laquelle le verrou sera acquis, cela peut être XYZ.class ou n’importe quel champ d’objet de la classe.
  • synchronized(this) verrouillera l’objet avant d’entrer dans le bloc synchronisé.
  • Vous devriez utiliser le niveau de verrouillage le plus bas, par exemple, s’il y a plusieurs blocs synchronisés dans une classe et que l’un d’eux verrouille l’objet, les autres blocs synchronisés ne seront pas disponibles pour l’exécution par d’autres threads. Lorsque nous verrouillons un objet, il acquiert un verrou sur tous les champs de l’objet.
  • La synchronisation Java assure l’intégrité des données au prix des performances, elle devrait donc être utilisée uniquement lorsque c’est absolument nécessaire.
  • La synchronisation Java ne fonctionne que dans le même JVM, donc si vous devez verrouiller une ressource dans un environnement avec plusieurs JVM, cela ne fonctionnera pas et vous devrez envisager un mécanisme de verrouillage global.
  • La synchronisation Java pourrait entraîner des impasses, consultez ce post sur les impasses en Java et comment les éviter.
  • Le mot-clé synchronized Java ne peut pas être utilisé pour les constructeurs et les variables.
  • Il est préférable de créer un objet privé factice à utiliser pour le bloc synchronisé afin que sa référence ne puisse pas être modifiée par un autre code. Par exemple, si vous avez une méthode setter pour un objet sur lequel vous synchronisez, sa référence peut être modifiée par un autre code, ce qui entraîne l’exécution parallèle du bloc synchronisé.
  • Nous ne devrions pas utiliser d’objet qui est maintenu dans un pool constant, par exemple String ne doit pas être utilisé pour la synchronisation car si un autre code verrouille également la même String, il tentera d’acquérir le verrou sur le même objet de référence du pool de chaînes et même si les deux codes sont sans rapport, ils se verrouilleront mutuellement.

Voici les modifications de code que nous devons apporter dans le programme ci-dessus pour le rendre thread-safe.

    // variable d'objet factice pour la synchronisation
    private Object mutex=new Object();
    ...
    // utilisation du bloc synchronisé pour lire, incrémenter et mettre à jour la valeur du compteur de manière synchrone
    synchronized (mutex) {
            count++;
    }

Voyons quelques exemples de synchronisation et ce que nous pouvons en apprendre.

public class MyObject {
 
  // Verrouille sur le moniteur de l'objet
  public synchronized void doSomething() { 
    // ...
  }
}
 
// Code des pirates
MyObject myObject = new MyObject();
synchronized (myObject) {
  while (true) {
    // Retard indéfini de myObject
    Thread.sleep(Integer.MAX_VALUE); 
  }
}

Remarquez que le code du pirate tente de verrouiller l’instance de myObject et une fois qu’il obtient le verrou, il ne le libère jamais, ce qui provoque le blocage de la méthode doSomething() en attendant le verrouillage, cela entraînera le système dans une impasse et causera un déni de service (DoS).

public class MyObject {
  public Object lock = new Object();
 
  public void doSomething() {
    synchronized (lock) {
      // ...
    }
  }
}

//code non fiable

MyObject myObject = new MyObject();
//changer la référence de l'objet de verrouillage
myObject.lock = new Object();

Remarquez que l’objet de verrouillage est public et en changeant sa référence, nous pouvons exécuter un bloc synchronisé en parallèle dans plusieurs threads. Un cas similaire est vrai si vous avez un objet privé mais avez une méthode setter pour changer sa référence.

public class MyObject {
  //verrouille le moniteur de l'objet de la classe
  public static synchronized void doSomething() { 
    // ...
  }
}
 
//code des pirates
synchronized (MyObject.class) {
  while (true) {
    Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
  }
}

Remarquez que le code pirate obtient un verrou sur le moniteur de la classe et ne le libère pas, ce qui causera un blocage et un DoS dans le système. Voici un autre exemple où plusieurs threads travaillent sur le même tableau de chaînes et une fois traitées, ajoutent le nom du thread à la valeur du tableau.

package com.journaldev.threads;

import java.util.Arrays;

public class SyncronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        String[] arr = {"1","2","3","4","5","6"};
        HashMapProcessor hmp = new HashMapProcessor(arr);
        Thread t1=new Thread(hmp, "t1");
        Thread t2=new Thread(hmp, "t2");
        Thread t3=new Thread(hmp, "t3");
        long start = System.currentTimeMillis();
        //démarrer tous les threads
        t1.start();t2.start();t3.start();
        //attendre que les threads finissent
        t1.join();t2.join();t3.join();
        System.out.println("Time taken= "+(System.currentTimeMillis()-start));
        //vérifier la valeur de la variable partagée maintenant
        System.out.println(Arrays.asList(hmp.getMap()));
    }

}

class HashMapProcessor implements Runnable{
    
    private String[] strArr = null;
    
    public HashMapProcessor(String[] m){
        this.strArr=m;
    }
    
    public String[] getMap() {
        return strArr;
    }

    @Override
    public void run() {
        processArr(Thread.currentThread().getName());
    }

    private void processArr(String name) {
        for(int i=0; i < strArr.length; i++){
            //traiter les données et ajouter le nom du thread
            processSomething(i);
            addThreadName(i, name);
        }
    }
    
    private void addThreadName(int i, String name) {
        strArr[i] = strArr[i] +":"+name;
    }

    private void processSomething(int index) {
        //traiter un travail quelconque
        try {
            Thread.sleep(index*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

Voici la sortie lorsque j’exécute le programme ci-dessus.

Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]

Les valeurs du tableau de chaînes sont corrompues en raison des données partagées et de l’absence de synchronisation. Voici comment nous pouvons modifier la méthode addThreadName() pour rendre notre programme sûr pour les threads.

    private Object lock = new Object();
    private void addThreadName(int i, String name) {
        synchronized(lock){
        strArr[i] = strArr[i] +":"+name;
        }
    }

Après ce changement, notre programme fonctionne bien et voici la sortie correcte du programme.

Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]

{
« error »: « Upstream error… »
}

Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java