Несколько полей: 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.

Неизменный подход также имеет смысл.

Другие вопросы по тегам