Vraag Java-synchronisatie werkt niet zoals verwacht


Ik heb een "eenvoudig" 4-klassevoorbeeld dat betrouwbaar onverwacht gedrag laat zien van java-synchronisatie op meerdere machines. Zoals je hieronder kunt lezen, gezien het contract van de Java sychronized trefwoord, Broke Synchronization mag nooit worden afgedrukt vanuit de klasse TestBuffer.

Hier zijn de 4 klassen die het probleem zullen reproduceren (tenminste voor mij). Ik ben niet geïnteresseerd in hoe dit gebroken voorbeeld te repareren, maar eerder waarom het breekt in de eerste plaats.

Synchronisatieprobleem - Controller.java

Synchronisatieprobleem - SyncTest.java

Synchronisatieprobleem - TestBuffer.java

Synchronisatieprobleem - Tuple3f.java 

En hier is de uitvoer die ik krijg als ik het uitvoer:

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

BIJWERKEN: @Gray heeft het eenvoudigste voorbeeld dat tot nu toe breekt. Zijn voorbeeld is hier te vinden: Vreemde JRC raceconditie

Op basis van de feedback die ik van anderen heb gekregen, lijkt het erop dat het probleem kan optreden op Java 64-bit 1.6.0_20-1.6.0_31 (niet zeker over nieuwere 1.6.0's) op Windows en OSX. Niemand heeft het probleem kunnen reproduceren op Java 7. Het kan ook een multi-core machine nodig hebben om het probleem te reproduceren.

ORIGINELE VRAAG:

Ik heb een klasse die de volgende methoden biedt:

  • verwijderen - Verwijdert het gegeven item uit de lijst
  • getBuffer - Herhaalt alle items in de lijst

Ik heb het probleem gereduceerd tot de 2 onderstaande functies, die beide zich in hetzelfde object bevinden en beide zijn synchronized. Tenzij ik me vergis, zou "Broke Synchronization" nooit mogen worden afgedrukt omdat insideGetBuffer moet altijd worden teruggezet naar false before remove kan worden ingevoerd. In mijn toepassing drukt het echter op "Brak synchronisatie" wanneer ik 1 thread-roeping herhaaldelijk verwijder terwijl de andere oproepen herhaaldelijk getBuffer worden. Het symptoom is dat ik een ConcurrentModificationException.

Zie ook:

Zeer vreemde raceconditie die op een JRE-probleem lijkt

Sun Bug Report:

Dit werd door Sun bevestigd als een fout in Java. Het staat blijkbaar vast (onbewust?) In jdk7u4, maar ze hebben de fix niet naar jdk6 teruggestuurd. Bug-ID: 7176993


37
2018-06-11 15:16


oorsprong


antwoorden:


Ik denk dat je inderdaad naar een JVM-bug in de OSR kijkt. Gebruik makend van het vereenvoudigde programma van @Gray (kleine aanpassingen om een ​​foutmelding af te drukken) en enkele opties om met JIT compilatie te knoeien / printen, kunt u zien wat er met de JIT aan de hand is. En je kunt enkele opties gebruiken om dat te beheersen tot een niveau dat het probleem kan onderdrukken, wat heel wat bewijsmateriaal oplevert voor dit als een JVM-bug.

Wordt uitgevoerd als:

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

je kunt een foutmelding krijgen (net als anderen ongeveer 80% van de runs) en de compilatieprint enigszins als:

 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)

Er zijn drie OSR-vervangingen (degenen met de% annotatie op de compilatie-ID). Mijn gok is dat het de derde is, dat is de loop calling remove (), die verantwoordelijk is voor de fout. Dit kan worden uitgesloten van JIT via een .hotspot_compiler-bestand in de werkdirectory met de volgende inhoud:

exclude phil/StrangeRaceConditionTest strangeRaceConditionTest

Wanneer u het programma opnieuw uitvoert, krijgt u deze uitvoer:

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)

en het probleem verschijnt niet (althans niet bij de herhaalde pogingen die ik heb gedaan).

Als u de JVM-opties een beetje wijzigt, kunt u het probleem laten verdwijnen. Met behulp van een van de volgende kan ik het probleem niet laten verschijnen.

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

Interessant is dat de compilatie-uitvoer voor beide nog steeds de OSR voor de verwijderingslus toont. Mijn gok (en het is een grote) is dat het uitstellen van de JIT via de compilatiedrempel of het veranderen van de FreqInlineSize oorzaak is voor wijzigingen in de OSR-verwerking in deze gevallen die een fout omzeilen die je anders raakt.

Zien hier voor informatie over de JVM-opties.

Zien hier en hier voor informatie over de uitvoer van -XX: + PrintCompilation en hoe u kunt knoeien met wat de JIT doet.


17
2018-06-15 17:12



Dus volgens de code die je hebt gepost, zou je nooit krijgen Broke Synchronization afgedrukt tenzij getBuffer() werpt een uitzondering tussen de true en false setting. Zie hieronder een beter patroon.

Bewerk:

Ik nam de code van @ Luke en smeerde ernaar toe deze pastebincursus. Zoals ik het zie, raakt @Luke een JRE-synchronisatiebug. Ik weet dat dat moeilijk te geloven is, maar ik heb de code bekeken en ik kan het probleem gewoon niet zien.


