Подтвердить что ты не робот

Синхронизация Java не работает в ожидании

У меня есть "простой" пример из 4-х классов, который надежно показывает непредвиденное поведение при синхронизации Java на нескольких машинах. Как вы можете прочитать ниже, учитывая контракт с ключевым словом java sychronized, Broke Synchronization никогда не должен печататься из класса TestBuffer.

Вот 4 класса, которые будут воспроизводить проблему (по крайней мере, для меня). Меня не интересует, как исправить этот сломанный пример, но скорее , почему он разбивает в первую очередь.

Проблема с синхронизацией - Controller.java

Ошибка синхронизации - SyncTest.java

Проблема с синхронизацией - TestBuffer.java

Ошибка синхронизации - Tuple3f.java

И вот результат, который я получаю при запуске:

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

UPDATE: @Gray имеет самый простой пример, который прорывается до сих пор. Его пример можно найти здесь: Странное состояние гонки JRC

Основываясь на обратной связи, полученной от других, похоже, что проблема может возникнуть на 64-разрядной версии 1.6.0_20-1.6.0_31 Java (неуверенности относительно новых 1.6.0) в Windows и OSX. Никто не смог воспроизвести проблему на Java 7. Для воспроизведения проблемы может потребоваться многоядерная машина.

ОРИГИНАЛЬНЫЙ ВОПРОС:

У меня есть класс, который предоставляет следующие методы:

  • remove - удаляет данный элемент из списка
  • getBuffer - Итерирует все элементы в списке

Я уменьшил проблему до двух функций ниже, оба из которых находятся в одном и том же объекте, и оба они synchronized. Если я не ошибаюсь, "Broke Synchronization" никогда не должен печататься, потому что insideGetBuffer всегда должен быть установлен на false до того, как ввести remove. Тем не менее, в моем приложении он печатает "Broke Synchronization", когда у меня есть один поток, вызывающий многократно удалять, в то время как другие вызовы getBuffer неоднократно. Симптом заключается в том, что я получаю ConcurrentModificationException.

См. также:

Очень странное состояние гонки, которое выглядит как проблема JRE

Отчет об ошибках Sun:

Это было подтверждено как ошибка в Java Sun. По-видимому, он зафиксирован (неосознанно?) В jdk7u4, но они не передали исправление в jdk6. Идентификатор ошибки: 7176993

4b9b3361

Ответ 1

Я думаю, что вы действительно смотрите на ошибку JVM в OSR. Используя упрощенную программу из @Gray (небольшие изменения, чтобы напечатать сообщение об ошибке) и некоторые опции, чтобы испортить/распечатать компиляцию JIT, вы можете увидеть, что происходит с JIT. И вы можете использовать некоторые опции, чтобы контролировать это до степени, которая может подавить проблему, что дает много доказательств тому, что это ошибка JVM.

Выполняется как:

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

вы можете получить условие ошибки (например, другие около 80% прогонов), а компиляция печатает несколько как:

 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)

Существует три замены OSR (те, которые содержат аннотацию% на идентификаторе компиляции). Я предполагаю, что это третий, который является вызовом loop remove(), который отвечает за ошибку. Это можно исключить из JIT через файл .hotspot_compiler, расположенный в рабочем каталоге со следующим содержимым:

exclude phil/StrangeRaceConditionTest strangeRaceConditionTest

Когда вы снова запустите программу, вы получите этот вывод:

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)

и проблема не появляется (по крайней мере, не в повторяющихся попытках, которые я сделал).

Кроме того, если вы немного измените параметры JVM, вы можете заставить проблему уйти. Используя одно из следующих действий, я не могу заставить проблему появиться.

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

Интересно, что вывод компиляции для обоих из них по-прежнему показывает OSR для цикла удаления. Моя догадка (и это большая) заключается в том, что задержка JIT через порог компиляции или изменение изменений FreqInlineSize в обработке OSR в этих случаях, которые обходят ошибку, которую вы в противном случае нажимаете.

См. здесь для информации о параметрах JVM.

Смотрите здесь и здесь для получения информации о выходе -XX: + PrintCompilation и о том, как возиться с тем, что делает JIT.

