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, мы можем обойти это условие гонки. Мы атомарно проверяем, остается ли текущее значение таким же, как когда мы начинали вычисления. Если да, мы можем безопасно обновить текущее значение. В противном случае нам нужно пересчитать новое значение с измененным текущим значением.