Как реализовать параллельный циклический тикер (счетчик) в Java?
Я хочу реализовать круговой счетчик в Java.
Счетчик по каждому запросу должен увеличиваться (атомарно) и при достижении верхнего предела должен перескакивать до 0.
Каким будет лучший способ реализовать это и существуют ли какие-либо существующие реализации?
Ответы
Ответ 1
Если вы беспокоитесь о конкуренции, используя CAS или synchronized
, тогда вы можете рассмотреть нечто более сложное, как предлагаемый источник JSR 166e LongAdder
(, javadoc).
Это простой счетчик с низким уровнем конкуренции при многопоточном доступе. Вы можете обернуть это, чтобы выставить (текущее значение max max). То есть, не сохраняйте завернутое значение вообще.
Ответ 2
Легко реализовать такой счетчик на вершине AtomicInteger
:
public class CyclicCounter {
private final int maxVal;
private final AtomicInteger ai = new AtomicInteger(0);
public CyclicCounter(int maxVal) {
this.maxVal = maxVal;
}
public int cyclicallyIncrementAndGet() {
int curVal, newVal;
do {
curVal = this.ai.get();
newVal = (curVal + 1) % this.maxVal;
} while (!this.ai.compareAndSet(curVal, newVal));
return newVal;
}
}
Ответ 3
С Java 8
public class CyclicCounter {
private final int maxVal;
private final AtomicInteger counter = new AtomicInteger(0);
public CyclicCounter(int maxVal) {
this.maxVal = maxVal;
}
return counter.accumulateAndGet(1, (index, inc) -> {
return ++index >= maxVal ? 0 : index;
});
}
Ответ 4
Я лично считаю, что решение AtomicInteger
немного уродливо, так как оно вводит условие гонки, которое означает, что попытка обновления может "потерпеть неудачу" и должна быть повторена (путем итерации в цикле while), делая время обновления менее детерминированным чем выполнение всей операции в критическом разделе.
Написание собственного счетчика настолько тривиально, что я рекомендую этот подход. Это лучше с точки зрения OO, так как оно предоставляет только те операции, которые вам разрешено выполнять.
public class Counter {
private final int max;
private int count;
public Counter(int max) {
if (max < 1) { throw new IllegalArgumentException(); }
this.max = max;
}
public synchronized int getCount() {
return count;
}
public synchronized int increment() {
count = (count + 1) % max;
return count;
}
}
ИЗМЕНИТЬ
Другая проблема, которую я воспринимаю с помощью решения while, заключается в том, что, учитывая большое количество потоков, пытающихся обновить счетчик, вы можете столкнуться с ситуацией, когда у вас есть несколько живых потоков, вращающихся и пытающихся обновить счетчик. Учитывая, что только один поток будет успешным, все остальные потоки не приведут к их итерации и сбоям циклов процессора.
Ответ 5
Если вы используете оператор модуля, вы можете просто увеличивать и возвращать модуль. К сожалению, оператор модуля дорог, поэтому я рекомендую другие решения, в которых важна производительность.
public class Count {
private final AtomicLong counter = new AtomicLong();
private static final long MAX_VALUE = 500;
public long getCount() {
return counter.get() % MAX_VALUE;
}
public long incrementAndGet(){
return counter.incrementAndGet() % MAX_VALUE;
}
}
Вам также придется решать вопрос Long.MAX_VALUE.
Ответ 6
Вы можете использовать класс java.util.concurrent.atomic.AtomicInteger
для увеличения атома. Что касается установки верхней границы и возврата к 0
, вам нужно сделать это извне... возможно, инкапсулировать все это в свой собственный класс-оболочку.
На самом деле, вы можете использовать compareAndSet
, чтобы проверить верхнюю границу, а затем перевернуться на 0
.
Ответ 7
Мне нужно создать аналогичный циклический тикер для пользовательской логики маршрутизации Akka, которая должна отличаться от стандартных по умолчанию, чтобы избежать сетевых издержек, так как моя логика - просто выбрать следующий маршрут.
Примечание: Скопировано из предлагаемой реализации Java 8:
import akka.routing.Routee;
import akka.routing.RoutingLogic;
import scala.collection.immutable.IndexedSeq;
import java.util.concurrent.atomic.AtomicInteger;
public class CircularRoutingLogic implements RoutingLogic {
final AtomicInteger cycler = new AtomicInteger();
@Override
public Routee select(Object message, IndexedSeq<Routee> routees) {
final int size = routees.size();
return size == 0 ? null : routees.apply(cycler.getAndUpdate(index -> ++index < size ? index : 0));
}
}
Ответ 8
Для высокоинтенсивного циркулярного счетчика, увеличенного несколькими потоками параллельно, я бы рекомендовал использовать LongAdder
(начиная с java 8, см. основную идею внутри Striped64.java
), потому что он более масштабируемый по сравнению с AtomicLong
. Его легко адаптировать к вышеуказанным решениям.
Предполагается, что операция get
не так часто встречается в LongAdder
. При вызове counter.get
примените к нему "counter.get% max_number". Да, modulo-operation стоит дорого, но для этого варианта использования это редкость, что должно амортизировать общие затраты на производительность.
Помните, что операция get
не является блокирующей, а не атомной.