Являются ли массивы потокобезопасными в Java?

Существуют ли какие-либо проблемы параллелизма с одним потоком, читающим из одного индекса массива, в то время как другой поток записывает в другой индекс массива, если индексы разные?

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

 class Test1
 {
     static final private int N = 4096;
     final private int[] x = new int[N];
     final private AtomicInteger nwritten = new AtomicInteger(0);
     // invariant: 
     // all values x[i] where 0 <= i < nwritten.get() are immutable

     // read() is not synchronized since we want it to be fast
     int read(int index) {
         if (index >= nwritten.get())
             throw new IllegalArgumentException();
         return x[index];
     }
     // write() is synchronized to handle multiple writers
     // (using compare-and-set techniques to avoid blocking algorithms
     // is nontrivial)
     synchronized void write(int x_i) {
         int index = nwriting.get();
         if (index >= N)
             throw SomeExceptionThatIndicatesArrayIsFull();
         x[index] = x_i;
         // from this point forward, x[index] is fixed in stone
         nwriting.set(index+1);
     }     
 }

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


person Jason S    schedule 15.07.2009    source источник


Ответы (5)


Хотя вы не получите недопустимое состояние, изменив массивы, как вы упомянули, у вас будет та же проблема, которая возникает, когда два потока просматривают энергонезависимое целое число без синхронизации (см. раздел в Учебнике по Java на Ошибки непротиворечивости памяти). По сути, проблема заключается в том, что поток 1 может записать значение в пространство i, но нет гарантии, когда (или если) поток 2 увидит изменение.

Класс java.util.concurrent.atomic.AtomicIntegerArray делает то, что ты хочешь сделать.

person Kathy Van Stone    schedule 15.07.2009
comment
спасибо... черт, я хотел использовать массив byte[] и похоже, что такого атомного животного не существует... Думаю, я просто буду использовать синхронизированные методы и не буду усложнять. - person Jason S; 15.07.2009
comment
Если у вас намного больше операций чтения, чем записи, вы можете посмотреть java.util.concurrent.locks.ReadWriteLock. - person Kathy Van Stone; 15.07.2009
comment
просто чтобы убедиться, что я понимаю: поэтому, если порядок выполнения ЦП: (1) поток A записывает 33 в x[0], (2) поток A устанавливает безопасный флаг, говорящий, что безопасно читать x[0], (3) поток B считывает безопасный флаг, говоря, что безопасно читать x[0], (4) поток B читает x[0], нет никакой гарантии, что распространение памяти произойдет, даже если я использую синхронизацию для безопасного флага; Я должен использовать синхронизацию таким образом, чтобы в процесс синхронизации входили чтение/запись x[0]? - person Jason S; 15.07.2009
comment
В соответствии со свойствами согласованности памяти (java.sun.com/javase/6/docs/api/java/util/concurrent/), если безопасный флаг всегда ложен до 2, то (1) происходит до (2) и ( 3) происходит раньше (4), потому что они находятся в одном потоке, и (2) происходит раньше (3) из-за синхронизации. Однако, если вы собираетесь использовать блокировку и логическое значение для каждого байта, вам лучше использовать AtomicIntegerArray, поскольку вы будете использовать примерно столько же места (с меньшей сложностью) - person Kathy Van Stone; 15.07.2009
comment
Другой вариант — разбить большой массив байтов на более мелкие фрагменты, но я не думаю, что вам понадобится гораздо больше фрагментов, чем у вас есть ядер (примечание: не тестировалось). Если одна блокировка (даже блокировка чтения-записи) для всего массива слишком медленная, вы можете проверить это. - person Kathy Van Stone; 15.07.2009
comment
(Первые два слова должны были быть «Другая мысль_т_») - person Kathy Van Stone; 15.07.2009
comment
В моем конкретном случае я не ищу безопасный флаг для каждого элемента массива; скорее, я ищу одно защитное целое число для всего массива, чтобы индексы, которые меньше защитного целого числа, никогда не изменялись, и существует (синхронизированный) процесс для продвижения защиты вперед. Но я могу сделать это довольно легко, используя ключевое слово synchronized или блокировки r/w. - person Jason S; 15.07.2009

В примере есть много вещей, которые отличаются от прозаического вопроса.

Ответ на этот вопрос заключается в том, что доступ к отдельным элементам массива осуществляется независимо, поэтому вам не нужна синхронизация, если два потока изменяют разные элементы.

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

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

person kdgregory    schedule 15.07.2009

Я не совсем уверен, будет ли работать синхронизация только метода write, оставляя метод read несинхронизированным. Не совсем так, каковы все последствия, но, по крайней мере, это может привести к тому, что метод read вернет некоторые значения, которые только что были переопределены write.

person Grzegorz Oledzki    schedule 15.07.2009

Да, так как плохое чередование кеша все еще может происходить в многопроцессорной/ядерной среде. Есть несколько вариантов избежать этого:

  • Используйте частную библиотеку Unsafe Sun для атомарной установки элемента в массиве (или добавленную функцию jsr166y в Java7).
  • Использовать массив AtomicXYZ[]
  • Используйте пользовательский объект с одним изменяемым полем и получите массив этого объекта.
  • Вместо этого используйте ParallelArray из дополнения jsr166y в своем алгоритме.
person akarnokd    schedule 15.07.2009

Поскольку read() не синхронизирован, у вас может быть следующий сценарий:

Thread A enters write() method
Thread A writes to nwriting = 0;
Thread B reads from nwriting =0;
Thread A increments nwriting. nwriting=1
Thread A exits write();

Поскольку вы хотите гарантировать, что ваши адреса переменных никогда не конфликтуют, как насчет чего-то вроде (без учета проблем с индексом массива):

int i;
synchronized int curr(){ return i; }
synchronized int next(){ return ++i;}

int read( ) {
         return values[curr()];
     }

void write(int x){
   values[next()]=x;
}
person Steve B.    schedule 15.07.2009
comment
спасибо, но это не мой вопрос, и ваш сценарий не может произойти (шаги 2 и 3 никогда не будут выполнены) - person Jason S; 15.07.2009