Синхронизация по локальным переменным
У меня есть многопоточный Java-код, в котором:
- несколько потоков считывают объекты с сохранением состояния из синхронизированного общего хранилища (т.е. в результате этого некоторые потоки могут ссылаться на одни и те же объекты);
- каждый поток затем вызывает метод
process()
и передает туда свой объект;
-
process()
каким-то образом обрабатывает объекты, что может привести к изменению состояния объектов;
- эти изменения состояния должны быть синхронизированы.
Я создал такой метод:
public void process(Foo[] foos) {
for (final Foo foo : foos) {
if (foo.needsProcessing()) {
synchronized (foo) {
foo.process(); // foo state may be changed here
}
}
}
}
Насколько мне известно, это выглядит законным. Тем не менее, инспекция IntelliJ жалуется на синхронизацию с локальной переменной, потому что "у разных потоков очень много разных локальных экземпляров" (это неверно для меня, поскольку я не инициализирую foos в методе).
По сути, я хочу добиться того же, что и метод Foo.process(), который не является для меня вариантом, поскольку Foo является частью сторонней библиотеки).
Я привык к коду без желтых отметок, поэтому любые советы сообщества оцениваются. Неужели так плохо делать синхронизацию для локальных жителей? Есть ли альтернатива, которая будет работать в моей ситуации?
Спасибо заранее!
Ответы
Ответ 1
if (foo.needsProcessing()) {
synchronized (foo) {
foo.process(); // foo state may be changed here
}
}
Я думаю, что в вышеупомянутом фрагменте есть условие гонки, которое может привести к тому, что foo.process()
иногда вызывается дважды на одном и том же объекте. Это должно быть:
synchronized (foo) {
if (foo.needsProcessing()) {
foo.process(); // foo state may be changed here
}
}
Неужели это так плохо для синхронизации на локальных компьютерах?
Неплохо синхронизировать локальные жители как таковые. Реальные проблемы:
-
синхронизируются ли разные потоки на правильных объектах для обеспечения правильной синхронизации и
-
может ли что-то другое вызвать проблемы, синхронизируя эти объекты.
Ответ 2
У Стивена C есть проблемы, он вводит множество блокировок синхронизации бессмысленно, как ни странно, лучший способ его форматирования:
public void process(Foo[] foos) {
for (final Foo foo : foos) {
if (foo.needsProcessing()) {
synchronized (foo) {
if (foo.needsProcessing()) {
foo.process(); // foo state may be changed here
}
}
}
}
}
Получение блокировки синхронизации иногда может занять некоторое время, и если оно было удержано, что-то что-то меняет. В то время это могло изменить что-то необходимое для обработки foo.
Вы не хотите ждать блокировки, если вам не нужно обрабатывать объект. И после того, как вы получите блокировку, она может не нуждаться в обработке. Поэтому, хотя это выглядит как глупый и начинающий программист, может быть склонен удалить одну из проверок, на самом деле это звуковой способ сделать это, пока foo.needsProcessing() является небрежной функцией.
Возвращая это к основному вопросу, когда это так, вы хотите синхронизировать на основе локальных значений в массиве, потому что время имеет решающее значение. И в этих случаях последнее, что вы хотите сделать, - заблокировать каждый элемент в массиве или обработать данные дважды. Вы бы только синхронизировали локальные объекты, если у вас есть несколько потоков, выполняющих кучу работы, и очень редко нужно касаться одних и тех же данных, но очень хорошо.
Выполнение этого только когда-либо ударит блокировку тогда и только тогда, когда обработка, которая нуждается в обработке, вызовет ошибки concurrency. В основном вы будете хотеть двойной стробированный синтаксис в тех случаях, когда вы хотите синхронизировать, основываясь только на точных объектах массива как таковых. Это предотвращает двойную обработку foos и блокировку любого foo, который не нуждается в обработке.
У вас остался очень редкий случай блокировки потока или даже входа в блокировку и только когда это имеет значение, и ваш поток блокируется только в том месте, где не блокировка приведет к ошибкам concurrency.
Ответ 3
Предполагается, что IDE поможет вам, если это допустит ошибку, вам не следует наклоняться и просить ее.
Вы можете отключить эту проверку в IntelliJ. (Смешно, что он включен только по умолчанию в разделе "Проблемы с потоками". Это самая распространенная ошибка, которую люди совершают?)
Ответ 4
Нет автоматического интеллекта. Я думаю, что ваша идея синхронизации по локальной переменной вполне правильна. Поэтому, возможно, сделайте это по-своему (это правильно) и предложите JetBrains, чтобы они настроили их проверку.
Ответ 5
Реорганизуйте тело цикла в отдельный метод, беря foo
в качестве параметра.
Ответ 6
Нет ничего плохого в вашей синхронизации на объекте внутри коллекции. Вы можете попробовать заменить foreach на обычный цикл:
for (int n = 0; n < Foos.length; n++) {
Foo foo = Foos[n];
if (null != foo && foo.needsProcessing()) {
synchronized (foo) {
foo.process(); // foo state may be changed here
}
}
}
или даже (поэтому детектор не будет отключен через Foo foo
):
for (int n = 0; n < foos.length; n++) {
if (null != foos[n] && foos[n].needsProcessing()) {
synchronized (foos[n]) {
foos[n].process(); // foos[n] state may be changed here
}
}
}
Не использовать временное значение для предотвращения множественного foos[n]
не является лучшей практикой, но если оно работает, чтобы предотвратить нежелательное предупреждение, вы можете с ним жить. Добавьте комментарий, почему этот код имеет отклоненную форму.: -)
Ответ 7
В объектах .NET мир иногда переносит свой объект блокировки как свойство.
synchronized (foo.getSyncRoot()) {
if (foo.needsProcessing()) {
foo.process(); // foo state may be changed here
}
}
Это позволяет объекту выдавать другой объект блокировки в зависимости от его реализации (например, делегирование монитора базовому соединению базы данных или что-то в этом роде).