Безопасна ли проверка! =?

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

Ответ 1

В отсутствие синхронизации этот код

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, сделанные другим потоком, будут видны текущему потоку.

Ответ 2

Является ли проверка a != a потокобезопасной?

Если a потенциально может быть обновлен другим потоком (без правильной синхронизации!), то Нет.

Я попытался запрограммировать это и использовать несколько потоков, но не смог. Я думаю, не мог имитировать гонку на моей машине.

Это ничего не значит! Проблема в том, что если выполнение, в котором a обновляется другим потоком, разрешено JLS, тогда код не является потокобезопасным. Тот факт, что вы не можете заставить условие гонки произойти с конкретным тестовым сценарием на конкретной машине и конкретной реализацией Java, не исключает ее возникновения в других обстоятельствах.

Означает ли это, что a!= a может возвращать true.

Да, теоретически, при определенных обстоятельствах.

В качестве альтернативы a != a может возвращать false, хотя a менялся одновременно.


Относительно "странного поведения":

Когда моя программа начинается между некоторыми итерациями, я получаю значение выходного флага, а это означает, что ссылка!= проверка не выполняется с той же ссылкой. НО после некоторых итераций выход становится постоянным значением false, а затем выполнение программы в течение длительного времени не генерирует ни одного истинного вывода.

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

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

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

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

Обратите внимание, что все это согласуется с тем, что JLS позволяет реализовать реализацию Java.


@kriss прокомментировал так:

Похоже, это может быть то, что программисты C или С++ называет "Undefined Behavior" (зависит от реализации). Похоже, в таких случаях, как это, может быть несколько UB в java.

Модель памяти Java (указанная в JLS 17.4) определяет набор предварительных условий, при которых одному потоку гарантированно будут отображаться значения памяти, записанные другим потоком. Если один поток пытается прочитать переменную, написанную другим, и эти предварительные условия не выполняются, тогда может быть множество возможных исполнений... некоторые из которых могут быть неверными (с точки зрения требований приложения). Другими словами, определен набор возможных поведений (т.е. Набор "хорошо сформированных исполнений" ), но мы не можем сказать, какое из этих поведений произойдет.

Компилятору разрешено комбинировать и перезаписывать нагрузки и сохранять (и делать другие вещи), если конечный эффект кода тот же:

  • при выполнении одним потоком и
  • при выполнении разными потоками, которые синхронизируются правильно (в соответствии с моделью памяти).

Но если код не синхронизируется должным образом (и поэтому отношения "происходит до" недостаточно ограничивают множество хорошо сформированных исполнений), компилятору разрешается переупорядочивать нагрузки и хранилища способами, которые давали бы "неправильные" Результаты. (Но это на самом деле просто говорит, что программа неверна.)

Ответ 3

Доказано с помощью 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 вызовах. Итак НЕТ, НЕ потокобезопасен

Ответ 4

Нет, это не так. Для сравнения виртуальная машина Java должна поместить два значения в стек и выполнить команду сравнения (которая зависит от типа "a" ).

Виртуальная машина Java может:

  • Прочитайте "a" два раза, поместите каждый в стек и затем сравните результаты.
  • Прочитайте "a" только один раз, поместите его в стек, дублируйте его (команда "dup" ) и запустите сравнение
  • Полностью исключить выражение и заменить его на false

В первом случае другой поток может изменить значение "a" между двумя считанными.

Какая стратегия выбрана, зависит от компилятора Java и Java Runtime (особенно JIT-компилятора). Это может даже измениться во время выполнения вашей программы.

Если вы хотите узнать, как обращаться к переменной, вы должны сделать ее volatile (так называемый "барьер с половинной памятью" ) или добавить полный барьер памяти (synchronized). Вы также можете использовать некоторый API уровня hgiher (например, AtomicInteger, как упоминалось Juned Ahasan).

Подробнее о безопасности потоков читайте JSR 133 (Модель памяти Java).

Ответ 5

Все это хорошо объяснено Стивеном К. Для удовольствия вы можете попробовать запустить тот же код со следующими параметрами JVM:

-XX:InlineSmallCode=0

Это должно предотвратить оптимизацию, выполняемую JIT (она выполняется на сервере hotspot 7), и вы увидите true навсегда (я остановился на 2 000 000, но я полагаю, что это продолжается после этого).

Для информации ниже приведен код 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::[email protected] (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::[email protected] (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::[email protected] (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::[email protected] (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::[email protected] (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::[email protected] (line 26)
  0x00000000027dcd01: int3   

Ответ 6

Нет, a != a не является потокобезопасным. Это выражение состоит из трех частей: load a, load a и выполнить !=. Другой поток может получить внутреннюю блокировку на родительском элементе a и изменить значение a между двумя операциями загрузки.

Другим фактором является то, является ли a локальным. Если a является локальным, то никакие другие потоки не должны иметь к нему доступ и поэтому должны быть потокобезопасными.

void method () {
    int a = 0;
    System.out.println(a != a);
}

также должна всегда печатать false.

Объявление a как volatile не решит проблему, если a есть static или экземпляр. Проблема заключается не в том, что потоки имеют разные значения a, но один поток загружает a дважды с разными значениями. Это может сделать ситуацию менее безопасной для потоков. Если a не volatile, тогда a может быть кэширован, а изменение другого потока не повлияет на кешированное значение.

Ответ 7

Относительно странного поведения:

Так как переменная a не помечена как volatile, в какой-то момент ее значение может быть кэшировано потоком a. Оба a of a != a являются тогда кэшированной версией и, следовательно, всегда одинаковы (значение flag теперь всегда false).

Ответ 8

Даже простое чтение не является атомарным. Если a long и не помечено как volatile, то на 32-разрядных JVM long b = a не является потокобезопасным.