Два потока обновляют один и тот же объект, будет ли это работать? java

#java #concurrency #thread-safety

#java #параллелизм #безопасность потоков

Вопрос:

У меня есть два метода следующим образом:

 class A{
  void method1(){
    someObj.setSomeAttribute(true);
    someOtherObj.callMethod(someObj);
  }
  void method2(){
    someObj.setSomeAttribute(false);
    someOtherObj.callMethod(someObj);
  }
}
  

где в другом месте вычисляется этот атрибут:

 class B{
  void callMethod(Foo someObj){
    if(someObj.getAttribute()){
        //do one thing

    } else{
        //so another thing
    }
  }
}
  

Обратите внимание, что A.method1 и A.method2 обновляют атрибут одного и того же объекта. Если эти 2 метода выполняются в 2 потоках, будет ли это работать или будут неожиданные результаты?

Комментарии:

1. » будут ли неожиданные результаты «, будет ли это неожиданным или нет, зависит от того, что вы ожидаете.

2. Будет ли это работать да, будут ли неожиданные результаты, скорее всего. Вам нужно взглянуть на теорию параллелизма и понять такие вещи, как условия гонки. Вы не можете гарантировать порядок, в котором будут выполняться два потока, и вам нужно будет подумать, что это значит, если порядок изменится. Вам также необходимо рассмотреть, как работает модель памяти JVM, поскольку изменение состояния объекта может не быть «обновлено» для других потоков до некоторого момента в будущем (это довольно расплывчато, и кто-то может описать это лучше, но вы хотели бы исследовать такие вещи, как volatile )

Ответ №1:

Будут ли неожиданные результаты? Да, гарантировано, в том смысле, что если вы измените вещи, которые вы не хотели бы влиять на ваше приложение (например, фазу луны, текущую песню, играющую в вашем winamp, обнимается ли ваша собака возле процессора, если это 5-й вторник месяца и другие подобныевещи), которые могут повлиять на поведение. Чего вы не хотите.

То, что вы описали, является так называемым нарушением модели памяти Java: конечным результатом является то, что любая реализация Java может возвращать любое из нескольких значений, и, тем не менее, эта виртуальная машина работает должным образом в соответствии со спецификацией Java. Даже если это происходит, по-видимому, произвольно.

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

Каждый раз, когда он считывает или записывает из любого поля, он переворачивает эту среднюю монету. В heads он будет использовать фактическое поле. В tails он будет использовать созданную им локальную копию.

Это немного упрощает модель, но это хорошее начало, чтобы попытаться понять, как это работает.

Выход состоит в том, чтобы принудительно установить так называемые отношения «предшествует»: что будет делать java, так это убедиться, что то, что вы можете наблюдать, соответствует этим отношениям: если событие A определено как имеющее отношение предшествует событию B, тогда все, что сделал A, будет наблюдаться точно так же, как есть, с помощьюB, гарантировано. Больше никаких подбрасываний монет.

Примеры установления предшествующих отношений включают использование volatile , synchronized , и любые методы, которые используют эти вещи внутри.

Примечание: Конечно. если ваш setSomeAttribute метод, который вы не вставляли, включает в себя какое-то действие, предшествующее установлению, то здесь нет проблем, но, как правило, вызываемый метод setX этого делать не будет.

Пример того, который не:

 class Example {
  private String attr;

  public void setAttr(String attr) {
    this.attr = attr;
  }
}
  

некоторые примеры тех, которые делают:

  • Допустим, метод B.callMethod выполняется в том же потоке, method1 что и — тогда вы гарантированно, по крайней мере, заметите внесенное изменение method1, хотя это все равно подбрасывание монеты (независимо от того, видите ли вы на самом деле, что сделал method2 или нет). Что было бы невозможно, так это увидеть значение этого атрибута перед запуском method1 или method2, потому что код, выполняемый в одном потоке, предшествует всему запуску (любая строка, которая выполняется перед другой в том же потоке, имеет предшествующее отношение).

  • Метод set выглядит следующим образом:

 class Example {
  private String attr;
  private final Object lock = new Object();

  public void setAttr(String attr) {
    synchronized (lock) {
      this.attr = attr;
    }
  }

  public String getAttr() {
    synchronized (lock) {
      return this.attr;
    }
  }
}
  

Теперь блокировка операций get и set для одного и того же объекта — это один из способов установить comes-before. Какой поток первым попал в блокировку, является наблюдаемым поведением; если набор method1 попал туда раньше, чем B get , тогда вы гарантированно будете наблюдать набор method1.

В более общем плане совместное использование состояния между потоками чрезвычайно сложно, и вы должны стараться этого не делать. Альтернативы:

  • Инициализируйте все состояния перед запуском потока, затем выполните задание и только после его завершения передайте все результаты обратно. Fork / join делает это.
  • Используйте систему обмена сообщениями, которая имеет отличные основы параллелизма, такие как база данных, в которой есть транзакции, или библиотеки очередей сообщений.
  • Если вам нужно поделиться состоянием, попробуйте написать вещи в терминах хороших классов в ju.concurrent.

Комментарии:

1. Да, поэтому вместо обновления одного и того же объекта создание его клона и передача 2 разных объектов будут работать правильно?

2. Конечно; если какой-либо объект наблюдается и изменяется исключительно из одного потока, тривиально у вас не будет никаких проблем.

Ответ №2:

Я предполагаю, что вы ожидали, что когда вы вызываете A.method1 , someObj.getAttribute() вернется true B.callMethod , когда вы вызываете A.method2 , someObj.getAttribute() вернется false B.callMethod .

К сожалению, это не сработает. Поскольку между строкой setSomeAttribute и callMethod другим потоком может измениться значение атрибута.

Если вы используете только атрибут callMethod , почему бы просто не передать атрибут вместо Foo объекта. Код следующим образом:

 class A{
  void method1(){
    someOtherObj.callMethod(true);
  }
}
class B{
  void callMethod(boolean flag){
    if(flag){
        //do one thing
    } else{
        //so another thing
    }
  }
}
  

Если вы должны использовать Foo в качестве параметра, что вы можете сделать, так это make setAttribute и callMethod atomic .
Самый простой способ добиться этого — синхронизировать его.Код следующим образом:

   synchronized void method1(){
    someObj.setSomeAttribute(true);
    someOtherObj.callMethod(someObj);
  }
  synchronized void method2(){
    someObj.setSomeAttribute(false);
    someOtherObj.callMethod(someObj);
  }
  

Но это может иметь плохую производительность, вы можете добиться этого с помощью более мелкозернистой блокировки.