RateLimiter в гуаве Интерпретация принципа ограничения тока

задняя часть база данных алгоритм Безопасность

Есть два способа создать RateLimiter

  1. Создайте взрывной путь

  2. Создайте метод WarmingUp

Следующий исходный код взят из guava-17.0.

Bursty

//初始化
RateLimiter r = RateLimiter.create(1); 
//不阻塞
r.tryAcquire();
//阻塞
r.acquire()

RateLimiter.create делает две вещи, чтобы создать объект Bursty и установить скорость до конца процесса инициализации.

RateLimiter rateLimiter = new Bursty(ticker, 1.0 /* maxBurstSeconds */); //ticker默认使用自己定义的
rateLimiter.setRate(permitsPerSecond);
  1. Новый Взрывной объект. Он указывает, что максимальное время, которое может быть сохранено, велико, например, время установки составляет 1 с, тогда предположение допускает количество выпущенных токенов в секунду, равное 2, может хранить максимальное количество 2;
  2. установить скорость. Через частную внутреннюю блокировку, чтобы гарантировать, что изменение скорости является потокобезопасным.
    synchronized (mutex) {
      //1:查看当前的时间是否比预计下次可发放令牌的时间要大,如果大,更新下次可发放令牌的时间为当前时间
      resync(readSafeMicros());
      //2:计算两次发放令牌之间的时间间隔,比如1s中需要发放5个,那它就是 200000.0微秒
      double stableIntervalMicros = TimeUnit.SECONDS.toMicros(1L) / permitsPerSecond;
      this.stableIntervalMicros = stableIntervalMicros;
      //3:设置maxPermits和storedPermits
      doSetRate(permitsPerSecond, stableIntervalMicros);
    }
    

повторная синхронизация исходного кода

