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