Ответ 2

Итак, в соответствии с кодом, который вы опубликовали, вы никогда не получите Broke Synchronization, если getBuffer() не выдает исключение между настройками true и false. См. Лучший рисунок ниже.

Edit:

Я взял код @Luke и уменьшил его до этот класс pastebin. Как я вижу, @Luke поражает ошибку синхронизации JRE. Я знаю, что трудно поверить, но я смотрел на код, и я просто не вижу проблемы.


Поскольку вы упоминаете ConcurrentModificationException, я подозреваю, что getBuffer() бросает его, когда он выполняет итерацию через list. Код, который вы отправили, никогда не должен вызывать ConcurrentModificationException из-за синхронизации, но я подозреваю, что какой-то дополнительный код вызывает add или remove, который не синхронизирован, или вы удаляете, когда выполняете итерацию через list, Единственный способ, которым вы можете изменить несинхронизированную коллекцию, когда вы выполняете итерацию по ней, - это метод Iterator.remove():

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

Чтобы защитить свой флаг, обязательно используйте try/finally, когда вы устанавливаете критическое логическое значение, подобное этому. Тогда любое исключение восстановит insideGetBuffer соответствующим образом:

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

Кроме того, это лучший шаблон для синхронизации вокруг определенного объекта, а не для использования синхронизации методов. Если вы пытаетесь защитить list, добавление синхронизации вокруг этого списка будет лучше. N

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

Вы также можете превратить свой список в синхронизированный список, который вам не приходилось бы на synchronize в каждый раз:

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

Ответ 3

Основываясь на этом коде, существует только два способа печати "Broke Synchronization".

  • Они синхронизируются на другом объекте (который, как вы говорите, не являются)
  • insideGetBuffer изменяется другим потоком вне синхронизированного блока.

Без этих двух способов не может быть указан код, который вы указали, будет печатать "Broke Synchronization" и ConcurrentModificationException. Можете ли вы дать небольшой фрагмент кода, который можно запустить, чтобы доказать, что вы говорите?

Update:

Я просмотрел пример Luke, и я вижу странное поведение на Java 1.6_24-64 бит Windows. Тот же экземпляр TestBuffer и значение insideGetBuffer являются "чередующимися" внутри метода удаления. Примечание поле не обновляется за пределами синхронизированной области. Существует только один экземпляр TestBuffer, но пусть предполагается, что они не являются - insideGetBuffer никогда не будет установлено значение true (поэтому он должен быть одним и тем же экземпляром).

    synchronized public void remove(Object item) {

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

Иногда он печатает Broke Synchronization : true - false

Я работаю над запуском ассемблера на Windows 64-разрядной Java.

Ответ 4

A ConcurrentModificationException, большую часть времени, не вызвано одновременными потоками. Это вызвано изменением коллекции при ее повторении:

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

Приведенный выше код вызовет исключение ConcurrentModificationException, если someCondition является истинным. Во время итерации коллекция может быть изменена только с помощью методов итератора:

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

Я подозреваю, что это то, что происходит в вашем реальном коде. Выложенный код в порядке.

Ответ 5

Вы можете попробовать этот код, который является самодостаточным?

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

печатает

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

Более подробно посмотрите на трассировку стека.

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)

Вы можете видеть, что вы получаете доступ к набору ключей HashMap, а не к списку. Это важно, потому что набор ключей - это представление на базовой карте. Это означает, что вам необходимо обеспечить, чтобы каждый доступ к этой карте также был защищен одной и той же блокировкой. например скажем, у вас есть сеттер вроде

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.

Ответ 6

Функция

'getBuffer' в классе Controller создает эту проблему. Если два потока одновременно вводятся в следующее условие "если", то контроллер будет создавать два объекта буфера. add функция вызывается в первом объекте, и удаление будет вызываться на втором объекте.