private void resync(long nowMicros) {
  // 查看当前的时间是否比预计下次可发放令牌的时间要大,如果大,更新下次可发放令牌的时间为当前时间
  if (nowMicros > nextFreeTicketMicros) {
    storedPermits = Math.min(maxPermits,
        storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
    nextFreeTicketMicros = nowMicros;
  }
}

исходный код doSetRate

@Overridevoid doSetRate(double permitsPerSecond, double stableIntervalMicros) {
 double oldMaxPermits = this.maxPermits;
 maxPermits = maxBurstSeconds * permitsPerSecond;
 storedPermits = (oldMaxPermits == 0.0)
     ? 0.0 // 初始条件存储的是没有
    storedPermits * maxPermits / oldMaxPermits;
}

Во время всего процесса инициализации ключевой информацией является:

  • nextFreeTicketMicros предсказывает время выпуска следующего токена, stableIntervalMicros — временной интервал между двумя выпусками токена.

  • maxPermits Максимальное количество токенов, которое может быть сохранено. storagePermits Количество сохраненных токенов.

Почему NextFreeTicketMicros?

Самый простой способ поддерживать частоту запросов в секунду — запомнить время последнего запроса, а затем убедиться, что 1/QPS в секунду истекло, когда приходит другой запрос. Например, если QPS составляет 5 раз в секунду, вам нужно только убедиться, что время двух запросов истекло в течение 200 мс. нужно 15 жетонов за один раз, требуемое время составляет 3 с. Однако для системы, которая давно не востребована, такой метод проектирования нецелесообразен. Рассмотрим сценарий: если RateLimiter генерирует 1 токен в секунду, он никогда не использовался, и вдруг приходит запрос, требующий 100 токенов, не очень разумно выбирать ожидание 100 с перед выполнением этого запроса, лучше способ сделать это, чтобы выполнить его немедленно, а затем отложить следующий запрос на 100 секунд.

Поэтому сам RateLimiter записывает не время последнего запроса, а время следующего ожидаемого запуска (nextFreeTicketMicros).

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

Почему существует токен, представляющий, сколько токенов хранится?

Такое же внимание уделяется сценариям, которые не использовались в течение длительного времени. Если долго нет запросов, а вдруг они приходят, то эти запросы нужно выпускать сразу в это время? Длительные периоды бездействия могут означать две вещи:

  1. Многие ресурсы простаивают, например, если в течение длительного времени нет запроса к сети, скорее всего, ее буфер пуст, в это время можно ускорить передачу и улучшить коэффициент ее использования.
  2. Иногда мгновенная вспышка приводит к переполнению. Например, срок действия кеша на сервисе истек, и необходимо выполнить запрос к базе данных. Эта стоимость очень «дорогая», и слишком много запросов приведет к тому, что база данных не сможет поддерживать Это.

Ratelimiter использует StoredPermits для моделирования неадекватности прошлых запросов. Его правила хранения следующие: Предполагая, что Ratelimiter генерирует токен каждую секунду, если нет запроса на каждую секунду, ратиметр не будет потреблять его, а StaredPermits увеличится на 1. Предполагая, что в течение 10 секунд нет запроса, StoredPermits становится 10 (предполагая Maxpermits> 10). В это время, если вы хотите получить 3 жетона, вы будете использовать токены в StoredPermits для обработки, а затем его значение становится 7, после В то время как, если вызывается (10), некоторые из них получат 7 разрешений от StoredPermits, а остальные 3 нужно будет регенерировать.

В общем случае RateLimiter предоставляет переменную StoredPermits, которая, когда ресурс полностью используется, равна 0 и может увеличиться до maxStoredPermits. Токен, необходимый для запроса, поступает из двух мест: сохраненные разрешения (токен, хранящийся при простое) и свежие разрешения (существующий токен).

Как измерить процесс получения токенов из storePermits?

Также предположим, что RateLimiter производит только один токен в секунду.В нормальных условиях, если есть 3 запроса одновременно, весь процесс будет длиться 3 секунды. Рассмотрим сценарий, когда долго нет запросов:

  1. Ресурсы бесплатны. В это время система может выдержать определенное количество запросов.Конечно, есть надежда, что запрос может быть предоставлен быстрее в пределах допустимого диапазона.То есть, если есть сохраненный токен, по сравнению с вновь сгенерированным токеном , есть надежда, что это может быть более эффективным в это время.Более быстрое приобретение токенов, то есть потребление времени на получение токенов из токенов хранилища в это время меньше, чем на создание новых токенов, что приводит к более быстрым соответствующим запросам
  2. Мгновенный поток слишком велик. В настоящее время нежелательно использовать сохраненный токен слишком быстро, и есть надежда, что это может занять больше времени, чем создание нового токена, так что запрос может быть относительно гладким.

Анализ показывает, что для разных сценариев необходимо делать разную обработку для получения StorePermits.Реализацией Ratelimiter является функция StoredPermitsToWaitTime, которая устанавливает модельную функцию получения токенов и временных затрат из StorePermits, а стоимость измерения времени - через Интегрируем модельную функцию Например, изначально хранилось 10 токенов, а теперь нужно 3 токена, а осталось 7 токенов, тогда потраченное время есть интеграл функции из интервала 7-10.

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

принцип реализации storagePermitsToWaitTime

Сам storePermits используется для измерения неиспользованного времени. Когда токен не используется, скорость хранения (количество токенов, хранимых в единицу времени) сохраняется один раз каждый раз, когда он не используется: rate=permites/time . Другими словами, 1/ставка = время/разрешения, тогда вы можете получить (1/ставка)*разрешения для измерения стоимости времени.

Выберите (1/ставка) в качестве базового уровня

  • Если выбрать строку над ним, это будет медленнее, чем получение из свежих разрешений;
  • Если он ниже базового уровня, это быстрее, чем получить его из свежих разрешений;
  • Так получилось, что это базовый уровень, поэтому скорость, полученная из StoredPermits, точно такая же, как вновь сгенерированная скорость;

Реализация функции Bursty StoredPermitsToWaitTime

long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
  return 0L;
}

Он напрямую возвращает 0, то есть ниже базового уровня, скорость получения storePermits выше, чем у нового поколения, и объем хранилища можно получить сразу

WarmingUp

//初始化
RateLimiter r =RateLimiter.create(1,1,TimeUnit.SECONDS);
//不阻塞
r.tryAcquire();
//阻塞
r.acquire()

Метод create создает объект WarmingUp, и это только устанавливает соответствующую скорость

