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

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

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

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

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

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

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

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