2
votes

Pourquoi les threads Java se comportent-ils si différemment s'ils ne le devraient pas dans ce scénario?

J'ai un problème de sommeil de thread. Dans la méthode d'exécution des threads, j'ai un bloc synchronisé et un temps de sommeil. Chaque thread incrémente ou décrémente la "valeur" de la classe partagée en 5 unités, puis se met en veille.

for(int i=0;i<times;i++) {
    synchronized(shared) {
        aux=shared.getValue()-1;
        shared.setValue(aux);
        System.out.println("Decrement, new value"+shared.getValue());
    }

    try {
        Thread.sleep(sleeptime);
    }catch(Exception e) {
        e.printStackTrace();
    }
}   

class ThreadClass extends Thread{

    Shared shared;
    int times=0;
    int sleeptime=0;
    boolean inc;

    public ThreadClass(Shared shared, int times, int sleeptime, boolean inc) {
        super();
        this.shared = shared;
        this.times = times;
        this.sleeptime = sleeptime;
        this.inc = inc;
    }

    public void run() {

        int aux;

        if(inc) {
            for(int i=0;i<times;i++) {
                synchronized(shared) {
                    aux=shared.getValue()+1;
                    shared.setValue(aux);
                    System.out.println("Increment, new value"+shared.getValue());

                    try {
                        Thread.sleep(sleeptime);
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }   
        }
        else {
            for(int i=0;i<times;i++) {
                synchronized(shared) {
                    aux=shared.getValue()-1;
                    shared.setValue(aux);
                    System.out.println("Decrement, new value"+shared.getValue());

                    try {
                        Thread.sleep(sleeptime);
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }   
        }
    }
}

class Shared{

    int  value=0;

    public Shared(int value) {
        super();
        this.value = value;
    }

    public int getValue() {
        return value;
    }

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

Mais si je déplace le Thread.sleep hors du bloc synchronisé , comme ceci, la sortie est incrémenter, décrémenter, incrémenter, décrémenter. Quand il arrête de dormir et démarre une nouvelle itération de la boucle, l'autre thread ne devrait-il pas essayer d'entrer? à la place, il continue à boucler jusqu'à ce que ce thread soit terminé:

public class borr {

    public static void main(String[] args) {

        int times=5;
        int sleeptime=1000;
        int initial=50;
        Shared shared = new Shared(initial);

        ThreadClass tIncrement = new ThreadClass(shared,times,sleeptime,true);
        ThreadClass tDecrement = new ThreadClass(shared,times,sleeptime,false);
        tIncrement.start();
        tDecrement.start();
    }
}


7 commentaires

Astuce: qu'est-ce que cela signifie pour tout le monde si vous dormez dans un bloc synchronisé?


Lorsque vous dormez en maintenant le verrou synchronisé, personne d'autre ne pourra l'obtenir.


les autres threads ne peuvent pas accéder à la classe partagée mais, quand il arrête de dormir et démarre une nouvelle itération de la boucle, l'autre thread ne devrait-il pas essayer d'entrer? au lieu de cela, il continue en boucle jusqu'à ce que ce fil soit terminé ... c'est ce que je ne comprends pas, ça me rend fou haha


Copie possible de appelant Thread.sleep () à partir d'un contexte synchronisé en Java


C'est essayant, d'accord. Mais au moment où il s'est rendu compte que le verrou est disponible, il a été repris.


N'ajoutez pas de choses comme «(résolu)» aux titres, accepter une réponse est pour cela (et s'il n'y a pas de réponse appropriée, écrivez la vôtre).


mark: désolé, je ne savais pas que


3 Réponses :


1
votes

Mais si je déplace le Thread.sleep hors du bloc synchronisé, comme ceci, la sortie est incrémenter, décrémenter, incrémenter, décrémenter. Le sommeil est toujours à l'intérieur de chaque itération de la boucle, donc, le résultat ne devrait-il pas être le même dans les deux cas?:

quand il arrête de dormir et démarre une nouvelle itération de la boucle, l'autre thread ne devrait-il pas essayer d'entrer.

Ils tous les deux essaient d'entrer.

Et l'autre est déjà en état d'attente (c'est-à-dire qu'il n'est pas actif) car il a déjà essayé d'entrer. Alors que le thread qui vient de libérer le verrou peut fonctionner et récupérer le verrou désormais incontesté.

Ceci est une condition de course. Lorsque les deux threads veulent le verrou en même temps, le système est libre d'en choisir un. Il semble qu'il choisisse celui qui, il y a quelques instructions, vient de le publier. Peut-être que vous pouvez changer cela par yield () ing. Peut être pas. Mais de toute façon, ce n'est pas spécifié / déterministe / juste. Si vous vous souciez de l'ordre d'exécution, vous devez planifier explicitement les choses vous-même.


2 commentaires

yield () n'est-il pas seulement conseillé à JVM sans aucune garantie?


@rkosegi Oui. Vous ne pouvez pas obtenir un comportement spécifié / déterministe / équitable avec yield . Si vous vous souciez de l'ordre d'exécution, vous devez planifier explicitement les choses vous-même.



1
votes

Dans la variante A, vous utilisez deux threads qui ...

  • répéter 5 fois
    • entrez un bloc de synchronisation
      • incrément
      • attendez 1 seconde
  • répéter 5 fois
    • entrez un bloc de synchronisation
      • décrémenter
      • attendez 1 seconde

Dans la variante B, vous utilisez deux threads qui ...

  • répéter 5 fois
    • entrez un bloc de synchronisation
      • incrément
    • attendez 1 seconde
  • répéter 5 fois
    • entrez un bloc de synchronisation
      • décrémenter
    • attendez 1 seconde

Dans la variante A, les deux threads sont actifs (= rester dans un bloc de synchronisation) tout le temps.

Dans la variante B, les deux threads dorment la plupart du temps.

Comme il n'y a absolument aucune garantie sur les threads exécutés ensuite, il n'est pas surprenant que les variantes A et B se comportent si différemment. Alors que dans A les deux threads pourraient - en théorie - être actifs en parallèle, le second thread n'a pas beaucoup de chance d'être actif car ne pas être dans un contexte de synchronisation ne garantit pas qu'un changement de contexte est effectué à ce moment (et un autre thread est exécuté ). Dans la variante B, c'est complètement différent: comme les deux threads dorment la plupart du temps, l'environnement d'exécution n'a aucune autre chance d'exécuter un autre thread pendant que l'un est en veille. Une mise en veille déclenchera le basculement vers un autre thread lorsque la VM essaiera de tirer le meilleur parti des ressources CPU existantes.

Néanmoins: le résultat APRÈS l'exécution des deux threads sera exactement le même. C'est le seul déterminisme sur lequel vous pouvez compter. Tout le reste dépend des détails d'implémentation spécifiques de la manière dont la VM gérera les threads et les blocs de synchronisation et peut même varier d'un OS à l'autre ou d'une implémentation d'une VM à l'autre.


0 commentaires

2
votes

C'est mauvais:

for(...) {
    synchronized(some_lock_object) {
        ...
    }
}

La raison pour laquelle c'est mauvais est que, une fois qu'un thread, A, entre dans cette boucle, puis chaque fois qu'il déverrouille le verrou, La chose suivante c'est de verrouiller à nouveau le verrou.

Si le corps de la boucle prend un certain temps à s'exécuter, alors tout autre thread, B, qui attend le verrou sera placé dans un état d'attente par le système d'exploitation. Chaque fois que le thread A libère le verrou, le thread B commencera à se réveiller, mais le thread A pourra le réacquérir avant que le thread B n'ait une chance.

Ceci est un exemple classique de famine .

Une façon de contourner le problème serait d'utiliser un ReentrantLock avec un politique de classement équitable au lieu d'utiliser un bloc synchronisé . Lorsque les threads se disputent un verrou équitable, le gagnant est toujours celui qui attend le plus longtemps.

Mais, les verrous équitables sont coûteux à implémenter. Une bien meilleure solution est de toujours garder le corps de tout bloc synchronisé aussi court et aussi doux que possible. Habituellement, un thread ne doit pas garder un verrou verrouillé plus longtemps qu'il n'en faut pour affecter un petit nombre de champs à un objet.


0 commentaires