Это!= проверить потокобезопасность?
Я знаю, что сложные операции, такие как i++
не являются потокобезопасными, поскольку они включают несколько операции.
но проверка ссылки сама по себе является потокобезопасной операцией?
a != a //is this thread-safe
Я попытался запрограммировать это и использовать несколько потоков, но он не потерпел неудачу. Наверное, я не мог симулировать гонку на своей машине.
EDIT:
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
8 ответов
при отсутствии синхронизации этот код
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
для локальных vars дважды, это неатомная операция, если a
был изменен между другим потоком сравнение может произвести false
.
кроме того, проблема видимости памяти актуальна здесь, нет никакой гарантии, что изменения в a
сделанный другим потоком будет виден текущий поток.
Регистрация
a != a
потокобезопасным?
если a
потенциально может быть обновлен другим потоком (без надлежащей синхронизации!), то нет.
я попробовал программу и использовать несколько потоков, но не получится. Я думаю, не мог имитировать гонку на моей машине.
это ничего не значит! Проблема в том, что если выполнение, в котором a
обновляется другим потоком разрешено by JLS, то код не является потокобезопасным. Тот факт, что вы не можете заставить условие гонки произойти с конкретным тестовым случаем на конкретной машине и конкретной реализацией Java, не исключает его возникновения в других обстоятельствах.
означает ли это, что a != а может вернуться
true
.
да, теоретически, при определенных обстоятельствах.
кроме того, a != a
может вернуться false
хотя a
был меняется одновременно.
относительно "странного поведения":
поскольку моя программа запускается между некоторыми итерациями, я получаю значение выходного флага, что означает, что ссылка != проверка не выполняется по той же ссылке. Но после некоторых итераций выход становится постоянным значением false, а затем выполнение программы в течение длительного времени не генерирует ни одного истинного выхода.
это "странное" поведение согласуется с следующий сценарий выполнения:
программа загружается и JVM запускается перевод байткод. Поскольку (как мы видели из выходных данных javap) байт-код выполняет две нагрузки, вы (по-видимому) иногда видите результаты состояния гонки.
через некоторое время код компилируется компилятором JIT. Оптимизатор JIT замечает, что есть две загрузки одного и того же слота памяти (
a
) близко друг к другу, и оптимизирует второй. (На самом деле, есть шанс, что он полностью оптимизирует тест ...)теперь состояние гонки больше не проявляется, потому что больше нет двух нагрузок.
обратите внимание, что это все в соответствии с тем, что JLS позволяет реализовать Java.
@kriss прокомментировал так:
похоже, это может быть то, что C или c++ программисты называют "неопределенное поведение" (зависит от реализации). Похоже, что в Java может быть несколько UB в угловых случаях, таких как этот.
модель памяти Java (указана в JLS 17.4) задает набор предварительных условий, при которых один поток гарантированно видит значения памяти, записанные другим потоком. Если один поток пытается прочитать переменную, написанную другим, и эти предварительные условия не выполняются, то может быть несколько возможные казни ... некоторые из них, вероятно, будут неверными (с точки зрения требований приложения). Другими словами, set возможного поведения (т. е. набор "хорошо сформированных казней") определен, но мы не можем сказать, какое из этих поведений произойдет.
компилятору разрешено комбинировать и переупорядочивать нагрузки и сохранять (и делать другие вещи) при условии, что конечный эффект кода одинаков:
- при выполнении одна нить, и
- при выполнении различными потоками, которые синхронизируются правильно (в соответствии с моделью памяти).
но если код не синхронизируется должным образом (и, следовательно, отношения "происходит до" недостаточно ограничивают набор хорошо сформированных исполнений), компилятору разрешено переупорядочивать нагрузки и хранилища способами, которые дали бы "неправильные" результаты. (Но это действительно просто говорит о том, что программа неверна.)
доказано с помощью 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 вызовов. Так что нет, это не thread safe
нет, это не так. Для сравнения Java VM должна поместить два значения для сравнения в стек и запустить инструкцию compare (которая зависит от типа "a").
Java VM может:
- прочитайте " a " два раза, поместите каждый из них в стек, а затем сравните результаты
- прочитайте "a" только один раз, поместите его в стек, дублируйте его (инструкция "dup") и запустите compare
- полностью исключите выражение и замените его с
false
в 1-м случае другой поток может изменить значение "a" между двумя считываниями.
какая стратегия выбрана, зависит от компилятора Java и среды выполнения Java (особенно компилятора JIT). Он может даже измениться во время выполнения вашей программы.
если вы хотите убедиться, как доступ к переменной, вы должны сделать это volatile
(так называемый "половинный барьер памяти") или добавьте полный барьер памяти (synchronized
). Вы также можете использовать некоторые API уровня hgiher (например,AtomicInteger
как отметил Juned Ahasan).
подробнее о безопасности резьбы читайте экспертная группа JSR 133 (Модель Памяти Java).
все это хорошо объяснил Стивен С. Для удовольствия вы можете попробовать запустить тот же код со следующими параметрами JVM:
-XX:InlineSmallCode=0
Это должно предотвратить оптимизацию, выполняемую JIT (это происходит на сервере hotspot 7), и вы увидите true
навсегда (я остановился на 2,000,000, но я полагаю, что это продолжается после этого).
для информации ниже приведен код JIT'Ed. Честно говоря, я не читаю сборку достаточно бегло, чтобы знать, действительно ли тест выполнен или откуда эти два груза. (строка 26 тест flag = a != a
и строка 31 является заключительной скобкой while(true)
).
# {method} 'run' '()V' in 'javaapplication27/TestThreadSafety'
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')}
0x00000000027dccc4: jne 0x00000000027dccf1
0x00000000027dccc6: test rbp,rbp
0x00000000027dccc9: je 0x00000000027dcce1
0x00000000027dcccb: cmp r12d,DWORD PTR [rbp+0xc]
0x00000000027dcccf: je 0x00000000027dcce1 ;*goto
; - javaapplication27.TestThreadSafety::run@62 (line 31)
0x00000000027dccd1: add rbx,0x1 ; OopMap{rbp=Oop off=85}
;*goto
; - javaapplication27.TestThreadSafety::run@62 (line 31)
0x00000000027dccd5: test DWORD PTR [rip+0xfffffffffdb53325],eax # 0x0000000000330000
;*goto
; - javaapplication27.TestThreadSafety::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::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::run@2 (line 26)
; {runtime_call}
0x00000000027dcd00: int3 ;*aload_0
; - javaapplication27.TestThreadSafety::run@2 (line 26)
0x00000000027dcd01: int3
нет, a != a
не является потокобезопасным. Это выражение состоит из трех частей: нагрузка a
загрузите a
еще раз, и проанализировать !=
. Возможно, что другой поток получит встроенную блокировку на a
' s родитель и изменить значение a
между 2 операциями загрузки.
еще один фактор, хотя является ли a
локальная. Если a
является локальным, тогда никакие другие потоки не должны иметь доступа к нему и, следовательно, должны быть потоком безопасный.
void method () {
int a = 0;
System.out.println(a != a);
}
также всегда следует печатать false
.
объявления a
as volatile
не решит проблему для if a
и static
или экземпляра. Проблема не в том, что потоки имеют разные значения a
, но этот поток загружает a
дважды с различными значениями. Это может сделать дело менее безопасным.. Если a
не volatile
затем a
может быть кэширован, и изменение в другом потоке не повлияет на кэшированное значение.
относительно странного поведения:
С переменной a
не помечается как volatile
в какой-то момент это может значение a
может быть кэширован потоком. Оба!--0-->с a != a
затем кэшированную версию, а так всегда одинаково (в смысле flag
всегда false
).
даже простое чтение не является атомарной. Если a
is long
и не помечен как volatile
тогда на 32-битной JVMs long b = a
не является потокобезопасным.