Я знаю, что возможное состояние гонки - чтение-изменение-запись, пример:
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());
может генерировать состояние гонки?
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())
вместо этого. Не беспокойтесь о возвращаемом значении, вам не нужно использовать его для чего-либо.
AtomicLong
гарантирует, что результатом get()
будет «правильное» содержимое соответствующего объекта (не то, где одна «половина» значения является старым значением, а другая половина исходит из «новое» значение).
Итак, конечно: это «внутреннее» значение объекта AtomicLong может очень хорошо измениться в вашем коде. Сначала вы выбираете его значение, добавляете что-то и записываете его обратно.
Другими словами: если вы обеспокоены тем, что вы всегда set()
используете правильное значение (что бы это ни значило в вашем контексте) - тогда ваша реализация не гарантирует этого!
Таким образом: вы можете захотеть использовать обычные длинные значения здесь - и вместо использования AtomicLong
ваш код обеспечивает необходимую синхронизацию для поддержки контракта вокруг этого «счетчика».
Это не атомное утверждение:
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()
.
@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
}
Да, это будет состояние гонки. Атомная реализация гарантирует, что только один поток имеет доступ к полю. Это не означает, что он будет отражать значение, которое может быть изменено другим потоком. Например, Thread 1
получает значение, а Thread 2
получает значение впоследствии, но при выполнении метода set
это быстрее. После этого у вас нет ожидаемого результата.
Существует Метод getAndSet специально для этих случаев. Он использует механизм сравнения и обмена, чтобы убедиться, что полученное значение фактически является текущим значением.
Нет абсолютно никакой причины изобретать велосипед при использовании 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
Похожие вопросы
Новые вопросы
java
Java - это язык программирования высокого уровня. Используйте этот тег, если у вас возникли проблемы с использованием или пониманием самого языка. Этот тег редко используется отдельно и чаще всего используется вместе с [spring], [spring-boot], [jakarta-ee], [android], [javafx], [hadoop], [gradle] и [maven].