Рубрики
Без рубрики

Руководство по AtomicStampedReference в Java

Узнайте, как использовать класс AtomicStampedReference в Java.

Автор оригинала: Mark Thomas.

1. Обзор

В предыдущей статье мы узнали, что AtomicStampedReference может предотвратить проблему ABA .

В этом уроке мы подробнее рассмотрим, как лучше всего его использовать.

2. Зачем Нам Нужна AtomicStampedReference?

Во-первых, AtomicStampedReference предоставляет нам как переменную ссылки на объект, так и штамп, который мы можем читать и записывать атомарно . Мы можем думать о штампе немного как о метке времени или номере версии .

Проще говоря, добавление штампа позволяет нам определить, когда другой поток изменил общую ссылку с исходной ссылки A на новую ссылку B и обратно на исходную ссылку A .

Давайте посмотрим, как он ведет себя на практике.

3. Пример Банковского Счета

Рассмотрим банковский счет, содержащий две части данных: баланс и дату последнего изменения. Дата последнего изменения обновляется при каждом изменении баланса. Наблюдая за этой последней датой изменения, мы можем узнать, что учетная запись была обновлена.

3.1. Считывание значения и его Штамп

Во-первых, давайте представим, что наша ссылка удерживает баланс счета:

AtomicStampedReference account = 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 .