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




get current (8)

Я знаю, что сложные операции, такие как i++ , не являются потокобезопасными, поскольку они связаны с несколькими операциями.

Но проверяет ли ссылка на себя безопасную по потоку операцию?

a != a //is this thread-safe

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

РЕДАКТИРОВАТЬ:

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

Answers

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

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

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

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

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

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

Подробнее о безопасности потоков см. JSR 133 ( Java Memory Model ).


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

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

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

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

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

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

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

Что касается «странного поведения»:

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

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

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

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

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

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

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

Это похоже на то, что программисты на C или C ++ называют «Неопределенное поведение» (зависит от реализации). Похоже, в таких случаях, как этот, может быть несколько UB в java.

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

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

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

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


Что касается странного поведения:

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


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


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

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

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

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

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


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

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


Доказано с помощью теста:

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 приглашений. Так что НЕТ , это НЕ потокобезопасно


Как вы писали, судя по реализации, forEach() является потокобезопасным для коллекций, поставляемых с JDK (см. Отказ от ответственности ниже), поскольку для этого требуется мониторинг мьютекса для продолжения.

Является ли это также безопасным потоком по спецификации?

Мое мнение - нет, и вот объяснение. Collections.synchronizedXXX() javadoc, переписанный короткими словами, говорит - «все методы являются потокобезопасными, за исключением тех, которые используются для итерации по нему».

Мой другой, хотя очень субъективный аргумент - это то, что yshavit написал - если не сказал / не прочитал, рассмотрите API / класс / безотносительно к потокобезопасности.

Теперь давайте более подробно рассмотрим javadocs. Я предполагаю, что могу указать, что метод forEach() используется для итерации по нему, поэтому, следуя рекомендациям javadoc, мы должны считать это не потокобезопасным, хотя оно противоположно реальности (реализации).

В любом случае, я согласен с утверждением yshavit о том, что документация должна быть обновлена, так как это скорее всего документация, а не ошибка реализации. Но никто не может точно сказать, кроме разработчиков JDK, см. Ниже.

Последнее, что я хотел бы упомянуть в этом обсуждении, - мы можем предположить, что пользовательская коллекция может быть обернута Collections.synchronizedXXX() , а реализация forEach() этой коллекции может быть ... может быть любой. Коллекция может выполнять асинхронную обработку элементов внутри метода forEach() , порождать поток для каждого элемента ... он ограничен только воображением автора, а синхронизированный (mutex) wrap не может гарантировать безопасность потоков для таких случаев . Эта конкретная проблема может быть причиной не объявлять метод forEach() как поточно-безопасный.





java multithreading thread-safety atomic race-condition