if (colorToBufferMap.containsKey(defaultColor)) {

Когда два потока (добавление и удаление потоков) вводятся одновременно (когда буфер еще не добавлен в colorToBufferMap), выше, если оператор возвращает false, и оба потока будут входить в часть else и создавать два буфера, поскольку buffer - это локальная переменная, эти два потока получат два разных экземпляра буфера как часть оператора return. Тем не менее, только последняя созданная будет сохранена в глобальной переменной colorToBufferMap.

Выше проблемной линии является частью функции 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;
}

Синхронизация функции getBuffer в классе Controller устранит эту проблему.

Ответ 7

Изменить: Ответ действителен только тогда, когда два разных экземпляра объекта используются для повторного вызова методов.

Сценарий: У вас есть два синхронизированного метода. Один для удаления объекта и другого для доступа. Проблема возникает, когда 1 поток находится внутри метода удаления, а другой поток - в методе getBuffer и устанавливает внутриGetBuffer = true.

Как вы выяснили, вам нужно поместить синхронизацию в список, потому что оба эти метода работают над вами.

Ответ 8

Если доступ к списку и внутри GetBuffer полностью содержится в этом коде, код выглядит, безусловно, потокобезопасным, и я не вижу возможности "распечатать синхронизацию", которая может быть напечатана, запрещая ошибку JVM.

Можно ли дважды проверить доступ ко всем переменным-членам (list и insideGetBuffer)? Возможности включают, если список был передан вам через конструктор (который ваш код не показывает), или эти переменные являются защищенными переменными, поэтому подклассы могут их изменять.

Другой возможностью является доступ через отражение.

Ответ 9

Я не верю, что это ошибка в JVM.

Мое первое подозрение состояло в том, что это была какая-то операция, переупорядочивающая то, что делает компилятор (на моей машине он отлично работает в отладчике, но при выполнении синхронизации происходит сбой), но

Я не могу сказать вам, почему, но я очень сильно подозреваю, что что-то отказывается от блокировки TestBuffer, которая неявно объявляется getBuffer() и удаляется (...).

Например, замените их следующим:

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

}

public void remove(final Object item) {
    synchronized (this) {
        // fails if this is called while getBuffer is running
        if (this.insideGetBuffer) {
            System.out.println("Broke Synchronization ");
            System.out.println(item);
        }
    }
}

И у вас все еще есть ошибка синхронизации. Но выберите что-то еще, чтобы войти в систему, например:

private Object lock = new Object();
public void getBuffer() {
    synchronized (this.lock) {
        this.insideGetBuffer = true;
        try {
            int i = 0;
            for (Object item : this.list) {
                if (item != null) {
                    i++;
                }
            }
        } finally {
            this.insideGetBuffer = false;
        }
    }

}

public void remove(final Object item) {
    synchronized (this.lock) {
        // fails if this is called while getBuffer is running
        if (this.insideGetBuffer) {
            System.out.println("Broke Synchronization ");
            System.out.println(item);
        }
    }
}

И все работает так, как ожидалось.

Теперь вы можете имитировать отказ от блокировки, добавив:

this.lock.wait(1);

в цикле for getBuffer(), и вы снова начнете сбой.

Я остаюсь в тупике от того, что отказывается от блокировки, но, как правило, лучше использовать явную синхронизацию для защищенных блокировок, чем оператор синхронизации.

Ответ 10

У меня была аналогичная проблема. Ошибка заключается в том, что вы не объявили какое-либо поле как volatile. Это ключевое слово используется, чтобы указать, что поле будет изменено различными потоками, и, следовательно, оно не может быть кэшировано. Вместо этого все записи и чтения ДОЛЖНЫ перейти в "реальную" ячейку памяти поля.

Для получения дополнительной информации просто введите google для Java Memory Model"

Хотя большинство читателей сосредоточено на классе TestBuffer, я думаю, что проблема может быть где-то в другом месте (например, вы пытались добавить синхронизацию в контроллере класса? Или сделать volatile свои поля?).

пс. обратите внимание, что разные виртуальные машины Java могут использовать различную оптимизацию на разных платформах, и поэтому проблемы синхронизации могут появляться чаще на платформе, чем другие. Единственный способ быть в безопасности - это соответствовать спецификациям Java и записывать ошибку, если VM не уважает ее.