RateLimiter rateLimiter = new WarmingUp(ticker, warmupPeriod, unit);
rateLimiter.setRate(permitsPerSecond);

По сравнению с Bursty, у него есть еще один параметрwarmupPeroid, который будет преобразован в микросекундное хранилище с предоставленной единицей измерения в качестве единицы времени. setRate похож на Bursty, но предоставляет другую реализацию в doSetRate.

void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
  double oldMaxPermits = maxPermits;
  //1:最大的存储个数为需要预热的时间除以两个请求的时间间隔,比如设定预热时间为1s,每秒有5个请求,那么最大的存储个数为1000ms/200ms=5个
  maxPermits = warmupPeriodMicros / stableIntervalMicros; 
  //2:计算最大存储permits的一半
  halfPermits = maxPermits / 2.0;
  //3:初始化稳定时间间隔的3倍作为冷却时间间隔
  double coldIntervalMicros = stableIntervalMicros * 3.0;
  //4:设置基准线的斜率
  slope = (coldIntervalMicros - stableIntervalMicros) / halfPermits;
  if (oldMaxPermits == Double.POSITIVE_INFINITY) {
    storedPermits = 0.0;
  } else {
    storedPermits = (oldMaxPermits == 0.0)
        ? maxPermits // 初始条件下,认为就是存储满的,以达到缓慢消费的效果
        : storedPermits * maxPermits / oldMaxPermits;
  }
}

В этом процессе вы можете видеть, что метод Warmup добавил дизайн halfPermits, и через формулуslope=(coldIntervalMicros-stableIntervalMicros)/halfPermits, они используются в функции StoredPermitsToWaitTime

@Overridelong storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
  //1:计算储存的令牌中超过了最大令牌一半的数量
  double availablePermitsAboveHalf = storedPermits - halfPermits;
  long micros = 0;
  // 计算超过一半的部分所需要的时间花销(对于函数来说,就是积分计算)
  if (availablePermitsAboveHalf > 0.0) {
    double permitsAboveHalfToTake = Math.min(availablePermitsAboveHalf, permitsToTake);
    micros = (long) (permitsAboveHalfToTake * (permitsToTime(availablePermitsAboveHalf)
        + permitsToTime(availablePermitsAboveHalf - permitsAboveHalfToTake)) / 2.0);
    permitsToTake -= permitsAboveHalfToTake;
  }
  // 计算函数的尚未超过一半的部分所需要的时间花销
  micros += (stableIntervalMicros * permitsToTake);
  return micros;
}

private double permitsToTime(double permits) {
  return stableIntervalMicros + permits * slope;
}

Философия дизайна WarmingUp

WarmingUp измеряет время, проведенное на следующем рисунке.

   *          ^ throttling
   *          |
   * 3*stable +                  /
   * interval |                 /.
   *  (cold)  |                / .
   *          |               /  .   <-- "warmup period" is the area of the trapezoid between
   * 2*stable +              /   .       halfPermits and maxPermits
   * interval |             /    .
   *          |            /     .
   *          |           /      .
   *   stable +----------/  WARM . }
   * interval |          .   UP  . } <-- this rectangle (from 0 to maxPermits, and
   *          |          . PERIOD. }     height == stableInterval) defines the cooldown period,
   *          |          .       . }     and we want cooldownPeriod == warmupPeriod
   *          |---------------------------------> storedPermits
   *              (halfPermits) (maxPermits)

Горизонтальная ось представляет количество сохраненных токенов, а вертикальная ось представляет время, поэтому интеграл функции может представлять затраченное время.
Когда программа только запускается, метод прогрева будет хранить все токены, и в соответствии с методом получения из токена хранилища можно понять, что время, необходимое для хранения самого большого токена, чтобы упасть до половины токена, одинаково как время хранения. В два раза больше времени, чем количество токенов, так что скорость выпуска токенов в начале относительно низкая. После половины потребления скорость приобретения такая же, как скорость производства, таким образом реализуя понятие «разогрев»

