37 votes

La synchronisation Java ne fonctionne pas comme prévu

J'ai un "simple" 4 exemple de classe de manière fiable que montre un comportement inattendu de java synchronisation sur plusieurs machines. Comme vous pouvez le lire ci-dessous, étant donné que le contrat de java sychronized mot-clé, Broke Synchronization ne doit jamais être imprimé à partir de la classe TestBuffer.

Voici les 4 classes qui vont reproduire le problème (au moins pour moi). Je ne suis pas intéressé par la façon de corriger ce mauvais exemple, mais plutôt pourquoi il se casse en premier lieu.

Le Problème De Synchronisation - Controller.java

Le Problème De Synchronisation - SyncTest.java

Le Problème De Synchronisation - TestBuffer.java

Le Problème De Synchronisation - Tuple3f.java

Et voici le résultat que j'obtiens quand je le lance:

java -cp . SyncTest
Before Adding
Creating a TestBuffer
Before Remove
Broke Synchronization
1365192
Broke Synchronization
1365193
Broke Synchronization
1365194
Broke Synchronization
1365195
Broke Synchronization
1365196
Done

Mise à JOUR: @Gray est l'exemple le plus simple que les pauses jusqu'à présent. Son exemple peut être trouvé ici: Étrange JRC Condition de Course

Basé sur les commentaires que j'ai obtenu d'autres, il semble que le problème peut se produire sur Java 64 bits 1.6.0_20-1.6.0_31 (pas sûr parler de nouvelle 1.6.0) sur Windows et OSX. Personne n'a été en mesure de reproduire le problème sur Java 7. Il peut aussi demander un multi-core de la machine à reproduire le problème.

QUESTION DE DÉPART:

J'ai une classe qui fournit les méthodes suivantes:

  • supprimer - Supprime l'élément donné de la liste
  • getBuffer - Itère sur tous les éléments de la liste

J'ai réduit le problème à l'2 fonctions ci-dessous, qui sont tous deux dans le même objet, et ils sont tous les deux synchronized. Sauf erreur de ma part, "Cassé la Synchronisation" ne devrait jamais être imprimé car insideGetBuffer doivent toujours être mis dos à faux avant remove peuvent être saisis. Cependant, dans mon application, c'est l'impression de "Fait de la Synchronisation" lorsque j'ai 1 thread appelant supprimer à plusieurs reprises tandis que l'autre appelle getBuffer à plusieurs reprises. Le problème est que je reçois ConcurrentModificationException.

Voir Aussi:

Très étrange condition de course qui ressemble à un JRE problème

Soleil De Rapport De Bug:

Cela a été confirmé comme un bug en Java par Sun. Il est apparemment résolu (sans le savoir?) dans jdk7u4, mais ils n'ont pas intégré le fixer à jdk6. Bug ID: 7176993

17voto

philwb Points 2847

