openjdk.ruOpenJDK на русском

JEP draft: Java Thread Sanitizer

Thread Sanitizer для Java

Авторjcbeyler
ОтветственныйJean Christophe Beyler
ТипFeature
ОбластьJDK
СтатусDraft
Выпускtbd
Компонентhotspot / runtime
Создан2018/07/30 16:26
Обновлён2025/08/14 15:38
Задача8208520

Примечание: это описание ещё не закончено :)

Аннотация

Реализовать в OpenJDK динамический детектор гонок данных для кода на Java и нативного кода JNI на основе ThreadSanitizer.

Цели

Дать способ обнаруживать гонки данных в программах на Java и в связанном с ними нативном коде JNI:

  • Обнаруживает гонки данных в одном выполнении программы полно (т. е. находит все гонки данных в этом выполнении) и точно (т. е. не сообщает о ложных гонках данных)
  • Включается флагом командной строки
  • Выводит сведения о гонках данных в файл или в stderr
  • Выдаёт трассировки стека на моменты чтения и записи, образующих гонку данных, со сведениями о потоках

Что не является целью

  • Обнаруживает все гонки данных в исходном коде программы
  • Дать способ обнаруживать гонки данных в самой JVM (HotSpot)
  • Создать детектор гонок данных с низкими накладными расходами, пригодный для постоянной работы в продакшене

Мотивация

Гонка данных делает программу на Java некорректно синхронизированной, что приводит к ошибочному, недетерминированному и неожиданному поведению, которое обычно проявляется лишь изредка. Поскольку в Java есть JNI для взаимодействия нативного кода с кодом на Java, а поведение программ на C/C++ с гонками данных не определено, гонки данных в нативном коде могут вызывать неопределённое поведение программ на Java и нарушать гарантии безопасности памяти и типов, которые должна давать Java. По мере того как программы становятся всё сложнее, гонки данных остаются одним из главных препятствий на пути к надёжности, безопасности и отказоустойчивости программ на Java, но обнаружить, воспроизвести, исправить или устранить все гонки данных крайне сложно.

Поэтому важно, чтобы в OpenJDK был стабильный и поддерживаемый инструмент обнаружения гонок данных как для кода на Java, так и для нативного кода JNI. Такой инструмент — важный шаг вперёд: он поможет разработчикам отлаживать и исправлять гонки данных в больших и малых программах, уменьшая их число и повышая надёжность. В идеале инструмент должен быть динамически полным, чтобы обнаруживать во время выполнения все проявившиеся гонки данных, и точным, чтобы приносить пользу разработчикам, ведь разобраться в гонке данных и так непросто.

Вместо того чтобы полагаться на исследовательские инструменты, инструментирующие байт-код Java (например, FastTrack2 в RoadRunner), реализация полностью встраивает ThreadSanitizer (TSan) в OpenJDK. Так инструмент может обнаруживать гонки данных и в коде на Java, и в нативном коде JNI.

Описание TSAN для случая LLVM

TSan использует анализ отношения happens-before, чтобы динамически, полно и точно обнаруживать гонки данных в программах на C/C++. Реализация устроена так: компилятор LLVM встраивает в пользовательскую программу обратные вызовы на интересующие события, например захват и освобождение мьютекса и чтение и запись памяти. Среда выполнения TSan компонуется с исполняемым файлом. Когда поток выполняет обратный вызов TSan, среда выполнения TSan обновляет метаданные (для эффективности они хранятся по фиксированному смещению от адреса мьютекса или переменной) и проверяет их, чтобы определить, есть ли гонка данных. Если есть, в stderr выводится отчёт. Проделана значительная работа, чтобы обеспечить и низкие накладные расходы на производительность (обычно около ~4x), и практически нулевую долю ложных срабатываний: если TSan выдаёт отчёт о гонке данных, когда гонка не проявилась, это считается ошибкой в TSan.

В случае Java конечная цель TSan/Java — дать всестороннее обнаружение гонок данных в пользовательском коде на стыке языков. Реализация TSan/Java охватывает три основных направления: (1) добавление обратных вызовов TSan в JVM, (2) встраивание среды выполнения TSan так, чтобы она работала рядом с JVM, и (3) предоставление символизации, когда TSan выдаёт предупреждение.

Обратные вызовы TSan в JVM

Чтобы обнаруживать гонки данных в коде на Java, среда выполнения TSan должна отслеживать всю синхронизацию Java и все обращения к полям Java. Есть три источника синхронизации на уровне Java (мониторы на уровне языка, volatile-обращения, библиотеки java.util.concurrent) и два источника синхронизации, экспортируемой VM (raw-мониторы и raw-мониторы JVMTI). Обращения к полям Java включают чтение и запись скалярных и статических полей и массивов, а также обращения к полям через рефлексию. Реализация TSan/Java должна вставлять нужные обратные вызовы в среду выполнения TSan во всех перечисленных источниках синхронизации Java и обращений к полям.

Поскольку TSan хранит метаданные по смещениям от указателей, TSan должен точно знать, где в куче размещены объекты Java. TSan не может знать жизненный цикл объектов Java, поэтому инструментирование JVM должно сообщать TSan, когда объект Java перемещён или собран. В TSan есть обратный вызов для обработки перемещённого объекта, и реализация TSan/Java вставляет его в подходящих местах, например в сборщиках мусора.

