Как реализовать параллельный круговой тикер (счетчик) в 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 операция неблокирующая, ни атомарная.