Я знаю, что составные операции, например i++
, не являются потокобезопасными, поскольку включают в себя несколько операций.
Но является ли проверка ссылки на самой себе поточно-ориентированной операцией?
a != a //is this thread-safe
Я пытался запрограммировать это и использовать несколько потоков, но ничего не вышло. Думаю, я не мог смоделировать гонку на своей машине.
РЕДАКТИРОВАТЬ:
public class TestThreadSafety {
private Object a = new Object();
public static void main(String[] args) {
final TestThreadSafety instance = new TestThreadSafety();
Thread testingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
long countOfIterations = 0L;
while(true){
boolean flag = instance.a != instance.a;
if(flag)
System.out.println(countOfIterations + ":" + flag);
countOfIterations++;
}
}
});
Thread updatingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
instance.a = new Object();
}
}
});
testingReferenceThread.start();
updatingReferenceThread.start();
}
}
Это программа, которую я использую для проверки безопасности потоков.
Странное поведение
Поскольку моя программа запускается между некоторыми итерациями, я получаю значение флага вывода, что означает, что !=
проверка ссылки не выполняется для той же ссылки. НО после нескольких итераций вывод становится постоянным значением, false
а затем выполнение программы в течение длительного времени не генерирует ни одного true
вывода.
Как видно из выходных данных, после некоторых n (не фиксированных) итераций выход кажется постоянным и не изменяется.
Выход:
Для некоторых итераций:
1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
источник
1234:true
никогда не разбивают друг друга ). Гоночный тест требует более жесткого внутреннего цикла. Напечатайте сводку в конце (как кто-то сделал ниже, используя среду модульного тестирования).Ответы:
При отсутствии синхронизации этот код
Object a; public boolean test() { return a != a; }
может производить
true
. Это байт-код дляtest()
ALOAD 0 GETFIELD test/Test1.a : Ljava/lang/Object; ALOAD 0 GETFIELD test/Test1.a : Ljava/lang/Object; IF_ACMPEQ L1 ...
как мы можем видеть, он
a
дважды загружает поле в локальные вары, это неатомарная операция, если онаa
была изменена между ними другим сравнением потоковfalse
.Также здесь актуальна проблема видимости памяти, нет гарантии, что изменения,
a
внесенные другим потоком, будут видны текущему потоку.источник
!=
, который включает загрузку LHS и RHS отдельно. И поэтому, если JLS не упоминает ничего конкретного об оптимизации, когда LHS и RHS синтаксически идентичны, тогда будет применяться общее правило, что означает загрузкуa
дважды.Если
a
потенциально может быть обновлен другим потоком (без надлежащей синхронизации!), То Нет.Это ничего не значит! Проблема в том, что если выполнение, в котором
a
обновляется другой поток, разрешено JLS, то код не является потокобезопасным. Тот факт, что вы не можете вызвать состояние гонки с конкретным тестовым примером на конкретной машине и конкретной реализации Java, не препятствует тому, чтобы это произошло в других обстоятельствах.Да, теоретически, при определенных обстоятельствах.
Как вариант,
a != a
можно было вернуться,false
даже еслиa
менялось одновременно.Относительно «странного поведения»:
Это «странное» поведение согласуется со следующим сценарием выполнения:
Программа загружается, и JVM начинает интерпретировать байт-коды. Поскольку (как мы видели из вывода javap) байт-код выполняет две загрузки, вы (по-видимому) время от времени видите результаты состояния гонки.
Через некоторое время код компилируется JIT-компилятором. Оптимизатор JIT замечает, что есть две загрузки одного и того же слота памяти (
a
) близко друг к другу, и оптимизирует второй. (На самом деле, есть шанс, что он полностью оптимизирует тест ...)Теперь состояние гонки больше не проявляется, потому что больше нет двух нагрузок.
Обратите внимание, что все это согласуется с тем, что JLS позволяет реализации Java.
@kriss так прокомментировал:
Модель памяти Java (указанная в JLS 17.4 ) определяет набор предварительных условий, при которых один поток гарантированно увидит значения памяти, записанные другим потоком. Если один поток пытается прочитать переменную, записанную другим, и эти предварительные условия не выполняются, то может быть несколько возможных выполнений ... некоторые из которых, вероятно, будут неправильными (с точки зрения требований приложения). Другими словами, набор возможных вариантов поведения (т. Е. Набор «правильно сформированных выполнений») определен, но мы не можем сказать, какое из этих поведений произойдет.
Компилятору разрешено комбинировать и переупорядочивать загрузки и сохранять (и делать другие вещи) при условии, что конечный результат кода такой же:
Но если код не синхронизируется должным образом (и, следовательно, отношения «происходит до» недостаточно ограничивают набор правильно сформированных исполнений), компилятору разрешается переупорядочивать загрузки и сохранения таким образом, чтобы это дало «неверные» результаты. (Но на самом деле это просто говорит о том, что программа неверна.)
источник
a != a
может вернуть истину?Подтверждено с помощью test-ng:
public class MyTest { private static Integer count=1; @Test(threadPoolSize = 1000, invocationCount=10000) public void test(){ count = new Integer(new Random().nextInt()); Assert.assertFalse(count != count); } }
У меня 2 фэйла на 10 000 обращений. Так что нет , это НЕ потокобезопасно
источник
Random.nextInt()
деталь лишняя. Вы могли бы протестировать сnew Object()
таким же успехом.Нет это не так. Для сравнения виртуальная машина Java должна поместить два значения для сравнения в стек и запустить инструкцию сравнения (которая зависит от типа «a»).
Виртуальная машина Java может:
false
В первом случае другой поток может изменить значение «a» между двумя чтениями.
Выбор стратегии зависит от компилятора Java и среды выполнения Java (особенно JIT-компилятора). Это может даже измениться во время выполнения вашей программы.
Если вы хотите убедиться, как осуществляется доступ к переменной, вы должны сделать это
volatile
(так называемый «половинный барьер памяти») или добавить полный барьер памяти (synchronized
). Вы также можете использовать API более высокого уровня (например,AtomicInteger
как упомянул Джунед Ахасан).Дополнительные сведения о безопасности потоков см. В JSR 133 ( модель памяти Java ).
источник
a
as по-volatile
прежнему будет подразумевать два разных чтения с возможностью изменения между ними.Все это было хорошо объяснено Стивеном С. Ради интереса вы можете попробовать запустить тот же код со следующими параметрами JVM:
-XX:InlineSmallCode=0
Это должно предотвратить оптимизацию, выполняемую JIT (это происходит на сервере точки доступа 7), и вы увидите
true
всегда (я остановился на 2000000, но я полагаю, что после этого она продолжится).Для информации ниже приведен код JIT. Честно говоря, я недостаточно бегло читаю сборку, чтобы знать, действительно ли проведен тест или откуда берутся две нагрузки. (строка 26 - это тест,
flag = a != a
а строка 31 - закрывающая фигурная скобкаwhile(true)
).# {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1' 0x00000000027dcc80: int3 0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0] 0x00000000027dcc8c: data32 data32 xchg ax,ax 0x00000000027dcc90: mov DWORD PTR [rsp-0x6000],eax 0x00000000027dcc97: push rbp 0x00000000027dcc98: sub rsp,0x40 0x00000000027dcc9c: mov rbx,QWORD PTR [rdx+0x8] 0x00000000027dcca0: mov rbp,QWORD PTR [rdx+0x18] 0x00000000027dcca4: mov rcx,rdx 0x00000000027dcca7: movabs r10,0x6e1a7680 0x00000000027dccb1: call r10 0x00000000027dccb4: test rbp,rbp 0x00000000027dccb7: je 0x00000000027dccdd 0x00000000027dccb9: mov r10d,DWORD PTR [rbp+0x8] 0x00000000027dccbd: cmp r10d,0xefc158f4 ; {oop('javaapplication27/TestThreadSafety$1')} 0x00000000027dccc4: jne 0x00000000027dccf1 0x00000000027dccc6: test rbp,rbp 0x00000000027dccc9: je 0x00000000027dcce1 0x00000000027dcccb: cmp r12d,DWORD PTR [rbp+0xc] 0x00000000027dcccf: je 0x00000000027dcce1 ;*goto ; - javaapplication27.TestThreadSafety$1::run@62 (line 31) 0x00000000027dccd1: add rbx,0x1 ; OopMap{rbp=Oop off=85} ;*goto ; - javaapplication27.TestThreadSafety$1::run@62 (line 31) 0x00000000027dccd5: test DWORD PTR [rip+0xfffffffffdb53325],eax # 0x0000000000330000 ;*goto ; - javaapplication27.TestThreadSafety$1::run@62 (line 31) ; {poll} 0x00000000027dccdb: jmp 0x00000000027dccd1 0x00000000027dccdd: xor ebp,ebp 0x00000000027dccdf: jmp 0x00000000027dccc6 0x00000000027dcce1: mov edx,0xffffff86 0x00000000027dcce6: mov QWORD PTR [rsp+0x20],rbx 0x00000000027dcceb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=112} ;*aload_0 ; - javaapplication27.TestThreadSafety$1::run@2 (line 26) ; {runtime_call} 0x00000000027dccf0: int3 0x00000000027dccf1: mov edx,0xffffffad 0x00000000027dccf6: mov QWORD PTR [rsp+0x20],rbx 0x00000000027dccfb: call 0x00000000027a90a0 ; OopMap{rbp=Oop off=128} ;*aload_0 ; - javaapplication27.TestThreadSafety$1::run@2 (line 26) ; {runtime_call} 0x00000000027dcd00: int3 ;*aload_0 ; - javaapplication27.TestThreadSafety$1::run@2 (line 26) 0x00000000027dcd01: int3
источник
0x27dccd1
до0x27dccdf
. Циклjmp
in безусловен (поскольку цикл бесконечен). Единственные две другие инструкции в циклеadd rbc, 0x1
- которые увеличиваютсяcountOfIterations
(несмотря на то, что цикл никогда не будет завершен, поэтому это значение не будет прочитано: возможно, это необходимо, если вы нарушите его в отладчике), .. .test
инструкция, которая на самом деле существует только для доступа к памяти (обратите внимание, чтоeax
она даже не задана в методе!): это специальная страница, которая установлена как нечитаемая, когда JVM хочет запустить все потоки чтобы достичь точки безопасности, чтобы он мог выполнить gc или другую операцию, которая требует, чтобы все потоки находились в известном состоянии.instance. a != instance.a
сравнение из цикла и выполняет его только один раз перед тем, как войти в цикл! Он знает, что перезагружать их не требуетсяinstance
илиa
они не объявлены изменчивыми, и нет другого кода, который мог бы изменить их в том же потоке, поэтому он просто предполагает, что они одинаковы в течение всего цикла, что разрешено памятью. модель.Нет,
a != a
не является потокобезопасным. Это выражение состоит из трех частей: загрузитьa
,a
снова загрузить и выполнить!=
. Другой поток может получить внутреннюю блокировкуa
родительского элемента и изменить значениеa
между двумя операциями загрузки.Другой фактор - это то, является ли
a
это местным. Еслиa
локальный, то никакие другие потоки не должны иметь к нему доступа и, следовательно, должны быть потокобезопасными.void method () { int a = 0; System.out.println(a != a); }
также всегда следует печатать
false
.Объявление
a
asvolatile
не решит проблему для ifa
isstatic
или instance. Проблема не в том, что потоки имеют разные значенияa
, а в том, что один потокa
дважды загружается с разными значениями. Это может фактически сделать случай менее потокобезопасным. Еслиa
нет,volatile
тоa
может быть кэширован, и изменение в другом потоке не повлияет на кешированное значение.источник
synchronized
неверно: для этого кода гарантирована печатьюfalse
, все методы, наборa
должны бытьsynchronized
тоже.a
родительского элемента во время выполнения метода, необходимого для установки значенияa
.Что касается странного поведения:
Поскольку переменная
a
не помечена какvolatile
, в какой-то момент ее значениеa
может быть кэшировано потоком. Обаa
s ofa != a
являются кэшированной версией и, следовательно, всегда одинаковы (flag
теперь это означает всегдаfalse
).источник
Даже простое чтение не атомарно. Если
a
естьlong
и не помечено как,volatile
то на 32-битных JVMlong b = a
не является потокобезопасным.источник