Есть!= проверить потокобезопасность?



Я знаю, что сложные операции, такие как 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
572   8  

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, а затем выполнение программы в течение длительного времени не генерирует ни одного истинного выхода.

это "странное" поведение согласуется с следующий сценарий выполнения:

  1. программа загружается и JVM запускается перевод байткод. Поскольку (как мы видели из выходных данных javap) байт-код выполняет две нагрузки, вы (по-видимому) иногда видите результаты состояния гонки.

  2. через некоторое время, код компилируется JIT-компилятором. Оптимизатор JIT замечает, что есть две загрузки одного и того же слота памяти (a) близко друг к другу, и оптимизирует второй прочь. (На самом деле, есть шанс, что он полностью оптимизирует тест ...)

  3. теперь состояние гонки больше не проявляется, потому что больше нет двух нагрузок.

обратите внимание, что это все согласуется с тем, что 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 может:

  1. прочитайте "a" два раза, поместите каждый из них в стек, а затем и сравните результаты
  2. прочитайте "a" только один раз, поместите его в стек, продублируйте его (инструкция"dup") и запустите сравнение
  3. полностью исключить выражение и заменить его с 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, load a еще раз, и проанализировать !=. Возможно, что другой поток получит встроенную блокировку на 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-битных JVMs long b = a не является потокобезопасным.

Comments

    Ничего не найдено.