Неправильно работает AtomicInteger в многопоточном счетчике

Есть обычный многопоточный счетчик. Если я использую обычные переменные типа int, то все работает правильно. Но если использую AtomicInteger, ничего не работает правильно. В чем моя ошибка?

Работающий класс:

public class CASCount {

    private final AtomicReference<Integer> count = new AtomicReference<>(0);

    public void increment() {
        int current, next;
        do {
            current = count.get();
            next = current + 1;
        } while (!count.compareAndSet(current, next));
    }

    public int get() {
        return count.get();
    }
}

Не работающий класс:

private final AtomicReference<Integer> count = new AtomicReference<>(0);
    AtomicInteger current = new AtomicInteger();
    AtomicInteger next = new AtomicInteger();

    public void increment() {
        //Integer current, next;
        do {
           current.getAndAdd(count.get());
           next.incrementAndGet();
        } while (!count.compareAndSet(current.get(), next.get()));
    }

    public int get() {
        return count.get();
    }

Тест:

@Test
public void whenUseCASCount() throws InterruptedException {
    CASCount count = new CASCount();
    Thread one = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("one");
            count.increment();
        }
    });

    Thread two = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("two");
            count.increment();
        }
    });
    one.start();
    two.start();
    one.join();
    two.join();
    assertThat(count.get(), is(10));
}

Ответы (1 шт):

Автор решения: Roman Konoval

В не работающем классе логика существенно отличается от правильной. Правильная логика в решении с int-ами и ее суть в том, что (и это общая схема неблокирующих алгоритмов):

  1. делается локальная копию разделяемой переменной
  2. проводится операция по изменению локальной копии
  3. атомарно делается замена значения, если оно все еще такое же как мы видели на шаге 1, иначе повторяем с шага 1

Во втором алгоритме куча проблем.

Первая проблема - это то, что current и next у вас разделяемые, а не локальные. Это значит, что их могут менять потоки одновременно, в отличии от правильного варианта, где они локальные.

Вторая проблема - это то, что логика шага 2 неправильная. Новое значение, которое хранится в next это не будет на 1 увеличенное значение в count. Во-первых, значение в next зависит только от того, сколько раз был вызван цикл, а во вторых, так как это разделяемая переменная, то ее могут изменить другие потоки.

Можно заменить int-ы на AtomicInteger, только в этом совсем нет смысла - int-ы будут работать быстрее:

AtomicInteger current = new AtomicInteger();
AtomicInteger next = new AtomicInteger();
do {
    current.set(count.get());
    next.set(current.get() + 1);
} while (!count.compareAndSet(current.get(), next.get()));

Ну и самое главное. AtomicInteger сам по себе реализует атомарный инкремент. Либо ваша реализация использует факт, что AtomicInt уже атомарный и compareAndSet роли не играет для вашего алгоритма, либо вы их используете для замены обычных int (т.е. делаете их локальными как в примере) - т.е. смысла особого в AtomicInteger нет.

Если суммировать: в задаче на compareAndSet не предполагается использовать AtomicInteger.

→ Ссылка