Je pense que vous êtes bien à la recherche à une JVM bug dans le DSO. En utilisant le programme simplifié de @Gray (de légères modifications pour imprimer un message d'erreur) et quelques options à jouer avec l'/imprimer la compilation JIT, vous pouvez voir ce qui se passe avec le JIT. Et, vous pouvez utiliser certaines options de contrôle à un degré qui peut supprimer la question, ce qui lui donne beaucoup de preuves à ce que cela soit une JVM bug.

Fonctionnant comme:

java -XX:+PrintCompilation -XX:CompileThreshold=10000 phil.StrangeRaceConditionTest

vous pouvez obtenir une condition d'erreur (comme les autres d'environ 80% de l'est) et la compilation d'impression un peu comme:

 68   1       java.lang.String::hashCode (64 bytes)
 97   2       sun.nio.cs.UTF_8$Decoder::decodeArrayLoop (553 bytes)
104   3       java.math.BigInteger::mulAdd (81 bytes)
106   4       java.math.BigInteger::multiplyToLen (219 bytes)
111   5       java.math.BigInteger::addOne (77 bytes)
113   6       java.math.BigInteger::squareToLen (172 bytes)
114   7       java.math.BigInteger::primitiveLeftShift (79 bytes)
116   1%      java.math.BigInteger::multiplyToLen @ 138 (219 bytes)
121   8       java.math.BigInteger::montReduce (99 bytes)
126   9       sun.security.provider.SHA::implCompress (491 bytes)
138  10       java.lang.String::charAt (33 bytes)
139  11       java.util.ArrayList::ensureCapacity (58 bytes)
139  12       java.util.ArrayList::add (29 bytes)
139   2%      phil.StrangeRaceConditionTest$Buffer::<init> @ 38 (62 bytes)
158  13       java.util.HashMap::indexFor (6 bytes)
159  14       java.util.HashMap::hash (23 bytes)
159  15       java.util.HashMap::get (79 bytes)
159  16       java.lang.Integer::valueOf (32 bytes)
168  17 s     phil.StrangeRaceConditionTest::getBuffer (66 bytes)
168  18 s     phil.StrangeRaceConditionTest::remove (10 bytes)
171  19 s     phil.StrangeRaceConditionTest$Buffer::remove (34 bytes)
172   3%      phil.StrangeRaceConditionTest::strangeRaceConditionTest @ 36 (76 bytes)
ERRORS //my little change
219  15      made not entrant  java.util.HashMap::get (79 bytes)

Il y a trois OSR remplacements (ceux avec le % d'annotation sur la compilation ID). Ma conjecture est que c'est la troisième, qui est la boucle d'appel remove(), qui est responsable de l'erreur. Cela peut être exclu de JIT par un .hotspot_compiler fichier situé dans le répertoire de travail avec le contenu suivant:

exclude phil/StrangeRaceConditionTest strangeRaceConditionTest

Lorsque vous exécutez à nouveau le programme, vous obtenez ce résultat:

CompilerOracle: exclude phil/StrangeRaceConditionTest.strangeRaceConditionTest
 73   1       java.lang.String::hashCode (64 bytes)
104   2       sun.nio.cs.UTF_8$Decoder::decodeArrayLoop (553 bytes)
110   3       java.math.BigInteger::mulAdd (81 bytes)
113   4       java.math.BigInteger::multiplyToLen (219 bytes)
118   5       java.math.BigInteger::addOne (77 bytes)
120   6       java.math.BigInteger::squareToLen (172 bytes)
121   7       java.math.BigInteger::primitiveLeftShift (79 bytes)
123   1%      java.math.BigInteger::multiplyToLen @ 138 (219 bytes)
128   8       java.math.BigInteger::montReduce (99 bytes)
133   9       sun.security.provider.SHA::implCompress (491 bytes)
145  10       java.lang.String::charAt (33 bytes)
145  11       java.util.ArrayList::ensureCapacity (58 bytes)
146  12       java.util.ArrayList::add (29 bytes)
146   2%      phil.StrangeRaceConditionTest$Buffer::<init> @ 38 (62 bytes)
165  13       java.util.HashMap::indexFor (6 bytes)
165  14       java.util.HashMap::hash (23 bytes)
165  15       java.util.HashMap::get (79 bytes)
166  16       java.lang.Integer::valueOf (32 bytes)
174  17 s     phil.StrangeRaceConditionTest::getBuffer (66 bytes)
174  18 s     phil.StrangeRaceConditionTest::remove (10 bytes)
### Excluding compile: phil.StrangeRaceConditionTest::strangeRaceConditionTest
177  19 s     phil.StrangeRaceConditionTest$Buffer::remove (34 bytes)
324  15      made not entrant  java.util.HashMap::get (79 bytes)

et le problème ne semble pas (du moins pas dans les tentatives répétées que j'ai fait).

Aussi, si vous modifiez les options de la JVM un peu, vous pouvez provoquer le problème de s'en aller. À l'aide de la suite, je ne peut pas obtenir le problème apparaître.

java -XX:+PrintCompilation -XX:CompileThreshold=100000 phil.StrangeRaceConditionTest
java -XX:+PrintCompilation -XX:FreqInlineSize=1 phil.StrangeRaceConditionTest

Fait intéressant, la compilation de sortie pour ces deux montrent encore l'OSR pour la suppression de la boucle. Je pense (et c'en est un) est que le report de l'JIT par la compilation de seuil ou de la modification de la FreqInlineSize provoquer des changements dans le DSO de traitement dans ces cas que le contournement d'un bug qui vous sont autrement frapper.

Voir ici pour plus d'informations sur les options de la JVM.

Voir ici et ici pour plus d'informations sur la sortie de l'-XX:+PrintCompilation et comment gâcher, avec ce que l'équipe n'.

10voto

Gray Points 58585

Donc, d'après le code que vous avez posté, vous ne serait jamais Broke Synchronization imprimé à moins d' getBuffer() déclenche une exception entre l' true et false réglage. Voir un meilleur modèle ci-dessous.

Edit:

J'ai pris @Luc code et taillé au couteau vers le bas pour ce pastebin classe. Comme je le vois, @Luc est de frapper une JRE de synchronisation de bug. Je sais que c'est dur à croire, mais j'ai regardé le code et je ne peux pas voir le problème.


Puisque vous parlez ConcurrentModificationException, je soupçonne qu' getBuffer() est de le jeter quand il effectue une itération sur l' list. Le code que vous avez posté ne devrait jamais jeter un ConcurrentModificationException en raison de la synchronisation, mais je soupçonne que du code supplémentaire de l'appelant add ou remove c'est pas synchronisé, ou vous déposez alors que vous êtes une itération à travers l' list. La seule façon vous pouvez modifier les nations unies synchronisé collection, tandis que vous êtes une itération à travers c'est par l' Iterator.remove() méthode:

Iterator<Object> iterator = list.iterator();
while (iterator.hasNext()) {
   ...
   // it is ok to remove from the list this way while iterating
   iterator.remove();
}

Pour protéger votre drapeau, Être tour à utiliser try/finally lorsque vous définissez une critique booléenne comme ça. Ensuite, toute exception à restaurer l' insideGetBuffer correctement:

synchronized public Object getBuffer() {
    insideGetBuffer = true;
    try {
        int i=0;
        for(Object item : list) {
            i++;
        }
    } finally {
        insideGetBuffer = false;
    }
    return null;
}

Aussi, il est un meilleur modèle pour synchroniser autour d'un objet au lieu de l'aide de la méthode de synchronisation. Si vous essayez de protéger l' list, puis l'ajout de la synchronisation autour de cette liste à chaque fois serait mieux.n

 synchronized (list) {
    list.remove();
 }

Vous pouvez également activer votre liste dans un synchronisé liste que vous n'aurez pas à l' synchronize à chaque fois:

 List<Object> list = Collections.synchronizedList(new ArrayList<Object>());

4voto

John Vint Points 19804

Basé sur ce code il y a seulement deux façons de "Cassé la Synchronisation" à l'impression.

  1. Ils sont la synchronisation sur les différents objets (dont vous dites qu'ils ne le sont pas)
  2. L' insideGetBuffer est en cours de modification par un autre thread en dehors de la synchronisation de bloc.

Sans ces deux-là ne peut pas être une façon que le code de la liste d'impression "Cassé la Synchronisation" & la ConcurrentModificationException. Pouvez-vous donner un petit extrait de code qui peut être exécuté à prouver ce que vous dites?

Mise à jour:

Je suis allé à travers l'exemple de Luc posté et je suis bizarre de voir des comportements sur Java 1.6_24-64 bits de Windows. La même instance de TestBuffer et la valeur de l' insideGetBuffer est "alternatif" à l'intérieur de la méthode remove. Remarque le champ n'est pas mis à jour en dehors d'un synchronisé de la région. Il y a un seul TestBuffer instance, mais admettons qu'ils ne sont pas - insideGetBuffer ne serait jamais la valeur vrai (donc ça doit être la même instance).

    synchronized public void remove(Object item) {

            boolean b = insideGetBuffer;
            if(insideGetBuffer){
                    System.out.println("Broke Synchronization : " +  b + " - " + insideGetBuffer);
            }
    }

Parfois, il imprime Broke Synchronization : true - false

Je suis en train de travailler sur l'obtention de l'assembleur pour fonctionner sur Windows 64 bits de Java.

2voto

JB Nizet Points 250258

La plupart du temps, une exception ConcurrentModificationException n'est pas provoquée par des threads simultanés. Cela est dû à la modification de la collection pendant son itération:

 for (Object item : list) {
    if (someCondition) {
         list.remove(item);
    }
}
 

Le code ci-dessus provoquerait une exception ConcurrentModificationException si une condition est vraie. En itérant, la collection ne peut être modifiée que par les méthodes de l'itérateur:

 for (Iterator<Object> it = list.iterator(); it.hasNext(); ) {
    Object item = it.next();
    if (someCondition) {
         it.remove();
    }
}
 

Je soupçonne que c'est ce qui se passe dans votre vrai code. Le code affiché est correct.

2voto

Peter Lawrey Points 229686

Pouvez-vous essayer ce code, qui est un autonome test?

public static class TestBuffer {
    private final List<Object> list = new ArrayList<Object>();
    private boolean insideGetBuffer = false;

    public TestBuffer() {
        System.out.println("Creating a TestBuffer");
    }

    synchronized public void add(Object item) {
        list.add(item);
    }

    synchronized public void remove(Object item) {
        if (insideGetBuffer) {
            System.out.println("Broke Synchronization ");
            System.out.println(item);
        }

        list.remove(item);
    }

    synchronized public void getBuffer() {
        insideGetBuffer = true;
//      System.out.println("getBuffer.");
        try {
            int count = 0;
            for (int i = 0, listSize = list.size(); i < listSize; i++) {
                if (list.get(i) != null)
                    count++;
            }
        } finally {
//          System.out.println(".getBuffer");
            insideGetBuffer = false;
        }
    }
}

public static void main(String... args) throws IOException {
    final TestBuffer tb = new TestBuffer();
    ExecutorService service = Executors.newCachedThreadPool();
    final AtomicLong count = new AtomicLong();
    for (int i = 0; i < 16; i++) {
        final int finalI = i;
        service.submit(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    for (int j = 0; j < 1000000; j++) {
                        tb.add(finalI);
                        tb.getBuffer();
                        tb.remove(finalI);
                    }
                    System.out.printf("%d,: %,d%n", finalI, count.addAndGet(1000000));
                }
            }
        });
    }
}

imprime

Creating a TestBuffer
11,: 1,000,000
2,: 2,000,000
... many deleted ...
2,: 100,000,000
1,: 101,000,000

En regardant votre trace de la pile de façon plus détaillée.

Caused by: java.util.ConcurrentModificationException
    at java.util.HashMap$HashIterator.nextEntry(Unknown Source)
    at java.util.HashMap$KeyIterator.next(Unknown Source)
    at <removed>.getBuffer(<removed>.java:62)

Vous pouvez voir que vous accédez à l'ensemble des clés d'une table de hachage, et non une liste. Ceci est important parce que l'ensemble des clés est une vue sur la carte sous-jacente. Cela signifie que vous devez vous assurer que tous les accès à cette carte est également protégée par la même serrure. par exemple, disons que vous avez un setter comme

Collection list;
public void setList(Collection list) { this.list = list; }


// somewhere else
Map map = new HashMap();
obj.setList(map.keySet());

// "list" is accessed in another thread which is locked by this thread does this
map.put("hello", "world");
// now an Iterator in another thread on list is invalid.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X