Время, необходимое для получения токена из storePermits, делится на две части, причем половина maxPetmits является точкой разделения.

  • Когда storagePermits

  • storagePermits>halfPermits, время, необходимое для хранения storePermits, соответствует скорости генерации, но время, необходимое для использования storePermites от maxPermits до halfPermits, в 2 раза превышает время, необходимое для роста от halfPermits до maxPermits, что медленнее, чем генерация новых токенов. Почему вы выбрали 3 раза и половину позиций для расчета точки разделения и наклона? Выполнение интегрального вычисления функции (графическая область) может просто гарантировать, что если более половины хранимых токенов необходимо удалить, время, необходимое для хранения того же количества (или генерации новых токенов), равно времени, затраченному дважды , если соответствующий сценарий не использовался в течение длительного времени (хранимый токен достигнет maxPermits), скорость, с которой могут быть получены запросы, в начале относительно низкая, а затем увеличивается до стабильной скорости потребления

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

TryAcquire

Он попытается получить токен, если вы не можете получить возврат немедленно, в противном случае истечет время ожидания возврата данного токена. Источник следует

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
  //1:使用微秒来转换超时时间
  long timeoutMicros = unit.toMicros(timeout);
  checkPermits(permits);
  long microsToWait;
  synchronized (mutex) {
    long nowMicros = readSafeMicros();
    //2.1:如果下次能够获取令牌的时间超过超时时间范围,立马返回;
    if (nextFreeTicketMicros > nowMicros + timeoutMicros) {
      return false;
    } else {
      //2.2:获取需要等待的时间,本次获取的时间肯定不会超时
      microsToWait = reserveNextTicket(permits, nowMicros);
    }
  }
  //3:实行等待
  ticker.sleepMicrosUninterruptibly(microsToWait);
  return true;
}

При первом запуске nextFreeTicketMicros - это время его создания, которое должно быть меньше текущего времени, поэтому он точно будет отпущен в первый раз, и выполнение разрешено, но необходимо только рассчитать время ожидания.

private long reserveNextTicket(double requiredPermits, long nowMicros) {
  //1:如果下次可以获取令牌的时间在过去,更新
  resync(nowMicros);
  //2:计算距离下次获取令牌需要的时间,如果nextFreeTikcetMicros>nowMicros,这个时间段必定在超时时间之内,假如入超时时间是0,那么必定是microsToNextFreeTicket趋近于0,也就是立马能够放行;
  long microsToNextFreeTicket = Math.max(0, nextFreeTicketMicros - nowMicros);
  //3:计算需要消耗的存储的令牌
  double storedPermitsToSpend = Math.min(requiredPermits, this.storedPermits);
  //4:计算需要新产生的令牌
  double freshPermits = requiredPermits - storedPermitsToSpend;
  //5:计算消耗存储令牌所需要的时间和新产生令牌所需要的时间。对于Bursty来讲,消耗存储的令牌所需要时间为0,WarmingUp方式则是需要根据不同的场景有不同的结果
  long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
      + (long) (freshPermits * stableIntervalMicros);
  //6:下次能够获取令牌的时间,需要延迟当前已经等待的时间,也就是说,如果立马有请求过来会放行,但是这个等待时间将会影响后续的请求访问,也就是说,这次的请求如果当前的特别的多,下一次能够请求的能够允许的时间必定会有很长的延迟
  this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;
  //7:扣除消耗的存储令牌
  this.storedPermits -= storedPermitsToSpend;
  //8:返回本次要获取令牌所需要的时间,它肯定不会超过超时时间
  return microsToNextFreeTicket;
}

Acquire

Он будет блокироваться до тех пор, пока выпуск не будет разрешен, а возвращаемое значение — это продолжительность блока. Исходный код выглядит следующим образом

public double acquire(int permits) {
  long microsToWait = reserve(permits); //也就是调用reserveNextTicket
  ticker.sleepMicrosUninterruptibly(microsToWait); //阻塞住需要等待的时长
  return 1.0 * microsToWait / TimeUnit.SECONDS.toMicros(1L);
}

ПопробуйтеПолучить дело

В программе установлено 10 потоков, так что количество параллелизма равно 10, имитируя онлайн-сцену, содержание задачи следующее

class MyTask implements Runnable{
    private CountDownLatch latch;
    private RateLimiter limiter;