Omdat je het zegt ConcurrentModificationException, Vermoed ik dat getBuffer() gooit het als het itereert aan de overkant van de list. De code die je hebt gepost mag nooit een ConcurrentModificationException vanwege de synchronisatie, maar ik vermoed dat een extra code belt add of remove dat is niet gesynchroniseerd, of je verwijdert terwijl je aan het itereren bent list. De enige manier waarop u een niet-gesynchroniseerde verzameling kunt aanpassen terwijl u eroverheen loopt, is via de Iterator.remove() methode:

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

Om uw vlag te beschermen, moet u try / finally gebruiken wanneer u een kritieke boolean als deze instelt. Dan zou elke uitzondering de insideGetBuffer op gepaste wijze:

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

Het is ook een beter patroon om rond een bepaald object te synchroniseren in plaats van synchronisatiesynchronisatie te gebruiken. Als u probeert het te beschermen list, dan zou het toevoegen van synchronisatie rond die lijst elke keer beter zijn

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

U kunt uw lijst ook omzetten in een gesynchroniseerde lijst, wat u niet zou hoeven te doen synchronize op elke keer:

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

10
2018-06-11 15:20



Op basis van die code zijn er slechts twee manieren waarop "Broke Synchronization" wordt afgedrukt.

  1. Ze synchroniseren op verschillende objecten (waarvan u zegt dat ze dat niet zijn)
  2. De insideGetBuffer wordt veranderd door een andere thread buiten het gesynchroniseerde blok.

Zonder deze twee kan er geen manier zijn dat de code die u hebt vermeld "Broke Synchronization" en de ConcurrentModificationException. Kun je een klein codefragment geven dat kan worden gebruikt om te bewijzen wat je zegt?

Bijwerken:

Ik ging door het voorbeeld dat Luke had gepost en ik zie vreemd gedrag op Java 1.6_24-64 bit Windows. Hetzelfde exemplaar van TestBuffer en de waarde van de insideGetBuffer is 'alternerend' in de verwijderingsmethode. Notitie het veld wordt niet bijgewerkt buiten een gesynchroniseerd gebied. Er is slechts één TestBuffer-instantie, maar laten we aannemen dat dit niet het geval is - insideGetBufferzou nooit op true worden ingesteld (dus het moet dezelfde instantie zijn).

    synchronized public void remove(Object item) {

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

Soms wordt het afgedrukt Broke Synchronization : true - false

Ik ben bezig om de assembler op Windows 64 bit Java te laten draaien.


4
2018-06-11 15:20



Een ConcurrentModificationException, meestal, wordt niet veroorzaakt door gelijktijdige threads. Het wordt veroorzaakt door de wijziging van de verzameling terwijl deze wordt geïtereerd:

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

De bovenstaande code zou een ConcurrentModificationException veroorzaken als someCondition waar is. Terwijl het itereert, kan de verzameling alleen worden gewijzigd via de methoden van iterator:

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

Ik vermoed dat dit is wat er gebeurt in je echte code. De geposte code is prima.


2
2018-06-11 15:21



Kun je deze code proberen, die een onafhankelijke test is?

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

prints

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

Kijken naar je stack traceer meer in detail.

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)

U kunt zien dat u toegang hebt tot de sleutelset van een HashMap, niet tot een lijst. Dit is belangrijk omdat de sleutelset a is uitzicht op de onderliggende kaart. Dit betekent dat u ervoor moet zorgen dat elke toegang tot deze kaart ook wordt beveiligd door hetzelfde slot. bijv. stel dat je een setter hebt zoals

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.

2
2018-06-12 07:27



'getBuffer' functie in de klasse Controller maakt dit probleem. Als twee threads voor de eerste keer tegelijkertijd de volgende 'als'-voorwaarde invoeren, maakt de controller uiteindelijk twee bufferobjecten. add-functie wordt aangeroepen op het eerste object en verwijderen wordt aangeroepen op het tweede object.

if (colorToBufferMap.containsKey(defaultColor)) {

Wanneer twee threads (threads toevoegen en verwijderen) tegelijkertijd invoeren (toen de buffer nog niet aan colorToBufferMap was toegevoegd), geeft bovenstaande if-opdracht false op en beide threads gaan naar het else-gedeelte en maken twee buffers, omdat buffer een lokale variabele deze twee threads ontvangen twee verschillende exemplaren van de buffer als onderdeel van de return-instructie. Alleen de laatst gemaakte wordt echter opgeslagen in de globale variabele 'colorToBufferMap'.

Boven de problematische lijn maakt deel uit van de functie getBuffer

public TestBuffer getBuffer() {
    TestBuffer buffer = null;
    if (colorToBufferMap.containsKey(defaultColor)) {
        buffer = colorToBufferMap.get(defaultColor);
    } else {
        buffer = new TestBuffer();
        colorToBufferMap.put(defaultColor, buffer);
    }
    return buffer;
}

Het synchroniseren van de functie 'getBuffer' in de klasse Controller lost dit probleem op.


2
2018-06-14 14:07



Bewerk: Antwoord is alleen geldig als twee verschillende Object-instanties worden gebruikt om de methoden herhaaldelijk aan te roepen.

Het scenario:   Je hebt twee gesynchroniseerde methoden. Eén voor het verwijderen van een entiteit en een andere voor toegang.   Het probleem komt als 1 thread binnen de verwijderingsmethode staat en een andere thread in de methode getBuffer en zet de insideGetBuffer = true.

Zoals je ontdekte, moet je synchronisatie op de lijst zetten omdat beide methoden op je lijst werken.


1
2018-06-11 15:29