Я знаю, что возможное состояние гонки - чтение-изменение-запись, пример:

public class UnsafeSequence {
     private int value;
     public int getNext(){
          value = value + 1;
          return value;
     }
}

В моем коде я использую AtomicLong:

public class Test {
      private AtomicLong size = new AtomicLong();

      public void test(Object myPersonalObject){
           //do something

           this.size.set(this.size.get() + myPersonalObject.getSize().get());


           //do something
      }
}

myPersonalObject.getSize() возвращает AtomicLong.

Вопрос в том, что эта инструкция this.size.set(this.size.get() + myPersonalObject.getSize().get()); может генерировать состояние гонки?

0
Simone Boccato 1 Сен 2017 в 13:33

6 ответов

Лучший ответ

Ваш this.size.set(this.size.get() + myPersonalObject.getSize().get()); далеко не атомарная операция, даже если вы используете атомарные компоненты.

Когда вы выполнили size.get() и начали добавлять его с помощью myPersonalObject.getSize().get(), исходное значение size можно изменить.

Присвоение значения вычисления обратно size делает изменение исчезающим. Это было бы несколько похоже на этот однопоточный код

int size = 2;
int myObj = 3;
int temp = size + myObj;    // The addition, keeping the result cached
size++;  // Another "thread" making a change
size = temp;   // The assignment, making the i++ disappear

Как я сказал в своем комментарии, используйте size.addAndGet(myPersonalObject.getSize().get()) вместо этого. Не беспокойтесь о возвращаемом значении, вам не нужно использовать его для чего-либо.

2
Kayaman 1 Сен 2017 в 10:47

AtomicLong гарантирует, что результатом get() будет «правильное» содержимое соответствующего объекта (не то, где одна «половина» значения является старым значением, а другая половина исходит из «новое» значение).

Итак, конечно: это «внутреннее» значение объекта AtomicLong может очень хорошо измениться в вашем коде. Сначала вы выбираете его значение, добавляете что-то и записываете его обратно.

Другими словами: если вы обеспокоены тем, что вы всегда set() используете правильное значение (что бы это ни значило в вашем контексте) - тогда ваша реализация не гарантирует этого!

Таким образом: вы можете захотеть использовать обычные длинные значения здесь - и вместо использования AtomicLong ваш код обеспечивает необходимую синхронизацию для поддержки контракта вокруг этого «счетчика».

4
GhostCat 1 Сен 2017 в 11:22

Это не атомное утверждение:

this.size.set(this.size.get() + myPersonalObject.getSize().get());

this.size.set, this.size.get() и myPersonalObject.getSize().get() являются атомарными утверждениями, но цепочка атомарных операторов не будет объединять и распространять свою атомарность, как это делает синхронизированный оператор:

synchronized(this){
  this.size.set(this.size.get() + myPersonalObject.getSize().get());
}

Так что в вашем примере поток this.size.get() может быть выполнен потоком, поток может быть приостановлен, а другой поток может быть выполнен:

this.size.set(this.size.get() + myPersonalObject.getSize().get());,

Без пауз.
Затем, когда первый поток возобновляется, используемое значение this.size.get() может не отражать действительное значение this.size.get().

2
davidxxx 1 Сен 2017 в 10:45

@Kayaman дал правильный, красивый ответ, но также можно сделать реализацию без блокировки:

public void test(Object myPersonalObject){
    //do something

    long currentValue;
    long newValue;
    do {
        currentValue = this.size.get();
        newValue = currentValue + myPersonalObject.getSize().get();
    } while (!size.compareAndSet(currentValue, newValue));

    //do something
}
0
ruslanys 1 Сен 2017 в 10:55

Да, это будет состояние гонки. Атомная реализация гарантирует, что только один поток имеет доступ к полю. Это не означает, что он будет отражать значение, которое может быть изменено другим потоком. Например, Thread 1 получает значение, а Thread 2 получает значение впоследствии, но при выполнении метода set это быстрее. После этого у вас нет ожидаемого результата.

Существует Метод getAndSet специально для этих случаев. Он использует механизм сравнения и обмена, чтобы убедиться, что полученное значение фактически является текущим значением.

0
Murat Karagöz 1 Сен 2017 в 10:43

Нет абсолютно никакой причины изобретать велосипед при использовании java.util.concurrent.atomic

Рассмотрите ручное добавление, как вы заявили изначально

Подход № 1 Java:

public void test(Test myPersonalObject){
        this.size.set(this.size.get() + myPersonalObject.getSize().get());
}

Байт-код для этого метода:

0: aload_0
1: getfield #4 = Field Test.size(Ljava/util/concurrent/atomic/AtomicLong;)
4: aload_0
5: getfield #4 = Field Test.size(Ljava/util/concurrent/atomic/AtomicLong;)
8: invokevirtual #5 = Method java.util.concurrent.atomic.AtomicLong.get(()J)
11: aload_1
12: invokevirtual #6 = Method Test.getSize(()Ljava/util/concurrent/atomic/AtomicLong;)
15: invokevirtual #5 = Method java.util.concurrent.atomic.AtomicLong.get(()J)
18: ladd
19: invokevirtual #7 = Method java.util.concurrent.atomic.AtomicLong.set((J)V)
22: return

И теперь addAndGet подход

Подход №2 Java:

public void test(Test myPersonalObject){
    this.size.addAndGet(myPersonalObject.getSize().get());
}

Байт-код производится:

0: aload_0
1: getfield #4 = Field Test.size(Ljava/util/concurrent/atomic/AtomicLong;)
4: aload_1
5: invokevirtual #5 = Method Test.getSize(()Ljava/util/concurrent/atomic/AtomicLong;)
8: invokevirtual #6 = Method java.util.concurrent.atomic.AtomicLong.get(()J)
11: invokevirtual #7 = Method java.util.concurrent.atomic.AtomicLong.addAndGet((J)J)
14: pop2
15: return

Хотя addAndGet подход производит меньше байтового кода, он также позволяет избежать непоследовательного приращения . Это происходит потому, что существует совершенно отдельное событие чтения myPersonalObject.getSize() - Line 15 из 1- Первый подход и соответствующий line 8 2-го. Затем следует совершенно отдельная инструкция ladd и отдельная set (lines 18-19 первой), по сравнению с addAndGet (line 11 второго подхода).

Теперь рассмотрим диаграмму ниже, пусть myPersonalObject.getSize().get() постоянно возвращает 1 только для упрощения нашего примера:

Подход № 1 (получить размер + получить 1)

Thread 1       |    Thread 2      |     size
               |                  |     0
get 1          |    get 1         |     0
get size       |                  | ←   0
               |    get size      | ←   0
ladd size +1   |                  |     0
               |    ladd size +1  |     0
set back       |                  | →   1
               |    set back      | →   1

Подход №2 addAndGet (получить 1)

Thread 1         |  Thread 2            |   size
                 |                      |   0
get 1            |                      |   0
                 |  get 1               |   0
addAndGet size+1 |                      | → 1
                 |  addAndGet size+1    | → 2
3
Kostiantyn 1 Сен 2017 в 11:47