Неправильно работает 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 шт):
В не работающем классе логика существенно отличается от правильной. Правильная логика в решении с int-ами и ее суть в том, что (и это общая схема неблокирующих алгоритмов):
- делается локальная копию разделяемой переменной
- проводится операция по изменению локальной копии
- атомарно делается замена значения, если оно все еще такое же как мы видели на шаге 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.