Есть!= проверить потокобезопасность?
Я знаю, что сложные операции, такие как 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 из возможных поведений (т. е. набор "хорошо сформированных казней") определяется, но мы не можем сказать, какое из этих поведений произойдет.
компилятор может комбинировать и переупорядочивать нагрузки и сохранять (и делать другие вещи) при условии, что конечный эффект кода одинаков:
- при исполнении 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 вызовов. Так что нет, это не потокобезопасным
нет, это не так. Для сравнения виртуальная машина Java должна поместить два значения для сравнения в стек и запустить инструкцию compare (которая зависит от типа "a").
Java VM может:
- прочитайте "a" два раза, поместите каждый из них в стек, а затем и сравните результаты
- прочитайте "a" только один раз, поместите его в стек, продублируйте его (инструкция"dup") и запустите сравнение
- полностью исключить выражение и заменить его с
falseв первом случае другой поток может изменить значение для "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, loadaеще раз, и проанализировать!=. Возможно, что другой поток получит встроенную блокировку наaродитель и изменить значениеaмежду 2 операциями загрузки.еще один фактор, хотя ли
aлокальная. Еслиaявляется локальным, то никакие другие потоки не должны иметь к нему доступа и поэтому должны быть потоком безопасный.void method () { int a = 0; System.out.println(a != a); }также следует всегда печатать
false.объявления
aкакvolatileне решит проблему, еслиaиstaticили экземпляра. Проблема не в том, что потоки имеют разные значенияa, но что один поток загружаетaдва раза с разными значениями. Это может фактически сделать случай менее потокобезопасным.. Еслиaнеvolatileзатемaможет быть кэширован, и изменение в другом потоке не повлияет на кэшированное значение.
Что касается странного поведения:
так как переменная
aне помечается какvolatileв какой-то момент это может значениеaможет быть кэширован потоком. Обаaсa != aтогда кэшированная версия и, следовательно, всегда одна и та же (что означаетflagвсегдаfalse).
даже простое чтение не является атомарной. Если
aиlongи не помечен какvolatileзатем на 32-битных JVMslong b = aне является потокобезопасным.
Comments