Как реализовать параллельный круговой тикер (счетчик) в Java? - PullRequest
12 голосов
/ 27 сентября 2011

Я хочу реализовать круговой счетчик в Java.Счетчик для каждого запроса должен увеличиваться (атомарно), а при достижении верхнего предела должен сбрасываться до 0.

Каков наилучший способ реализовать это, и существуют ли какие-либо реализации?

Ответы [ 8 ]

20 голосов
/ 27 сентября 2011

Легко реализовать такой счетчик поверх 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;
    }

}
7 голосов
/ 28 сентября 2011

Если вас беспокоит конфликт с использованием CAS или synchronized, вы можете рассмотреть что-то более сложное, например, предложенный JSR 166e LongAdder ( source , javadoc ).

Это простой счетчик с низким уровнем конкуренции при многопоточном доступе.Вы можете обернуть это, чтобы выставить (текущее значение mod max value).То есть не храните упакованное значение вообще.

6 голосов
/ 08 апреля 2015

с 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;
    });      

}

2 голосов
/ 25 января 2013

Если вы используете оператор модуля, вы можете просто увеличить и вернуть модуль.К сожалению, оператор модуля является дорогостоящим, поэтому я рекомендую другие решения, где важна производительность.

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.

2 голосов
/ 27 сентября 2011

Лично я считаю, что решение AtomicInteger немного уродливо, поскольку оно вводит условие гонки, которое означает, что ваша попытка обновления может "провалиться" и должна быть повторена (путем итерации внутри цикла while), делая время обновления менее детерминированным чем выполнение всей операции в критической секции.

Написание собственного счетчика настолько тривиально, что я бы рекомендовал такой подход. Это также лучше с точки зрения ОО, так как оно показывает только те операции, которые вам разрешено выполнять.

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;
  }
}

EDIT

Другая проблема, с которой я столкнулся при решении цикла while, заключается в том, что, учитывая большое количество потоков, пытающихся обновить счетчик, вы можете столкнуться с ситуацией, когда у вас вращается несколько потоков и пытается обновить счетчик. Учитывая, что только 1 поток будет успешным, все другие потоки будут сбои, что приведет к их итерации и потере циклов ЦП.

1 голос
/ 01 мая 2015

Мне нужно создать аналогичный круговой тикер для пользовательской логики маршрутизации 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));
  }
}
1 голос
/ 27 сентября 2011

Вы можете использовать класс java.util.concurrent.atomic.AtomicInteger для атомарного увеличения. Что касается установки верхней границы и отката на 0, вам нужно сделать это внешне ... возможно, инкапсулируя все это в вашем собственном классе оболочки.

На самом деле, кажется, что вы можете использовать compareAndSet, чтобы проверить верхнюю границу, а затем перейти к 0.

0 голосов
/ 19 мая 2015

Для высокоинтенсивного кругового счетчика, увеличиваемого несколькими параллельными потоками, я бы рекомендовал использовать LongAdder (начиная с java 8, см. Основную идею внутри Striped64.java), потому что он более масштабируем по сравнению с AtomicLong. Его легко адаптировать к вышеуказанным решениям.

Предполагается, что операция get не так часта в LongAdder. При вызове counter.get примените к нему counter.get% max_number. Да, операция по модулю обходится дорого, но для этого случая использования она нечаста, что должно амортизировать общую стоимость производительности.

Помните, что операция get не блокирующая и не атомарная.

...