    public MyTask(CountDownLatch latch, RateLimiter limiter) {
        this.latch = latch;
        this.limiter = limiter;
    }

    @Override    public void run() {
        try {
            //使得线程同时触发            
           latch.await();
            System.out.println("time "+System.currentTimeMillis()+"ms :"+limiter.tryAcquire());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

Bursty-TryAcquire

Ограничение трафика в секунду установлено здесь равным 5, что означает, что после первого запроса следующий запрос должен ждать 200 мс.

RateLimiter r =RateLimiter.create(5);
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);

for (int i=0;i<10;i++)
{

    service.submit(new MyTask(latch, r));
    latch.countDown();
    System.out.println("countdown:" + latch.getCount());
}
System.out.println("countdown over");
service.shutdown();

Результат выглядит следующим образом

countdown:9
countdown:8
countdown:7
countdown:6
countdown:5
countdown:4
countdown:3
countdown:2
countdown:1
countdown:0
countdown over
time 1538487195698ms :true
time 1538487195699ms :false
time 1538487195699ms :false
time 1538487195699ms :false
time 1538487195699ms :false
time 1538487195699ms :false
time 1538487195699ms :false
time 1538487195698ms :false
time 1538487195698ms :false
time 1538487195699ms :false

Если поток будет ждать 401 мс, программа сохранит 2 токена.

Учтите, что в начале хранения оно не медленное, объем хранилища здесь медленно увеличивается, и его можно получить сразу

RateLimiter r =RateLimiter.create(5);
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);

for (int i=0;i<10;i++)
{

    service.submit(new MyTask(latch, r));
    if (i==9){
        TimeUnit.MILLISECONDS.sleep(401);
        System.out.println("sleep 10 seconds over");
    }
    latch.countDown();
    System.out.println("countdown:" + latch.getCount());
}
System.out.println("countdown over");
service.shutdown();

Результат прогона - разрешено ровно 3 прогона

countdown:9
countdown:8
countdown:7
countdown:6
countdown:5
countdown:4
countdown:3
countdown:2
countdown:1
sleep 10 seconds over
countdown:0
countdown over
time 1538487297981ms :true
time 1538487297981ms :false
time 1538487297981ms :false
time 1538487297981ms :false
time 1538487297981ms :true
time 1538487297981ms :true
time 1538487297981ms :false
time 1538487297981ms :false
time 1538487297981ms :false
time 1538487297981ms :false

Если время ожидания превышает 1 секунду, разрешенный трафик не превысит 6, сохраненный токен + первый токен

RateLimiter r =RateLimiter.create(5);
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);

for (int i=0;i<10;i++)
{

    service.submit(new MyTask(latch, r));
    if (i==9){
        TimeUnit.MILLISECONDS.sleep(1001);
        System.out.println("sleep 10 seconds over");
    }
    latch.countDown();
    System.out.println("countdown:" + latch.getCount());
}
System.out.println("countdown over");
service.shutdown();

Результат

countdown:9
countdown:8
countdown:7
countdown:6
countdown:5
countdown:4
countdown:3
countdown:2
countdown:1
sleep 10 seconds over
countdown:0
countdown over
time 1538487514780ms :true
time 1538487514780ms :true
time 1538487514780ms :true
time 1538487514780ms :false
time 1538487514780ms :true
time 1538487514780ms :false
time 1538487514780ms :false
time 1538487514780ms :false
time 1538487514780ms :true
time 1538487514780ms :true

WarmingUp-TryAcquire

Так как способ использования warmingUp уже заполнен токенами по умолчанию, после выполнения первого запроса он должен ждать определенный период времени для запуска следующего запроса, а время освобождения этого запроса превысит хранилище время нужно время

Обратите внимание на разницу здесь, по умолчанию хранилище заполнено, то есть потребление в начале происходит намного медленнее.

RateLimiter r =RateLimiter.create(5,1,TimeUnit.SECONDS);
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);

for (int i=0;i<10;i++)
{

    service.submit(new MyTask(latch, r));
    latch.countDown();
    System.out.println("countdown:" + latch.getCount());
}
System.out.println("countdown over");
service.shutdown();

Текущий результат выглядит следующим образом