Работа среды выполнения TSan в процессе JVM

Среда выполнения TSan была спроектирована для поддержания корректного состояния памяти и синхронизации в программе на C++. Поскольку JVM сама является сложной программой на C++, для мирного сосуществования им нужна тщательная координация. Главная проблема — активный перехват функций libc в TSan; TSan/Java обходит её так: TSan анализирует стек и хранит для каждого потока логический флаг «вызвано из JVM». Большинство функций libc при вызове из JVM перенаправляются в исходную реализацию libc. Заметное исключение — функции выделения памяти: они перенаправляются в собственный аллокатор TSan.

Символизация для отчётов TSan

Чтобы выдавать пользователям полезные трассировки стека, TSan поддерживает для каждого потока теневой стек, в котором сохраняются значения счётчика команд. Для TSan/Java передавать сырой «счётчик команд» бесполезно: символизатор C++ не может его декодировать, а блок кода позже может быть собран. Вместо этого мы используем механизм GTrace, чтобы передавать в среду выполнения TSan токены идентификаторов методов GTrace. Токены сохраняются, а позже TSan должен их декодировать. JVM предоставляет публичную функцию C++ (TsanJavaSymbol), через которую поток C++ или Java может обратиться к JVM и получить строку метода, связанную с токеном.

Пример использования

Разработчик конфигурирует и собирает JDK с флагом конфигурации --use-tsan. Затем при запуске java разработчик указывает -XX:+TSan, чтобы включить отслеживание TSan. Если обнаружена гонка, при завершении программы TSan/Java выдаст следующий отчёт:

WARNING: ThreadSanitizer: data race (pid=7616)
  Read of size 4 at 0x7fd80270b328 by thread T26:
    #0 C.Get()I (C.java:7)  
    #1 A.Get()I (A.java:6)  
    #2 (Generated Stub)  
    #3 GetValue (Native.cc:18) (Test_native+0x10e330f)
    #4 Java_N_GetNative (Native.cc:23) (Test_native+0x10e330f)
    ...
  Previous write of size 4 at 0x7fd80270b328 by thread T5:
    #0 C.Set(I)V (C.java:4)  
    #1 A.Set(I)V (A.java:3)  
    #2 (Generated Stub)  
    #3 SetValue (Native.cc:7) (Test_native+0x10e31d8)
    #4 Java_N_SetNative (Native.cc:12) (Test_native+0x10e31d8)
    ...
  SUMMARY: ThreadSanitizer: data race  in C.Get()I (C.java:7)

Флаг конфигурации и новый флаг

Должен быть флаг конфигурации --use-tsan для компиляции и компоновки со специфичными для TSan настройками. Также должен быть флаг JVM -XX:+TSan для включения TSan во время выполнения.

Прочие требования:

  • Только 64-битные платформы (TSan поддерживает только 64-битные)
  • Код JNI нужно компилировать компилятором LLVM с поддержкой TSan

Детали реализации

Текущие детали реализации:

  • Реализация сделана только для интерпретатора, другие уровни многоуровневой компиляции и среды выполнения не поддерживаются
  • Система обращается к среде выполнения LLVM Tsan, и среда выполнения Tsan сообщает пользователю о гонках; повторно реализовывать библиотеку Tsan внутри OpenJDK сейчас не планируется

Альтернативы

FastTrack и FastTrack2 — передовые алгоритмы динамического обнаружения гонок данных, одновременно полные и точные. Их прототипы реализованы во фреймворке RoadRunner с помощью инструментирования байт-кода. Поэтому они могут обнаруживать гонки данных в программах на Java, но не в нативном коде JNI.

Алгоритмы статического обнаружения гонок данных могут найти все гонки данных в исходном коде программы на Java, но обычно они также сообщают о большом числе ложных гонок. Кроме того, статическим алгоритмам трудно масштабироваться на большие объёмы исходного кода на Java и учитывать динамическую загрузку классов, рефлексию, код JNI и синхронизацию через volatile-поля и библиотеки java.util.concurrent.

Динамические детекторы гонок данных на основе алгоритма lockset или его вариантов могут обнаружить в исходном коде больше настоящих гонок данных, но не умеют учитывать все виды синхронизации, например обращения к volatile-полям и библиотеки java.util.concurrent. Поэтому они могут сообщать о ложных гонках данных.

Тестирование

С функциональной точки зрения, чтобы убедиться, что TSan не сломан, можно запускать существующие тесты JTREG с новым флагом. Кроме того, будет добавлено немного кода с состояниями гонки, чтобы явно тестировать состояния гонки и показать работоспособность и полезность системы TSan.

Например, должны проходить следующие модульные тесты:

  • Симметричная гонка данных в двух потоках.
  • Один поток обращается к данным без защиты, другой защищает своё обращение
  • Оба потока корректно защищают свои обращения
  • Использование кучи и глобальных статических переменных
  • Использование synchronized-методов и synchronized-блоков
  • Выход из synchronized-блока при обычном потоке управления, по исключению и по прерыванию потока
  • Wait/notify и wait/interrupt
  • Рекурсивные мониторы
  • Синхронизация через volatile
  • Синхронизация через java.util.concurrent

Риски и допущения

Когда функция выключена, потерь производительности и рисков нет. Пользователь, который не включает систему, не заметит разницы в производительности.

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