Синхронизация элементов в массиве

Я новичок в многопоточности в Java и не совсем понимаю, что происходит.

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

В моей программе несколько потоков обновили массив чисел, поэтому я создал массив из Long объектов:

synchronized (grid[arrayIndex]){
    grid[arrayIndex] += a.getNumber();
}

Этот код находится внутри метода run() класса потока, который я расширил. Массив, сетка, используется всеми моими потоками. Однако это не возвращает правильных результатов при выполнении одной и той же программы в одном потоке.


person user929404    schedule 06.09.2012    source источник


Ответы (4)


Так не пойдет. Важно понимать, что grid[arrayIndex] += ... фактически заменяет элемент в grid новым объектом. Это означает, что вы синхронизируете объект в массиве, а затем сразу же заменяете объект другим в массиве. Это заставит другие потоки заблокировать другой объект, чтобы они не блокировались. Вы должны заблокировать постоянный объект.

Вместо этого вы можете заблокировать весь объект массива, если он никогда не заменяется другим объектом массива:

synchronized (grid) {
    // this changes the object to another Long so can't be used to lock
    grid[arrayIndex] += a.getNumber();
}

Это одна из причин, почему блокировка объекта final является хорошим шаблоном. Смотрите этот ответ с более подробной информацией:

Почему не рекомендуется синхронизировать по логическим значениям?

person Gray    schedule 06.09.2012
comment
Вы правы, но блокировать весь массив может быть излишним. Вы можете настроить одноуровневый массив с объектами блокировки final Object[] locks = new Object[arrayLength]; for (int i = 0; i‹ arrayLength; i++) { locks[i] = new Object(); } затем синхронизировано (locks[arrayIndex]) { grid[arrayIndex] += a.getNumber(); } - person ashirley; 06.09.2012
comment
@ashirley Стоит отметить, что элементы массива никогда не должны изменяться. Он должен быть инициализирован, заполнен и больше никогда не изменяться. К сожалению, объявления его как final недостаточно, чтобы гарантировать, что его внутренние элементы не изменятся. - person Brian; 06.09.2012
comment
ОП защищает только назначение массива @ashirley. Детализация блокировки в этом случае не имеет значения. - person Gray; 06.09.2012
comment
@ Брайан, да, лучше, чем ничего - person ashirley; 06.09.2012
comment
@Gray Правда, но стоит отметить, что это превратится в более длительную операцию. - person ashirley; 06.09.2012
comment
На самом деле это параллельная программа, поэтому для ускорения работы нельзя заблокировать весь массив. @ashirley, твое предложение кажется хорошим. Однако при попытке реализовать его я продолжаю получать исключения NullPointerException в потоках. Я инициализировал массив блокировок перед вызовом потоков, а потоки продолжают выдавать исключения NullPointerException. Однако я могу получить доступ к объектам из основного метода, который вызывает потоки. - person user929404; 06.09.2012
comment
@ user929404, я надеюсь, что это параллельная программа, иначе вам придется беспокоиться о synchronized. Стоимость самого вызова synchronized намного больше, чем степень детализации блокировки, которую вы предположительно сохраняете с помощью массива блокировок. Создание массива блокировок выполняется редко и никогда для защиты только назначения массива. Если вы выполняли больше работы внутри блока synchronized, тогда все в порядке, но в остальном делайте это проще. Вы не ускоряете его, добавляя массив - гарантировано. Взгляните на мой значок значка многопоточности, если есть какие-либо сомнения относительно моего опыта. - person Gray; 06.09.2012
comment
@user929404 user929404 Предположительно, перед тем, как вам нужно будет взять блокировку, обновить массив и снять блокировку, выполняется другая работа. Эта работа все еще может происходить параллельно. Грей прав, это не будет улучшением, когда единственное, для чего вам нужен замок, это простое задание, вам нужно будет сделать что-то более сложное, удерживая замок, прежде чем оно того стоит. Кроме того, блокировки должны быть созданы до запуска потоков, иначе существует риск того, что объекты блокировки не будут видны потокам. Самый простой способ сделать это - в статическом блоке. - person ashirley; 06.09.2012
comment
@ashirley и (это не позволит мне пометить серым), я вижу, что блокировка для одного оператора не нужна. Но даже когда я реализовал массив замков, я все равно получаю неверные результаты - person user929404; 06.09.2012
comment
@gray Разве блокировка всего массива (сетки) не сделает программу последовательной, поскольку только один поток сможет обновлять массив за раз? Это то, что я ожидал, и все же я все еще получаю неверные результаты - person user929404; 06.09.2012
comment
Блокировка всего массива сделает назначения последовательными @user929404. Вот почему вы это делаете. Все остальные операции потока выполняются параллельно. Например, вы можете захотеть переместить метод a.getNumber() из блока synchronized, если он выполняет больше, чем просмотр поля. Какие неправильные результаты вы видите? - person Gray; 06.09.2012
comment
@Gray Под неверными результатами я подразумеваю, что они неверны. Я сравниваю последовательную версию алгоритма и параллельную версию, работающую в одном потоке. Это также обучающее упражнение, поэтому я хотел заблокировать отдельные объекты массива (чтобы лично увидеть разницу во времени). Но в настоящее время ничего не работает. Я заблокировал весь массив и получил неправильные ответы. Я даже поместил вышеупомянутый код в синхронизированный метод, но все равно получаю неправильные ответы. - person user929404; 07.09.2012
comment
@Gray Мне очень жаль тратить ваше время, но я сделал глупую ошибку, которая озадачила меня на несколько часов. Раньше в моем коде я вводил data.length вместо datalength, что все испортило. - person user929404; 07.09.2012
comment
Нп @user929404. Это хорошая причина, чтобы убедиться, что ваша верблюжья оболочка хороша. dataLength может не так сильно запутать. :-) - person Gray; 07.09.2012

