Несколько полей: volatile или AtomicReference?
Я должен синхронизировать доступ между потоками к общему объекту, состояние которого состоит из нескольких полей. Сказать:
class Shared{
String a; Integer b;
//constructor, getters and setters
....
}
Я, возможно, много потоков, читающих эти объекты, делая
//readers
shared.getA();
shared.getB();
и только один поток, который будет писать в определенный момент:
//writer
shared.setA("state");
shared.setB(1);
теперь мой вопрос заключается в том, чтобы гарантировать, что читающие потоки не найдут общий объект в несовместимом состоянии.
Я прочитал много ответов, говорящих, что для согласованности между тем volatile
это решение, но я не уверен, как это работает на нескольких полях. Например, этого достаточно?
volatile String a; volatile Integer b;
Другое решение - сделать общий объект неизменным и использовать AtomicReference, например,
AtomicReference<Shared> shared = ....
и тогда автор просто поменяет ссылку:
Shared prev = shared.get();
Shared newValue = new Shared("state",1);
while (!shared.compareAndSet(prev, newValue))
Это правильный подход? Спасибо!
Обновление В моих настройках общие объекты извлекаются из ConcurrentHashMap<Id,Shared>
Таким образом, комментарии согласны с тем, что можно использовать неизменный подход или синхронизировать обновления совместно используемых ресурсов. Однако для полноты картины было бы неплохо узнать, является ли приведенное выше решение с ConcurrentHashMap<Id,AtomicReference<Shared>>
является жизнеспособным или неправильным или просто лишним. Кто-нибудь может объяснить? Спасибо!
4 ответа
Прежде всего, вы должны сделать Shared
неизменный:
class Shared{
private final String a;
private final int b;
//constructor, getters and NO setters
}
И если у вас есть только один писатель, которого вы можете безопасно использовать volatile, в AtomicRefference нет необходимости. В тот момент, когда информация обновляется, старый объект не должен быть изменен, а скорее новый, созданный и присвоенный изменчивой ссылке.
Как @Mikhail говорит в своем ответе, делая Shared
неизменный и заменяющий весь объект - хороший подход. Если вы не хотите или не можете использовать этот подход по какой-то причине, вы можете просто убедиться, что все поля Shared
защищены одним и тем же замком, и что они только когда-либо модифицируются вместе (см. update
в моем примере), тогда их невозможно увидеть в несогласованном состоянии.
например
class Shared {
private String a;
private String b;
public synchronized String getA() {
return a;
}
public synchronized String getB() {
return b;
}
public synchronized void update(String a, String b) {
this.a = a;
this.b = b;
}
}
Маркировка полей изменчивыми или синхронизация методов не гарантируют атомарность.
Писатель должен позаботиться об атомности.
Writer должен вызывать все сеттеры (которые должны обновляться атомарно) внутри синхронизированного блока. синхронизированный (общий) { shared.setA() shared.setB() ... }
Для этого все синхронизаторы в разделяемом объекте тоже должны быть синхронизированы.
Если вам нужно написать и A, и B вместе, чтобы они соответствовали друг другу, например, это имя и номер социального страхования, один из подходов заключается в использовании synchronized
везде и напиши единый, комбинированный сеттер.
public synchronized void setNameAndSSN(String name, int ssn) {
// do validation checking etc...
this.name = name;
this.ssn = ssn;
}
public synchronized String getName() { return this.name; }
public synchronized int getSSN() { return this.ssn; }
В противном случае читатель может "увидеть" объект с новым именем, но со старым SSN.
Неизменный подход также имеет смысл.