Data Race Detector
by Devexperts
drd.properties - файл, в котором прописаны все остальные настройки. По умолчанию этот файл ищется в "<DRD_HOME>\drd.properties", но путь к нему можно указать через -Ddrd.settings.file=<FULL_PATH_TO_FILE>. Если этого файла нет, используются настройки по умолчанию.
Все настройки перечислены ниже. Кроме файла drd.properties их всегда можно указать через -D<property_name>=<property_value>
DRD создает 3 лог-файла:
Папка, в которой эти файлы будут созданы, регулируется свойством "drd.log.dir". Его можно прописать либо в DRD_HOME_DIR\config\drd.properties, либо в строке параметров JVM через -Ddrd.log.dir= ...
Это пример конфигурационного файла. Актуальный находится в delivery bundle.
<DRDConfig xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="config-scheme.xsd"> <!--NB: the less instrumentation scope is, the less is overhead, that DRD imposes on target application--> <InstrumentationScope> <!--Detect sync operations everywhere--> <SyncInterception defaultPolicy="include"> <!--<Rule type="include" path="com/devexperts/"/>--> </SyncInterception> <!--Detect data operations only in "com.devexperts" package by default--> <RaceDetection defaultPolicy="exclude"> <Rule type="include" path="com/devexperts/"/> </RaceDetection> <!--Races wouldn't be detected on fields, listed in SkipOurFields tag --> <SkipOurFields/> <!--Races wouldn't be detected on foreign calls, listed in SkipForeignCalls tag Foreign call is method call of object not from race detection scope --> <SkipForeignCalls> <!--Well, we believe, that any call to System is thread safe, so do not detect races on it, because we have a lot of System.currentTimeMillis() calls from different threads and do not want to see them in DRD output file--> <Target clazz="java/lang/System" name="*" type="method"/> </SkipForeignCalls> </InstrumentationScope> <!--Contracts determine, if foreign call should be treated as read or as write operation--> <Contracts> <!--Contracts are analyzed top down until some matches. If no one matches, foreign call would be treated as write--> <!--If write="*", treat all methods, not listed in "read", as writes. Same for read="*"--> <!--Methods "keySet","values" and "entrySet" of java.util.Map are reads--> <Contract clazz="java.util.Map" read="keySet,values,entrySet"/> <!--Methods java.util.List.listIterator() is read too --> <Contract clazz="java.util.List" read="listIterator"/> <!--If method name is "hashCode", "toString" or "equals", or method name starts with "get", "is", "contains", "iter" or "has", than this method is read--> <Contract clazz="*" read="get*,toString,hashCode,equals,is*,contains*,iter*,has*"/> <!--All other foreign calls are writes--> </Contracts> <!--When DRD should print additional info on accessing field of instrumented class or foreign call? traceDataOperations = trace clocks for each read/write operation on this field/call. traceSyncOperations = trace all sync operations in this class These two options would help to detect bugs in DRD itself storeThreadAccesses = store last thread accesses to specified field/call. If field is racy, enabling this option would make it possible to see stack traces of both threads, when race occurs (by default only one stack trace is displayed - stack trace of current thread. For second thread only brief location is available) printThreadAccess = print all thread accesses to specified field/call By default all these options are true --> <TraceTracking> <!--We've found race in our app on MyBusinessLogicClass.myField field and want to get both stack traces. We enable storing Thread Accesses for this field and restart app We may optionally specify caller classes: classes, whose method's accesses to myField we want to track --> <Target clazz="com/my/MyBusinessLogicClass" type="myField" name="activeIssue" caller="com/my/class/that/uses/MyBusinessLogicClass" storeThreadAccesses="true"/> </TraceTracking> </DRDConfig>
Файл состоит из трех частей:
DRD agent модифицирует классы приложения с помощью Java Instrumentation API - перед тем, как класс будет загружен, он в виде массива байтов передается DRD agent-у, а тот, если необходимо, вставляет вызовы внутреннего алгоритма DRD, отвечающего за поиск гонок. Этот процесс называется инструментированием байт-кода.
Интересные с точки зрения поиска гонок операции можно разделить на две группы:
Чтобы DRD не отслеживал операции синхронизации во всех классах / не искал гонки во всех классах (например, логично не искать гонки в java.*), можно ограничить его область инструментирования.
<SyncInterception defaultPolicy="include"/> <RaceDetection defaultPolicy="exclude"> <Rule type="include" path="com/devexperts/"/> </RaceDetection>
Тег "SyncInterception" отвечает за область, в которой будут отслеживаться синхронизационные операции, тег "RaceDetection" - обращения к разделяемым переменным, то есть, непосредственно за поиск гонок.
Поскольку искать гонки в классе Х, и при этом не отслеживать в нем операции синхронизации, бессмысленно, DRD пересекает области RaceDetection с SyncInterception, чтобы первая была подножеством второй.
Для каждого загружаемого класса C DRD выполняет следующее:
Классы, в которых DRD ищет гонки, мы называем "проинструментированными". Соответственно, инстансы этих классов - проинструментированные объекты.
Если класс проинструментирован, то он внутри себя содержит вставки кода, проверяющие обращения к полям этого класса (как static, так и instance) на предмет вовлеченности в гонки. Таким образом, если мы видим вызов метода проинструментированного класса, нам ничего не нужно делать - класс внутри себя разберется сам. Однако, мы можем встретить вызов непроинструментированного класса. Например, если мы ищем гонки только в пакете "/com/devexperts/", то вызов java.util.List.get() будет таким примером. Такие вызовы мы далее называем "foreign calls", а непроинструментированные объекты, встреченные в проинструментированном коде - "foreign objects".
В отличие от проинструментированных объектов, foreign-объекты не содержат часов внутри себя для своих полей, поэтому мы ассоциируем отдельные часы с каждым foreign-объектом и каждый foreign-call трактуем как read или write операцию.
Тег "Contracts" предназначен для того, чтобы описать контракты foreign классов (например, java.util.Map.put() - это write, а get() - это read).
<Contracts> <!--Contracts are analyzed top down until some matches. If no one matches, foreign call would be treated as write--> <!--If write="*", treat all methods, not listed in "read", as writes. Same for read="*"--> <!--Methods "keySet","values" and "entrySet" of java.util.Map are reads--> <Contract clazz="java.util.Map" read="keySet,values,entrySet"/> <!--Methods java.util.List.listIterator() is read too --> <Contract clazz="java.util.List" read="listIterator"/> <!--If method name is "hashCode", "toString" or "equals", or method name starts with "get", "is", "contains", "iter" or "has", than this method is read--> <Contract clazz="*" read="get*,toString,hashCode,equals,is*,contains*,iter*,has*"/> <!--All other foreign calls are writes--> </Contracts>
Контракты внутри этого тега анализируются сверху вниз до первого совпадения: для каждого foreign call X.f() DRD идет по списку контрактов сверху вниз, пока не найдет такой контракт c, что имя класса начинается с contract.clazz и либо название вызываемого метода указано в contract.read/contract.write, либо contract.read/contract.write="*".
Если таких контрактов не найдено, то foreign call трактуется как write.
Важно:
У класса может быть существенно более сложный контракт, чем простое "трактовать такие-то методы, как read, а остальные, как write". Например, некоторые методы могут быть не просто потокобезопасными, а еще и при вызове их в правильном порядке обеспечивать сихнронизацию потоков (happens-before) - например, ConcurrentHashMap.get() синхронизирован с предыдущем вызовом set() по тому же ключу. Для этого мы разработали конфигуратор синхронизационных контрактов, которые описываются в файле hb-config.xml.
В этом файле перечислены все синхронизационные контракты. В частности, описаны контракты классов Unsafe и AbstractQueuedSynchronizer, что позволяет автоматически поддерживать все сложные механизмы синхронизации пакета java.util.concurrent.
Все дескрипторы методов описываются согласно спецификации JVM.
Например, "(Ljava/lang/Object;JLjava/util/List;)Z" описывает дескриптор метода с тремя входными параметрами (Object, long, List), возвращающего значение типа boolean.
На данный момент выделяются 2 типа контрактов:
Такие контракты содержатся внутри тега "Syncs".
Этот и следующий xml-фрагменты - примеры частей конфигурационного файла. Актуальный файл hb-config.xml находится в delivery bundle.
<Sync> <Links> <Link send="owner" receive="owner"/> <Link send="param" send-number="0" receive="param" receive-number="0"/> </Links> <Send> <MethodCall owner="java.util.concurrent.ConcurrentMap" name="put" descriptor="(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"/> </Send> <Receive> <MethodCall owner="java.util.concurrent.ConcurrentMap" name="get" descriptor="(Ljava/lang/Object;)Ljava/lang/Object;"/> </Receive> </Sync>
В данном контракте указано, что вызов метода put объекта типа ConcurrentMap happens-before вызова get того же объекта (link owner-owner) по тому же ключу (link param0-param0).
Такие контракты содержатся внутри тега "Multiple-Syncs".
<Multiple-Sync owner="java.util.concurrent.locks.AbstractQueuedSynchronizer"> <Multiple-Links> <Multiple-Link type="owner"/> </Multiple-Links> <Call type="receive" name="tryAcquire" descriptor="(I)Z" shouldReturnTrue="true"/> <Call type="send" name="tryRelease" descriptor="(I)Z"/> <Call type="receive" name="tryAcquireShared" descriptor="(I)Z" shouldReturnTrue="true"/> <Call type="send" name="tryReleaseShared" descriptor="(I)Z"/> <Call type="receive" name="acquire" descriptor="(I)V"/> <Call type="receive" name="acquireInterruptibly" descriptor="(I)V"/> <Call type="receive" name="tryAcquireNanos" descriptor="(IJ)Z" shouldReturnTrue="true"/> <Call type="send" name="release" descriptor="(I)Z"/> <Call type="receive" name="acquireShared" descriptor="(I)V"/> <Call type="receive" name="acquireSharedInterruptibly" descriptor="(I)V"/> <Call type="receive" name="tryAcquireSharedNanos" descriptor="(IJ)Z" shouldReturnTrue="true"/> <Call type="send" name="releaseShared" descriptor="(I)Z"/> <Call type="send" name="setState" descriptor="(I)V"/> <Call type="receive" name="getState" descriptor="()I"/> <Call type="full" name="compareAndSetState" descriptor="(II)Z" shouldReturnTrue="true"/> </Multiple-Sync>
Здесь везде идет речь об одном и том же объекте типа AbstractQueuedSynchronizer (link типа owner). Указано, например, что метод release является левой частью отношения happens-before (имеет семантику monitorexit), а, скажем, "compareAndSetState" - полным отношением happens-before (имеет семантику volatile read и write одновременно), но только если вернул true.
JVM method descriptors : http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.3
JLS chapter about threads: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
happens-before relation: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5
java.util.concurrent.atomic documentation: http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/atomic/package-summary.html
java.util.concurrent documentation: http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/package-summary.html