1. Обзор
В предыдущей статье мы узнали, что AtomicStampedReference может предотвратить проблему ABA .
В этом уроке мы подробнее рассмотрим, как лучше всего его использовать.
2. Зачем Нам Нужна AtomicStampedReference?
Во-первых, AtomicStampedReference предоставляет нам как переменную ссылки на объект, так и штамп, который мы можем читать и записывать атомарно . Мы можем думать о штампе немного как о метке времени или номере версии .
Проще говоря, добавление штампа позволяет нам определить, когда другой поток изменил общую ссылку с исходной ссылки A на новую ссылку B и обратно на исходную ссылку A .
Давайте посмотрим, как он ведет себя на практике.
3. Пример Банковского Счета
Рассмотрим банковский счет, содержащий две части данных: баланс и дату последнего изменения. Дата последнего изменения обновляется при каждом изменении баланса. Наблюдая за этой последней датой изменения, мы можем узнать, что учетная запись была обновлена.
3.1. Считывание значения и его Штамп
Во-первых, давайте представим, что наша ссылка удерживает баланс счета:
AtomicStampedReferenceaccount = new AtomicStampedReference<>(100, 0);
Обратите внимание, что мы предоставили баланс 100 и штамп 0.
Чтобы получить доступ к балансу, мы можем использовать метод AtomicStampedReference.getReference() в нашей переменной account member.
Аналогично, мы можем получить штамп через AtomicStampedReference.get Stamp() .
3.2. Изменение значения и его штампа
Теперь давайте рассмотрим, как установить значение AtomicStampedReference атомарно.
Если мы хотим изменить баланс счета, нам нужно изменить как баланс, так и штамп:
if (!account.compareAndSet(balance, balance + 100, stamp, stamp + 1)) { // retry }
Метод compareAndSet возвращает логическое значение, указывающее на успех или неудачу. Сбой означает, что либо баланс, либо штамп изменились с тех пор, как мы в последний раз его читали.
Как мы видим, легко получить ссылку и штамп, используя их геттеры.
Но, как уже упоминалось выше, нам нужна их последняя версия , когда мы хотим обновить их значения с помощью CAS. Чтобы получить эти две части информации атомарно, нам нужно получить их одновременно.
К счастью, AtomicStampedReference предоставляет нам API на основе массива для достижения этой цели. Давайте продемонстрируем его использование, реализовав метод withdrawal() для нашего класса Account :
public boolean withdrawal(int funds) { int[] stamps = new int[1]; int current = this.account.get(stamps); int newStamp = this.stamp.incrementAndGet(); return this.account.compareAndSet(current, current - funds, stamps[0], newStamp); }
Аналогично, мы можем добавить метод deposit() :
public boolean deposit(int funds) { int[] stamps = new int[1]; int current = this.account.get(stamps); int newStamp = this.stamp.incrementAndGet(); return this.account.compareAndSet(current, current + funds, stamps[0], newStamp); }
Самое приятное в том, что мы только что написали, – это то, что мы можем знать, прежде чем снимать или вносить депозит, что никакая другая нить не изменила баланс, даже до того, каким он был с момента нашего последнего чтения.
Например, рассмотрим следующее чередование потоков:
Баланс установлен на уровне 100 долларов. Поток 1 выполняется депозит(100) до следующей точки:
int[] stamps = new int[1]; int current = this.account.get(stamps); int newStamp = this.stamp.incrementAndGet(); // Thread 1 is paused here
это означает, что депозит еще не завершен.
Затем поток 2 запускает депозит(100) и вывод(100) , доводя баланс до 200 долларов, а затем обратно до 100 долларов.
Наконец, поток 1 запускается:
return this.account.compareAndSet(current, current + 100, stamps[0], newStamp);
Поток 1 успешно обнаружит, что какой-то другой поток изменил баланс счета с момента его последнего чтения, даже если сам баланс такой же, каким он был, когда поток 1 его прочитал.
3.3. Тестирование
Это сложно проверить, так как это зависит от очень специфического чередования потоков. Но давайте, по крайней мере, напишем простой модульный тест, чтобы убедиться, что депозиты и снятие средств работают:
public class ThreadStampedAccountUnitTest { @Test public void givenMultiThread_whenStampedAccount_thenSetBalance() throws InterruptedException { StampedAccount account = new StampedAccount(); Thread t = new Thread(() -> { while (!account.withdrawal(100)) Thread.yield(); }); t.start(); Assert.assertTrue(account.deposit(100)); t.join(1_000); Assert.assertFalse(t.isAlive()); Assert.assertSame(0, account.getBalance()); } }
3.4. Выбор следующей марки
Семантически штамп похож на метку времени или номер версии, поэтому он обычно всегда увеличивается . Также можно использовать генератор случайных чисел.
Причина этого заключается в том, что, если штамп можно изменить на то, что он был ранее, это может привести к нарушению цели AtomicStampedReference . |/AtomicStampedReference сам по себе не применяет это ограничение, поэтому мы должны следовать этой практике.
4. Заключение
В заключение, AtomicStampedReference – это мощная утилита параллелизма, которая предоставляет как ссылку, так и штамп, которые могут быть прочитаны и обновлены атомарно. Он был разработан для обнаружения A-B-A и должен быть предпочтительнее других классов параллелизма, таких как AtomicReference , где проблема A-B-A вызывает беспокойство.
Как всегда, мы можем найти код, доступный на GitHub .