Одновременная проблема среди потоков - PullRequest
1 голос
/ 17 июля 2010

Предположим, у меня есть переменная экземпляра, которая имеет исходное значение:

Integer mMyInt = 1;

Есть два потока.

Первое изменение mMyInt путем вызова:

void setInt() {
    mMyInt = 2;
}

Второй поток получает mMyInt, вызывая:

Integer getInt() {
  return mMyInt;
}

Оба потока не используют синхронизацию.

Мои вопросы: каково возможное значение, которое второй поток может получить от getInt ()?Это может быть только 1 или 2?Может ли оно быть нулевым?

Спасибо

Ответы [ 2 ]

10 голосов
/ 17 июля 2010

РЕДАКТИРОВАТЬ: Важное обновление благодаря @ irreputable.

Если объект не скрылся во время строительства (см. Ниже), присваивание mMyInt=1 происходит перед любым доступом к получателю / установщику. Также в Java назначение объектов является атомарным (есть вероятность того, что вы заметите какой-то неверный назначенный адрес. Будьте осторожны, потому что 64-битные примитивные назначения, такие как double и long, не являются атомарными).

Таким образом, в этом случае возможное значение равно 1 или 2.

Объект может убежать во время строительства в такой ситуации:

 class Escape {
    Integer mmyInt = 1;

    Escape(){
        new Thread(){
            public void run(){
                System.out.println(Escape.this.mmyInt);
            }
        }.start();
    }
 }

Хотя на практике это, вероятно, случается редко, в вышеупомянутом случае новый поток может наблюдать не полностью построенный объект Escape и, таким образом, в теории получить значение mmyInt null (AFAIK вы все равно не будете получить некоторую случайную ячейку памяти).

Что если это HashMap объект? Переменная экземпляра mMyMap имеет первоначальное значение. Затем первый поток вызывает "mMyMap = new HashMap ();" Второй поток вызывает "возврат mMyMap; "Может ли второй поток получить null, или он может быть только оригинальным или новый объект HashMap?

Когда «Назначение ссылки на объект является атомарным», это означает, что вы НЕ будете наблюдать промежуточное назначение. Это либо значение до, либо значение после. Таким образом, если единственное присвоение, которое происходит, это map = someNonNullMap(); после завершения строительства (и поле было присвоено ненулевое значение во время строительства), и объект не избежал во время строительства, вы не сможете наблюдать null.

Обновление: Я проконсультировался с экспертом по параллелизму, и, по его словам, модель памяти Java позволяет компиляторам переупорядочивать присваивания и конструирование объектов (хотя на практике я полагаю, что это крайне маловероятно).

Так, например, в приведенном ниже случае thread1 может выделить некоторую кучу, присвоить некоторое значение map, продолжение строительства map. Тем временем поток 2 приходит и наблюдает частично построенный объект.

class Clever {
   Map map;

   Map getMap(){
       if(map==null){
           map = deriveMap();        }
       return map;
   }
}

JDK имеет аналогичную конструкцию в классе String (не точная цитата):

class String {
   int hashCode = 0;

   public int hashCode(){
       if(hashCode==0){
           hashCode = deriveHashCode();
    }
       return hashCode;
   }
}

Это работает, потому что, по мнению тех же экспертов по параллелизму, энергонезависимый кэш является примитивным, а не объектом.

Этих проблем можно избежать, если ввести отношения до того, как они возникнут. В вышеупомянутых случаях это можно сделать, объявив членов volatile. Также для 64-битных примитивов их объявление volatile сделает их назначение атомарным.

0 голосов
/ 17 июля 2010
// somewhere
static YourClass obj;

//thread 1
obj = new YourClass();

// thread 2
if(obj!=null)
    obj.getInt();

теоретически, поток 2 может получить ноль.

...