web-dev-qa-db-fra.com

Multithreading Java - Compteur sécurisé des threads

Je commence par un exemple très simple de multithreading. J'essaie de faire un compteur threadsafe. Je veux créer deux threads qui incrémentent le compteur par intermittence pour atteindre 1000. Code ci-dessous:

public class ThreadsExample implements Runnable {
     static int counter = 1; // a global counter

     public ThreadsExample() {
     }

     static synchronized void incrementCounter() {
          System.out.println(Thread.currentThread().getName() + ": " + counter);
          counter++;
     }

     @Override
     public void run() {
          while(counter<1000){
               incrementCounter();
          }
     }

     public static void main(String[] args) {
          ThreadsExample te = new ThreadsExample();
          Thread thread1 = new Thread(te);
          Thread thread2 = new Thread(te);

          thread1.start();
          thread2.start();          
     }
}

D'après ce que je peux dire, la boucle while signifie maintenant que seul le premier thread a accès au compteur jusqu'à ce qu'il atteigne 1000. Résultat:

Thread-0: 1
.
.
.
Thread-0: 999
Thread-1: 1000

Comment puis-je résoudre ce problème? Comment puis-je obtenir les discussions pour partager le compteur?

6
BrotherBarnabas

Les deux threads ont accès à votre variable.

Le phénomène que vous voyez s'appelle la famine de fil. En entrant dans la partie gardée de votre code (désolé d’avoir manqué cela plus tôt), les autres threads devront bloquer jusqu’à ce que le thread contenant le moniteur soit terminé (c’est-à-dire lorsque le moniteur est publié ). Alors que l'on peut s'attendre à ce que le thread actuel transmette le moniteur au prochain thread en attente, Java ne garantit aucune politique d'équité ou de commande à laquelle le prochain thread reçoit le moniteur. Il est tout à fait possible (et même probable) pour un thread qui libère et tente de réacquérir le moniteur de le récupérer via un autre thread qui attend depuis un certain temps.

D'Oracle:

La famine décrit une situation dans laquelle un thread ne peut pas accéder régulièrement à des ressources partagées et ne peut pas progresser. Cela se produit lorsque des ressources partagées sont rendues indisponibles pendant de longues périodes par des threads "gloutons". Par exemple, supposons qu'un objet fournisse une méthode synchronisée dont le retour prend souvent beaucoup de temps. Si un thread appelle fréquemment cette méthode, les autres threads nécessitant également un accès synchronisé fréquent au même objet seront souvent bloqués.

Alors que vos deux threads sont des exemples de threads "gloutons" (puisqu'ils libèrent et réachètent le moniteur de manière répétée), thread-0 est démarré techniquement en premier, ce qui entraîne la mort de thread-1.

La solution consiste à utiliser une méthode de synchronisation simultanée prenant en charge l'équité (par exemple, ReentrantLock), comme indiqué ci-dessous:

public class ThreadsExample implements Runnable {
    static int counter = 1; // a global counter

    static ReentrantLock counterLock = new ReentrantLock(true); // enable fairness policy

    static void incrementCounter(){
        counterLock.lock();

        // Always good practice to enclose locks in a try-finally block
        try{
            System.out.println(Thread.currentThread().getName() + ": " + counter);
            counter++;
        }finally{
             counterLock.unlock();
        }
     }

    @Override
    public void run() {
        while(counter<1000){
            incrementCounter();
        }
    }

    public static void main(String[] args) {
        ThreadsExample te = new ThreadsExample();
        Thread thread1 = new Thread(te);
        Thread thread2 = new Thread(te);

        thread1.start();
        thread2.start();          
    }
}

notez la suppression du mot clé synchronized en faveur de ReentrantLock dans la méthode. Un tel système, avec une politique d'équité, permet aux threads en attente longue de s'exécuter, supprimant ainsi la famine.

13
initramfs

Vous pouvez utiliser la variable AtomicInteger. C'est une classe qui peut être incrémentée de manière atomique, donc deux threads distincts appelant sa méthode d'incrémentation ne s'entrelacent pas.

public class ThreadsExample implements Runnable {
     static AtomicInteger counter = new AtomicInteger(1); // a global counter

     public ThreadsExample() {
     }

     static void incrementCounter() {
          System.out.println(Thread.currentThread().getName() + ": " + counter.getAndIncrement());
     }

     @Override
     public void run() {
          while(counter.get() < 1000){
               incrementCounter();
          }
     }

     public static void main(String[] args) {
          ThreadsExample te = new ThreadsExample();
          Thread thread1 = new Thread(te);
          Thread thread2 = new Thread(te);

          thread1.start();
          thread2.start();          
     }
}
11
Bert Peters

Eh bien, avec votre code, je ne sais pas comment obtenir "exactement" par intermittence, mais si vous utilisez Thread.yield() après l'appel incrementCounter(), vous obtiendrez une meilleure distribution.

public void run() {
         while(counter<1000){
              incrementCounter();
              Thread.yield();

         }
    }

Sinon, pour obtenir ce que vous proposez, vous pouvez créer deux classes de thread différentes (ThreadsExample1 et ThreadsExample2 si vous le souhaitez) et une autre classe sous forme de variable partagée.

public class SharedVariable {
    private int value;
    private boolean turn; //false = ThreadsExample1 --> true = ThreadsExample2

    public SharedVariable (){
        this.value = 0;
        this.turn = false;
    }

    public void set (int v){
        this.value = v;
    }

    public int get (){
        return this.value;
    }

    public void inc (){
        this.value++;
    }

    public void shiftTurn(){
        if (this.turn){
            this.turn=false;
        }else{
            this.turn=true;
        }
    }

    public boolean getTurn(){
        return this.turn;
    }

}

Maintenant, le principal peut être:

public static void main(String[] args) {
        SharedVariable vCom = new SharedVariable();
        ThreadsExample1 hThread1 = new ThreadsExample1 (vCom);
        ThreadsExample2 hThread2 = new ThreadsExample2 (vCom);

        hThread1.start();
        hThread2.start();

        try {
            hThread1.join();
            hThread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

Et vous devez changer votre ligne static int counter = 1; // a global counter Pour private SharedVariable counter;

Et la nouvelle course est:

public void run() {
    for (int i = 0; i < 20; i++) {
        while (!counter.getTurno()){
            Thread.yield();
        }
        System.out.println(this.counter.get());
        this.counter.cambioTurno();
    }
}

}

Oui, c'est un autre code, mais je pense que cela peut vous aider un peu.

1
Shondeslitch

essayez de dormir un fil pour vous assurer que l'autre aura un essai:

     @Override
     public void run() {
          while(counter<1000){
               incrementCounter();
               Thread.sleep(1);
          }
     }
0
almeynman