Как реализовать параллельный круговой тикер (счетчик) в Java?
Я хочу реализовать круговой счетчик в Java. Счетчик по каждому запросу должен увеличиваться (атомарно) и при достижении верхнего предела должен переворачиваться до 0.
каков был бы лучший способ реализовать это и существуют ли какие-либо существующие реализации?
8 ответов
Если вы так беспокоитесь о споре, используя либо CAS, либо synchronized
тогда вы могли бы рассмотреть что-то более сложное, как предлагаемый JSR 166e LongAdder
(источник, javadoc).
это простой счетчик с низким уровнем конкуренции при многопоточном доступе. Вы можете обернуть это, чтобы выставить (текущее значение mod max value). То есть, не храните завернутое значение вообще.
легко реализовать такой счетчик на 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;
}
}
С 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;
});
}
Я лично думаю, что 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 loop, заключается в том, что при большом количестве потоков, пытающихся обновить счетчик, вы можете оказаться в ситуации, когда у вас есть несколько живых потоков, вращающихся и пытающихся обновить счетчик. Учитывая, что только 1 поток будет успешным, все остальные потоки не смогут заставить их повторять и тратить циклы процессора.
Если вы используете оператор модуля, вы можете просто увеличить и вернуть модуль. К сожалению, оператор модуля стоит дорого, поэтому я рекомендую другие решения, где важна производительность.
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;
}
}
вам придется решить длинный.Корпус массив также.
можно использовать java.util.concurrent.atomic.AtomicInteger
класс для атомарного увеличения. как для установки верхней границы и отката назад к 0
, вам нужно будет сделать это внешне...возможно, помещая все это в свой собственный класс-оболочку.
на самом деле, похоже, что вы можете использовать compareAndSet
чтобы проверить верхнюю границу, а затем перевернуться на 0
.
Я должен создать аналогичный круговой тикер для пользовательской логики маршрутизации 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));
}
}
для высокоинтенсивного кругового счетчика, увеличенного несколькими потоками параллельно, я бы рекомендовал использовать LongAdder
(начиная с java 8, см. Основную идею внутри Striped64.java
), потому что он более масштабируемый по сравнению с AtomicLong
. Легко приспособить его к вышеуказанным решениям.
предполагается, что get
операция не так часто в LongAdder
. При вызове counter.get
, применить к нему счетчик.получайте % max_number'. Да, операция по модулю стоит дорого, но для этого она нечаста use-case, который должен амортизировать общую стоимость исполнения.
помни, что get
операция неблокирующая, ни атомарная.