countdown:9
countdown:8
countdown:7
countdown:6
countdown:5
countdown:4
countdown:3
countdown:2
countdown:1
countdown:0
countdown over
time 1538487677462ms :true
time 1538487677462ms :false
time 1538487677462ms :false
time 1538487677462ms :false
time 1538487677462ms :false
time 1538487677462ms :false
time 1538487677462ms :false
time 1538487677462ms :false
time 1538487677462ms :false
time 1538487677462ms :false

Получить операционный случай

Необходимый исходный код задачи выглядит следующим образом

class MyTask implements Runnable{
    private CountDownLatch latch;
    private RateLimiter limiter;
    private long start;

    public MyTask(CountDownLatch latch, RateLimiter limiter,long start) {
        this.latch = latch;
        this.limiter = limiter;
       this.start=start;
    }

    @Override    public void run() {
        try {
            //使得线程同时触发            
           latch.await();
            System.out.printf("result:"+limiter.acquire(2));
            System.out.println(" time "+(System.currentTimeMillis()-start)+"ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

Busty-Acquire

Приобретение блокирует результат запуска и потребляет его раньше

RateLimiter r =RateLimiter.create(1); 
r.acquire();
System.out.println("time cost:"+(System.currentTimeMillis()-start)+"ms");
r.acquire();
System.out.println("time cost:"+(System.currentTimeMillis()-start)+"ms");
r.acquire(3);
System.out.println("time cost:"+(System.currentTimeMillis()-start)+"ms");
r.acquire();
System.out.println("time cost:"+(System.currentTimeMillis()-start)+"ms");

В первый раз он будет запущен сразу, а затем, поскольку запрос сделан один раз, время для следующего выпуска токена будет перенесено позже.Чем больше токенов будет получено, тем больше времени потребуется для запуска в следующий раз.

Результат бега есть

time cost:0ms
time cost:1005ms
time cost:2004ms
time cost:5001ms

Запуск в многопоточном фоне выглядит следующим образом

RateLimiter r =RateLimiter.create(1);
long start=System.currentTimeMillis();
r.acquire(3);
System.out.println("time cost:"+(System.currentTimeMillis()-start)+"ms");
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);

for (int i=0;i<10;i++)
{

    service.submit(new MyTask(latch, r,start));
    latch.countDown();
    System.out.println("countdown:" + latch.getCount());
}
System.out.println("countdown over");
service.shutdown();

Результат выглядит следующим образом

time cost:1ms
countdown:9
countdown:8
countdown:7
countdown:6
countdown:5
countdown:4
countdown:3
countdown:2
countdown:1
countdown:0
countdown over
result:2.995732 time 3024ms
result:4.995725 time 5006ms
result:6.995719 time 7007ms
result:8.995716 time 9006ms
result:10.995698 time 11004ms
result:12.995572 time 13006ms
result:14.995555 time 15007ms
result:16.995543 time 17005ms
result:18.995516 time 19005ms
result:20.995463 time 21005ms

WarmingUp-acquire

Токен, полученный при разогреве через приобретение, также будет получен синхронно.

RateLimiter r =RateLimiter.create(1,1,TimeUnit.SECONDS);
long start=System.currentTimeMillis();
r.acquire(3);
System.out.println("time cost:"+(System.currentTimeMillis()-start)+"ms”);
ExecutorService service = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(10);

for (int i=0;i<10;i++)
{

    service.submit(new MyTask(latch, r,start));
    latch.countDown();
    System.out.println("countdown:" + latch.getCount());
}
System.out.println("countdown over");
service.shutdown();

Результат выглядит следующим образом

time cost:0ms
countdown:9
countdown:8
countdown:7
countdown:6
countdown:5
countdown:4
countdown:3
countdown:2
countdown:1
countdown:0
countdown over
result:3.496859 time 3521ms
result:5.496854 time 5506ms
result:7.49685 time 7505ms
result:9.496835 time 9504ms
result:11.496821 time 11505ms
result:13.496807 time 13502ms
result:15.496793 time 15504ms
result:17.496778 time 17506ms
result:19.496707 time 19506ms
result:21.496699 time 21506ms

Сам RateLimiter реализуетАлгоритм ведра токенов