Java.util.concurrent.atomic.AtomicReference — это класс, предназначенный для обновления переменных потокобезопасным способом. Зачем нам нужен класс AtomicReference
? Почему мы не можем просто использовать переменную? И как мы можем использовать это правильно?
Почему AtomicReference?
Для инструмента, который я пишу, мне нужно определить, был ли объект вызван из нескольких потоков. Я использую следующий неизменяемый класс для этого:
Джава
1
public class State {
2
private final Thread thread;
3
private final boolean accessedByMultipleThreads;
4
public State(Thread thread, boolean accessedByMultipleThreads) {
5
super();
6
this.thread = thread;
7
this.accessedByMultipleThreads = accessedByMultipleThreads;
8
}
9
public State() {
10
super();
11
this.thread = null;
12
this.accessedByMultipleThreads = false;
13
}
14
public State update() {
15
if(accessedByMultipleThreads) {
16
return this;
17
}
18
if( thread == null ) {
19
return new State(Thread.currentThread()
20
, accessedByMultipleThreads);
21
}
22
if(thread != Thread.currentThread()) {
23
return new State(null,true);
24
}
25
return this;
26
}
27
public boolean isAccessedByMultipleThreads() {
28
return accessedByMultipleThreads;
29
}
30
}
Вы можете скачать исходный код всех примеров на GitHub .
Я сохраняю первый поток, обращающийся к объекту, в потоке переменных, строка 2. Когда другой поток обращается к объекту, я устанавливаю переменную accessedByMultipleThreads
в значение true, а переменный поток в ноль, строка 23. Когда переменная accessedByMultipleThreads
равна true, я не изменяю состояние, строка 15 до 17.
Я использую этот класс в каждом объекте, чтобы определить, был ли доступ к нему несколькими потоками. В следующем примере используется состояние в классе UpdateStateNotThreadSafe
:
Джава
xxxxxxxxxx
1
public class UpdateStateNotThreadSafe {
2
private volatile State state = new State();
3
public void update() {
4
state = state.update();
5
}
6
public State getState() {
7
return state;
8
}
9
}
Я сохраняю состояние в состоянии переменной переменной, строка 2. Мне нужно ключевое слово volatile, чтобы убедиться, что потоки всегда видят текущие значения, как описано более подробно здесь .
Чтобы проверить, является ли использование volatile-переменной потокобезопасным, я использую следующий тест:
Джава
1
import com.vmlens.api.AllInterleavings;
2
public class TestNotThreadSafe {
3
4
public void test() throws InterruptedException {
5
try (AllInterleavings allInterleavings =
6
new AllInterleavings("TestNotThreadSafe");) {
7
while (allInterleavings.hasNext()) {
8
final UpdateStateNotThreadSafe object = new UpdateStateNotThreadSafe();
9
Thread first = new Thread( () -> { object.update(); } ) ;
10
Thread second = new Thread( () -> { object.update(); } ) ;
11
first.start();
12
second.start();
13
first.join();
14
second.join();
15
assertTrue( object.getState().isAccessedByMultipleThreads() );
16
}
17
}
18
}
19
}
Мне нужно два потока, чтобы проверить, является ли использование изменяемой переменной поточно-ориентированной, созданной в строках 9 и 10. Я запускаю эти два потока, строки 11 и 12. И затем жду, пока оба не завершатся, используя соединение потоков, строки 13 и 14. После того, как оба потока остановлены, я проверяю, является ли флаг accessedByMultipleThreads истинным, строка 15.
Чтобы проверить все чередования потоков, мы помещаем полный тест в цикл while, итерируя по всем чередованиям потоков, используя класс AllInterleavings из vmlens , строка 7. При выполнении теста я вижу следующую ошибку:
Джава
xxxxxxxxxx
1
java.lang.AssertionError:
2
at org.junit.Assert.fail(Assert.java:91)
3
at org.junit.Assert.assertTrue(Assert.java:43)
4
at org.junit.Assert.assertTrue(Assert.java:54)
Отчет vmlens показывает, что пошло не так:
Проблема состоит в том, что для определенного потока, чередующего оба потока, сначала читают состояние. Таким образом, один поток перезаписывает результат другого потока.
Как использовать AtomicReference?
Чтобы решить это условие гонки, я использую compareAndSet
метод из AtomicReference
.
compareAndSet
Метод принимает два параметра, ожидаемое значение тока и новое значение. Метод атомарно проверяет, равно ли текущее значение ожидаемому значению. Если да, то метод обновляет значение до нового значения и возвращает true. Если нет, метод оставляет текущее значение без изменений и возвращает false.
Идея использования этого метода состоит в том, чтобы позволить compareAndSet
проверять, было ли текущее значение изменено другим потоком, пока мы вычисляли новое значение. Если нет, мы можем безопасно обновить текущее значение. В противном случае нам нужно пересчитать новое значение с измененным текущим значением.
Ниже показано, как использовать compareAndSet
метод для атомарного обновления состояния:
Джава
xxxxxxxxxx
1
public class UpdateStateWithCompareAndSet {
2
private final AtomicReference<State> state =
3
new AtomicReference<State>(new State());
4
public void update() {
5
State current = state.get();
6
State newValue = current.update();
7
while( ! state.compareAndSet( current , newValue ) ) {
8
current = state.get();
9
newValue = current.update();
10
}
11
}
12
public State getState() {
13
return state.get();
14
}
15
}
16
Теперь я использую AtomicReference
для состояния, строка 2. Чтобы обновить состояние, мне сначала нужно получить текущее значение, строка 5. Затем я вычисляю новое значение, строка 6, и пытаюсь обновить AtomicReference
использование compareAndSet
, строка 7. Если обновление прошло успешно, все готово. Если нет, мне нужно снова получить текущее значение, строка 8, и пересчитать новое значение, строка 9. Затем я могу попытаться снова обновить AtomicReference
использование compareAndSet
. Мне нужен цикл while, так как он compareAndSet
может несколько раз потерпеть неудачу.
Как отметил Гжегож Борчух в комментарии к этой статье, начиная с JDK 1.8 более простой в использовании метод в AtomicReference, который достигает того же результата: updateAndGet. Этот метод внутренне использует compareAndSet, используя цикл while для
обновления AtomicReference.
Вывод
Использование изменчивых переменных приводит к условиям состязания, поскольку определенные чередования потоков для потока перезаписывают вычисления других потоков. Используя compareAndSet
метод из класса AtomicReference
, мы можем обойти это условие гонки. Мы атомарно проверяем, остается ли текущее значение таким же, как когда мы начинали вычисления. Если да, мы можем безопасно обновить текущее значение. В противном случае нам нужно пересчитать новое значение с измененным текущим значением.