Видимость состояния нескольких потоков в Java: есть ли способ превратить JVM в наихудший сценарий?

Предположим, что у нашего кода есть 2 потока (A и B), где-то есть ссылка на тот же экземпляр этого класса:

public class MyValueHolder {

    private int value = 1;

    // ... getter and setter

}

Когда Thread A делает myValueHolder.setValue(7), нет гарантии, что Thread B когда-либо прочитает это значение: myValueHolder.getValue() может - в теории - продолжать возвращать 1 навсегда.

Однако на практике аппаратное обеспечение рано или поздно очистит кеш второго уровня, поэтому Thread B будет читать 7 раньше или позже (обычно раньше).

Есть ли способ заставить JVM эмулировать этот наихудший сценарий, для которого он продолжает возвращать 1 forever для Thread B?. Было бы очень полезно протестировать наш многопоточный код с помощью нашего существующие испытания при этих обстоятельствах.

Ответ 1

jcstress. Есть несколько способов ответить на этот вопрос.

  • Самое простое решение - обернуть геттер в цикле и позволить JIT поднять его. Это разрешено для чтения в нелетучих полевых условиях и имитирует сбой видимости при оптимизации компилятора.
  • Более сложный трюк включает в себя получение отладочной сборки OpenJDK и использование -XX:+StressLCM -XX:+StressGCM, эффективно выполняющее планирование расписания команд. Скорее всего, нагрузка, о которой идет речь, будет плавать где-нибудь, где вы можете обнаружить, с регулярными тестами, которые ваш продукт имеет.
  • Я не уверен, что есть практическое оборудование, которое удерживает письменное значение, достаточно долго непрозрачное для согласованности кеша, но несколько легко построить тестовый файл с помощью jcstress. Вы должны иметь в виду, что оптимизация в (1) также может произойти, поэтому нам нужно использовать трюк, чтобы предотвратить это. Я думаю, что что-то вроде этого должно работать.

Ответ 2

Было бы здорово иметь компилятор Java, который намеренно выполнил бы как можно больше странных (но разрешенных) трансфирант, чтобы легче разбить небезопасный код потока, например Csmith для C. К сожалению, такого компилятора не существует (насколько мне известно).

Тем временем вы можете попробовать jcstress библиотеку * и реализовать свой код на нескольких архитектурах, если возможно, с помощью более слабые модели памяти (т.е. не x86), чтобы попытаться сломать ваш код:

Java Concurrency Стресс-тесты (jcstress) - это экспериментальная привязка, а набор тестов помогает исследовать правильность поддержки Concurrency в JVM, библиотеках классов и оборудовании.

Но в конце концов, к сожалению, единственный способ доказать правильность кода на 100% - проверка кода (и я не знаю инструмент статического анализа кода, способный обнаруживать все условия гонки).

* Я не использовал его, и я не понимаю, какой из jcstress и java- concurrency -torture library больше (я бы заподозрил jcstress).

Ответ 3

Не на реальной машине, печально тестирование многопоточного кода будет затруднено.

Как вы говорите, аппаратное обеспечение очистит кеш второго уровня, и JVM не имеет никакого контроля над этим. JSL указывает только, что должно произойти, и это случай, когда B может никогда не увидеть обновленное значение value.

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

Однако вы можете запустить это на симуляторе, который имитирует аппаратное обеспечение, которое не очищает кеш второго уровня. Звучит как много усилий, хотя!

Ответ 4

Я думаю, вы ссылаетесь на принцип, называемый "ложным совместным использованием", когда разные ЦП должны синхронизировать свои кеши или иначе сталкиваться с возможностью того, что данные, такие как вы описываете, могут быть несовместимы. Существует очень хорошая статья о ложном совместном использовании на веб-сайте Intel. Intel описывает некоторые полезные инструменты в своей статье для диагностики этой проблемы. Это релевантная цитата:

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

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

Существуют другие ресурсы, которые решают эту проблему в Интернете. Например, в этой статье есть предложение об избежании ложного обмена в Java. Я не пробовал этот метод, поэтому я не могу ручаться за него, но я думаю, что идея автора звучит. Возможно, вам стоит попробовать его предложение.

Ответ 5

Я ранее предлагал худшее поведение JVM для тестирования в списке моделей памяти, но идея не показалась популярной.

Итак, как получить "худшее поведение JVM", с существующими технологиями. Как я могу проверить сценарий в вопросе и заставить его сработать КАЖДЫЙ раз. Вы можете попытаться найти установку со слабой моделью памяти, но вряд ли она будет идеальной.

То, что я часто рассматривал, - это использование распределенного JVM, похожего на то, как я считаю, что Terracotta работает под обложкой, поэтому ваше приложение теперь работает на нескольких JVM (удаленных или локальных) (потоки в одном приложении запускаются в разных экземплярах). В этой настройке связь между JVM-потоками происходит при барьерах памяти, например. синхронизированные ключевые слова, отсутствующие в запрограммированном коде (он соответствует модели памяти Java), и приложение настроено, то есть вы говорите, что этот поток классов работает здесь. Никакое изменение кода не требуется для ваших тестов только для конфигурации, любое упорядоченное Java-приложение должно работать из коробки, однако эта настройка будет очень нетерпима к плохо упорядоченному приложению (обычно это проблема... теперь актив, т.е. модель памяти демонстрирует очень слабое, но юридическое поведение). В приведенном выше примере загрузка кода на кластер, если два потока выполняются на разных узлах, setValue не имеет эффекта, видимого для другого потока, если только код не был изменен и не синхронизирован, volatile и т.д., Тогда код работает по назначению.

Теперь ваш тест для приведенного выше примера (правильно настроенный) будет терпеть неудачу каждый раз без правильного "происходит до заказа", что потенциально очень полезно для тестов. Недостатком плана полного охвата вам может быть потенциально node для потока приложений (может быть один и тот же компьютер или несколько в кластере) или несколько тестовых прогонов. Если у вас 1000 потоков, то это может быть непомерно высоким, хотя, надеюсь, они будут объединены и уменьшены для сценариев тестирования E2E или запущены в облаке. Если ничего подобного не может быть полезной при демонстрации проблемы.

связь между потоками через JVM

Ответ 6

Приведенный вами пример описывается как "Неправильно синхронизирован" в http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4. Я думаю, что это всегда неверно и рано или поздно приведет к ошибкам. В большинстве случаев позже: -).

Чтобы найти такие неправильно синхронизированные кодовые блоки, я использую следующий алгоритм:

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

Я реализовал этот алгоритм внутри http://vmlens.com, который является инструментом для поиска гонок данных внутри java-программ.

Ответ 7

Вот простой способ: просто закомментируйте код для setValue. Вы можете раскомментировать его после тестирования. Поскольку во многих случаях такой механизм необходим для фальсификации отказов, было бы неплохо построить общий механизм для всех таких случаев.