Java - неизменный потокобезопасный массив - PullRequest
10 голосов
/ 08 июля 2011

У меня есть вопрос, касающийся модели памяти Java. Вот простой класс, представляющий проблему:

public class ImmutableIntArray {

    private final int[] array;

    public ImmutableIntArray() {
        array = new int[10];
        for (int i = 0; i < 10; i++) {
            array[i] = i;
        }
    }

    // Will always return the correct value?
    public int get(int index) {
        return array[index];
    }

}

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

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

Ответы [ 4 ]

11 голосов
/ 08 июля 2011

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

Амир упоминается volatile, что обычно полезно- но конструктор также имеет аналогичную семантику для final переменных, которые обеспечивают видимость.Подробности см. В JLS разделе 17.5 - по сути, конструктор формирует отношение случай-до между записью в конечные переменные и любыми последующими чтениями.

РЕДАКТИРОВАТЬ: Таким образом, вы устанавливаете ссылку values ​​ на массив в конструкторе, он виден во всех потоках в этой точке и затем не изменяется.Итак, мы знаем, что все остальные потоки будут видеть тот же массив .Но как насчет содержимого массива ?

В сущности, элементы массива не имеют какой-либо особой семантики в отношении волатильности, они как если бы вы только что объявили класс себе как-то так::

public class ArrayTen {
    private int _0;
    private int _1;
    // ...
    private int _9;

    public int get(int index) {
       if (index == 0) return _0;
       // etc.
    }
}

Итак, другой поток увидит эти переменные только в том случае, если мы сможем что-то сделать для установления отношения случай-до .И если мое понимание верно, это требует лишь небольшого изменения вашего исходного кода.

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

public class ImmutableIntArray {

    private final int[] array;

    public ImmutableIntArray() {
        int[] tmp = new int[10];
        for (int i = 0; i < 10; i++) {
            tmp[i] = i;
        }
        array = tmp;
    }

    // get() etc.
}

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

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

3 голосов
/ 08 июля 2011

ваш пример не совсем верен.чтобы получить окончательную полевую гарантию, вам необходимо:

public ImmutableIntArray() {
    int tmparray = new int[10];
    for (int i = 0; i < 10; i++) {
        tmparray[i] = i;
    }
    array = tmparray;
}
2 голосов
/ 08 июля 2011

Я думаю, что вам предоставляется та же семантика для массива, что и для окончательной ссылки на объект.Спецификация заявляет

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

В нем также указано

Также будут отображаться версии любого объекта или массива, на которые ссылаются эти последние поля, которые по крайней мере так же актуальны, как и последние поля.1011 *

http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5

0 голосов
/ 12 декабря 2014

Я думаю, что ваши изменения массива будут видны с вашим ImmutableIntArray.Из моего чтения JLS действие [freeze] должно происходить при выходе из конструктора.Использование временного массива, я думаю, бесполезно:

int tmparray = new int[10];
for (int i = 0; i < 10; i++) {
    tmparray[i] = i;
}
array = tmparray;

Для получения окончательных полевых гарантий нам потребуется [freeze] где-то перед выходом из конструктора:

int tmparray = new int[10];
for (int i = 0; i < 10; i++) {
    tmparray[i] = i;
}
array = tmparray;
[freeze]

В любом случае, [freeze]] оставляет ворота открытыми, чтобы переупорядочить инструкции над ними, поэтому у нас будет то же самое:

int tmparray = new int[10];
array = tmparray; 
for (int i = 0; i < 10; i++) {
    tmparray[i] = i;
}
[freeze]

[freeze] реализован так, чтобы содержать как минимум [StoreStore].Этот барьер [StoreStore] должен быть выпущен до момента публикации созданного экземпляра.

Из Поваренная книга JSR-133 :

Нельзя перемещать хранилища финалов внутри конструкторов ниже хранилища вне конструктора, которое может сделать объект видимым для других потоков.(Как видно ниже, это также может потребовать создания барьера).Точно так же вы не можете изменить порядок одного из первых двух с третьим назначением в: v.afield = 1;x.finalField = v;...;sharedRef = x;

И я думаю, что это делается с помощью (JSR-133 Cookbook) :

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

Таким образом, мы не можем сохранить в sharedRef до того, как все остальные хранилища контуров будут сделаны.

Вы можете искать по: "Транзитивные гарантии из конечных полей "в (спецификация JSR133) .

...