Другой вариант — использовать массив объектов AtomicLong и использовать их методы addAndGet() или getAndAdd(). Вам не потребуется синхронизация для увеличения ваших объектов, и несколько объектов могут быть увеличены одновременно.

person JB Nizet    schedule 06.09.2012
comment
Хороший @JB. Конечно, вы все еще платите цену за барьер памяти. - person Gray; 06.09.2012

Java-класс Long неизменен, вы не можете изменить его значение. Итак, когда вы выполняете действие:

grid[arrayIndex] += a.getNumber();

он не меняет значение grid[arrayIndex], которое вы фиксируете, а фактически создает новый объект Long и устанавливает его значение в старое значение плюс a.getNumber. Таким образом, вы получите разные потоки, синхронизирующиеся с разными объектами, что приводит к результатам, которые вы видите.

person matt freake    schedule 06.09.2012

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

Правильнее будет один из этих двух вариантов:

private final int[] grid = new int[10];

synchronized (grid) {
    grid[arrayIndex] += a.getNumber();
}

Если grid не может быть final:

private final Object MUTEX = new Object();

synchronized (MUTEX) {
    grid[arrayIndex] += a.getNumber();
}

Если вы используете второй вариант и grid не является final, любое назначение grid также должно быть синхронизировано.

synchronized (MUTEX) {
    grid = new int[20];
}

Всегда синхронизируйте что-то окончательное, всегда синхронизируйте как доступ, так и модификацию, и как только вы это сделаете, вы можете начать изучать другие механизмы блокировки, такие как Lock, ReadWriteLock и Semaphore. Они могут обеспечить более сложные механизмы блокировки, чем синхронизация, которая лучше подходит для сценариев, где одной синхронизации Java по умолчанию недостаточно, например, для блокировки данных в системе с высокой пропускной способностью (блокировка чтения/записи) или блокировка в пулах ресурсов (подсчет семафоров).

person Brian    schedule 06.09.2012
comment
Не согласен с последним абзацем. Эти замки требуют гораздо более тщательного программирования (попробуй/наконец), поэтому их нельзя назвать более безопасными или более предсказуемыми, и они не избегают многих проблем. Верно и обратное. Более мощный и в определенных ситуациях более производительный, да. - person Gray; 06.09.2012
comment
Также в первом вы не синхронизируетесь по номеру, вы синхронизируетесь по объекту. Вы также используете значение слова, которое вводит в заблуждение. - person Gray; 06.09.2012
comment
@Gray Я уступлю слово «безопасно» и удалю его. Пока вы следуете правильным шаблонам для любой синхронизации, она будет безопасной. Что касается предсказуемости, встроенная синхронизация Java является непредсказуемой. Поток, который получит монитор, если его ожидают два или более потока, является случайным. В других системах я могу включить справедливость и поставить потоки в очередь и установить блокировку в порядке очереди вместо произвольного порядка, предотвращая такие вещи, как голодание потока. Что касается вашего второго комментария, я согласен, формулировка изменена. - person Brian; 06.09.2012
comment
Интересный RE: предсказуемость, но я подозреваю, что это академическая проблема, связанная с определением языка @Brian. Знаете ли вы какие-либо ситуации, когда это верно на практике? Я думаю, что по этой причине крайне опасно использовать Lock вместо synchronized. Особенно когда вы консультируете начинающих программистов потоков по SO. - person Gray; 06.09.2012
comment
@Gray Насколько я понимаю, первый поток, который планировщик пробуждает и блокирует на мониторе, — это тот, который его получает, поэтому на практике это возможно, если алгоритм планировщика процессора пробуждает потоки в том порядке, который всегда ставит один из потоков позади других. Я предполагаю, что в более практическом смысле большинство систем (Windows и *NIX) имеют хорошие алгоритмы планирования, которые, вероятно, обходят более серьезные проблемы голодания. Но приятно иметь гарантию при работе с системами, где вы хотите, чтобы блокировки обслуживались по принципу FIFO, например, в чувствительных ко времени системах запроса/ответа. - person Brian; 06.09.2012
comment
Я пишу такие системы некоторое время без проблем. Я подозреваю, что проблемы с программированием - это больше проблема @Brian. Например, по следующей ссылке. В любом случае, я кое-что узнал, так что спасибо. stackoverflow.com/a/2960811/179850 - person Gray; 06.09.2012
comment
@Gray Спасибо за ссылку :) Это зависит от системы, но я согласен, обычно реализация блокировки не так важна, как просто ее наличие, и поэтому большинство систем, вероятно, справятся без справедливости. Однако знать, что справедливость существует, не так уж и плохо. - person Brian; 06.09.2012
comment
Согласен @Брайан. Я просто беспокоюсь, что какой-то парень заменит все свои synchronized блоки вызовами Lock из-за такого поста. определение преждевременной оптимизации. Отвечая на ТАК вопросы, обязательно осознайте свою аудиторию. :-) - person Gray; 06.09.2012
comment
@Gray Конечно, поскольку вы так выразились, я изменил ответ, чтобы отразить варианты использования других механизмов, и просто удалил другую формулировку, чтобы не быть предвзятым. Спасибо за ваш вклад, я ценю это :) - person Brian; 06.09.2012
comment
Твоя переформулировка выглядит намного лучше, @Brian. Спасибо чувак. - person Gray; 06.09.2012