Вызов метода регулирования для M запросов за N секунд

137

Мне нужен компонент / класс, который ограничивает выполнение какого-либо метода до максимума M вызовов в N секунд (или мс или нанос, не имеет значения).

Другими словами, мне нужно убедиться, что мой метод выполняется не более M раз в скользящем окне из N секунд.

Если вы не знаете существующий класс, не стесняйтесь размещать свои решения / идеи, как бы вы это реализовали.

vtrubnikov
источник
3
Есть некоторые большие ответы на эту проблему в stackoverflow.com/questions/667508/...
skaffman
Я должен убедиться, что мой метод выполняется не более M раз за скользящее окно N секунд. Недавно я написал сообщение в блоге о том, как сделать это в .NET. Возможно, вы сможете создать нечто подобное в Java. Лучшее ограничение скорости в .NET
Джек Лейтч
Исходный вопрос очень похож на проблему, решаемую в этом посте: [Многоканальный асинхронный регулятор Java] ( cordinc.com/blog/2010/04/java-multichannel-asynchronous.html ). Для скорости M вызовов в N секунд регулятор, обсуждаемый в этом блоге, гарантирует, что любой интервал длины N на временной шкале не будет содержать более M вызовов.
Hbf

Ответы:

81

Я бы использовал кольцевой буфер временных меток с фиксированным размером M. Каждый раз, когда вызывается метод, вы проверяете самую старую запись, и если она меньше N секунд в прошлом, вы выполняете и добавляете другую запись, иначе вы спите для разницы во времени.

Майкл Боргвардт
источник
4
Прекрасный. Как раз то, что мне нужно. Быстрые попытки показывают ~ 10 строк для реализации этого и минимальный объем памяти. Просто нужно подумать о безопасности потоков и организации очередей входящих запросов.
Вторников 10.09.09
5
Вот почему вы используете DelayQueue из java.util.concurrent. Это предотвращает проблему нескольких потоков, действующих на одну и ту же запись.
Эриксон
5
Я думаю, что для многопоточного случая подход с использованием маркеров может быть лучшим выбором.
Майкл Боргвардт
1
Вы знаете, как называется этот алгоритм, если у него вообще есть имя?
Владо Панджич
80

То, что работало из коробки для меня, было Google Guava RateLimiter .

// Allow one request per second
private RateLimiter throttle = RateLimiter.create(1.0);

private void someMethod() {
    throttle.acquire();
    // Do something
}
schnatterer
источник
19
Я бы не рекомендовал это решение, так как Guava RateLimiter заблокирует поток, и это легко исчерпает пул потоков.
kaviddiss
18
@kaviddiss, если вы не хотите блокировать, тогда используйтеtryAquire()
slf
7
Проблема с текущей реализацией RateLimiter (по крайней мере для меня) заключается в том, что он не допускает периоды времени более 1 секунды и, следовательно, скорости, например, 1 в минуту.
Джон Б.
4
@John B Насколько я понимаю, вы можете выполнять 1 запрос в минуту с помощью RateLimiter, используя RateLimiter.create (60.0) + rateLimiter.acquire (60)
divByZero
2
@radiantRazor Ratelimiter.create (1.0 / 60) и acqu () выполняет 1 вызов в минуту.
bizentass
30

Конкретно, вы должны быть в состоянии реализовать это с помощью DelayQueue. Инициализируйте очередь M Delayedэкземплярами с задержкой, изначально установленной на ноль. Когда поступают запросы к методу, takeтокен, который заставляет метод блокироваться, пока не будет выполнено требование регулирования. Когда токен был взят, addновый токен в очереди с задержкой N.

Эриксон
источник
1
Да, это бы сработало. Но мне не особенно нравится DelayQueue, потому что он использует (через PriortyQueue) сбалансированный бинарный хеш (что означает много сравнений offerи возможный рост массива), и все это довольно тяжело для меня. Я думаю, для других это может быть совершенно нормально.
Вторников 10.09.09
5
На самом деле, в этом приложении, поскольку новый элемент, добавляемый в кучу, почти всегда будет максимальным элементом в куче (т. Е. Имеет самую длинную задержку), обычно требуется одно сравнение на добавление. Кроме того, массив никогда не будет расти, если алгоритм реализован правильно, поскольку один элемент добавляется только после взятия одного элемента.
Эриксон
3
Я нашел это полезным также в тех случаях, когда вы не хотите, чтобы запросы выполнялись большими пакетами, поскольку размер M и задержка N относительно малы в порядке нескольких миллисекунд. например. M = 5, N = 20 мс обеспечит сквозной импульс 250 к / с, который произойдет в размере 5.
FUD
Будет ли это масштабироваться на миллион оборотов в минуту и ​​когда разрешены параллельные запросы? Мне нужно было бы добавить миллион задержанных элементов. Также угловые случаи будут высокими с задержкой - случай, когда несколько потоков вызывают poll (), и он будет блокироваться каждый раз.
Адитья Джоши
@AdityaJoshee Я не проверял это, но если у меня будет время, я постараюсь почувствовать, что такое накладные расходы. Однако стоит отметить, что вам не нужен 1 миллион токенов, срок действия которых истекает через 1 секунду. Вы можете иметь 100 токенов, срок действия которых истекает через 10 миллисекунд, 10 токенов, срок действия которых истекает через миллисекунды, и т. Д. Это фактически заставляет мгновенную скорость быть ближе к средней скорости, сглаживая пики, что может вызвать резервные копии на клиенте, но это естественное следствие ограничения скорости. 1 миллион оборотов в минуту вряд ли звучит как дросселирование. Если вы можете объяснить ваш вариант использования, у меня могут быть лучшие идеи.
Эриксон
21

Читайте на ведро Token алгоритме . По сути, у вас есть ведро с жетонами. Каждый раз, когда вы выполняете метод, вы берете токен. Если токенов больше нет, вы блокируете их, пока не получите. Между тем, существует некоторый внешний субъект, который пополняет токены с фиксированным интервалом.

Я не знаю библиотеки для этого (или чего-то подобного). Вы можете написать эту логику в своем коде или использовать AspectJ для добавления поведения.

Kevin
источник
3
Спасибо за предложение, интересный алгоритм. Но это не совсем то, что мне нужно. Например, мне нужно ограничить выполнение до 5 вызовов в секунду. Если я использую корзину токенов и одновременно поступает 10 запросов, первые 5 вызовов будут принимать все доступные токены и выполняться мгновенно, а остальные 5 вызовов будут выполняться с фиксированным интервалом в 1/5 с. В такой ситуации мне нужно, чтобы оставшиеся 5 вызовов выполнялись в одном пакете только через 1 секунду.
Вторников 10.09.09
5
Что если вы добавляете 5 жетонов в корзину каждую секунду (или 5 - (5 оставшихся) вместо 1 каждые 1/5 секунды?
Кевин
@ Kevin нет, это все равно не дало бы мне эффект «скользящего окна»
vtrubnikov
2
@ Валерий, да, это так. (Не забудьте заглушить жетоны на M, хотя)
nos
нет необходимости во «внешнем актере». Все можно сделать однопоточным, если вы храните метаданные о времени запроса.
Марселлус Уоллес
8

Если вам нужен ограничитель скорости скользящего окна на основе Java, который будет работать в распределенной системе, вы можете взглянуть на проект https://github.com/mokies/ratelimitj .

Конфигурация с поддержкой Redis, ограничивающая количество запросов по IP до 50 в минуту, будет выглядеть так:

import com.lambdaworks.redis.RedisClient;
import es.moki.ratelimitj.core.LimitRule;

RedisClient client = RedisClient.create("redis://localhost");
Set<LimitRule> rules = Collections.singleton(LimitRule.of(1, TimeUnit.MINUTES, 50)); // 50 request per minute, per key
RedisRateLimit requestRateLimiter = new RedisRateLimit(client, rules);

boolean overLimit = requestRateLimiter.overLimit("ip:127.0.0.2");

См. Https://github.com/mokies/ratelimitj/tree/master/ratelimitj-redis для получения дополнительной информации о конфигурации Redis.

user2326162
источник
5

Это зависит от приложения.

Представьте себе случай, когда несколько потоков хотят, чтобы токен выполнял какое-либо глобально ограниченное по скорости действие, при котором пакет не был разрешен (т.е. вы хотите ограничить 10 действий в 10 секунд, но вы не хотите, чтобы 10 действий происходили в первую секунду, а затем оставались 9 секунд остановились).

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

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

Вот реализация:

public class LeakyBucket {
    protected float maxRate;
    protected long minTime;
    //holds time of last action (past or future!)
    protected long lastSchedAction = System.currentTimeMillis();

    public LeakyBucket(float maxRate) throws Exception {
        if(maxRate <= 0.0f) {
            throw new Exception("Invalid rate");
        }
        this.maxRate = maxRate;
        this.minTime = (long)(1000.0f / maxRate);
    }

    public void consume() throws InterruptedException {
        long curTime = System.currentTimeMillis();
        long timeLeft;

        //calculate when can we do the action
        synchronized(this) {
            timeLeft = lastSchedAction + minTime - curTime;
            if(timeLeft > 0) {
                lastSchedAction += minTime;
            }
            else {
                lastSchedAction = curTime;
            }
        }

        //If needed, wait for our time
        if(timeLeft <= 0) {
            return;
        }
        else {
            Thread.sleep(timeLeft);
        }
    }
}
Дуарте Менезес
источник
что minTimeзначит здесь? Что оно делает? Вы можете объяснить это?
вспышка
minTimeминимальное количество времени, которое должно пройти после того, как токен будет израсходован, прежде чем будет использован следующий токен.
Дуарте Менезес
3

Хотя это не то, что вы просили, ThreadPoolExecutorэто может быть полезно для ограничения одновременных запросов M вместо M запросов за N секунд.

Евгений Йокота
источник
2

Я реализовал простой алгоритм регулирования. Попробуйте эту ссылку, http://krishnaprasadas.blogspot.in/2012/05/throttling-algorithm.html

Краткое описание алгоритма,

Этот алгоритм использует возможности Java Delayed Queue . Создайте задержанный объект с ожидаемой задержкой (здесь 1000 / M для миллисекунды TimeUnit ). Поместите тот же объект в отложенную очередь, которая будет интерном для нас. Затем перед каждым вызовом метода возьмите объект из очереди, take является блокирующим вызовом, который будет возвращаться только после указанной задержки, и после вызова метода не забудьте поместить объект в очередь с обновленным временем (здесь текущие миллисекунды) ,

Здесь мы также можем иметь несколько задержанных объектов с различной задержкой. Этот подход также обеспечит высокую пропускную способность.

Krishas
источник
6
Вы должны опубликовать резюме вашего алгоритма. Если ваша ссылка исчезнет, ​​то ваш ответ станет бесполезным.
2012 года
Спасибо, я добавил краткое изложение.
Кришас
1

Моя реализация ниже может обрабатывать произвольную точность времени запроса, она имеет O (1) временную сложность для каждого запроса, не требует никакого дополнительного буфера, например O (1) пространственной сложности, кроме того, он не требует фонового потока для выпуска токена, вместо этого токены выпускаются в соответствии с временем, прошедшим с момента последнего запроса.

class RateLimiter {
    int limit;
    double available;
    long interval;

    long lastTimeStamp;

    RateLimiter(int limit, long interval) {
        this.limit = limit;
        this.interval = interval;

        available = 0;
        lastTimeStamp = System.currentTimeMillis();
    }

    synchronized boolean canAdd() {
        long now = System.currentTimeMillis();
        // more token are released since last request
        available += (now-lastTimeStamp)*1.0/interval*limit; 
        if (available>limit)
            available = limit;

        if (available<1)
            return false;
        else {
            available--;
            lastTimeStamp = now;
            return true;
        }
    }
}
tonywl
источник
0

Попробуйте использовать этот простой подход:

public class SimpleThrottler {

private static final int T = 1; // min
private static final int N = 345;

private Lock lock = new ReentrantLock();
private Condition newFrame = lock.newCondition();
private volatile boolean currentFrame = true;

public SimpleThrottler() {
    handleForGate();
}

/**
 * Payload
 */
private void job() {
    try {
        Thread.sleep(Math.abs(ThreadLocalRandom.current().nextLong(12, 98)));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.err.print(" J. ");
}

public void doJob() throws InterruptedException {
    lock.lock();
    try {

        while (true) {

            int count = 0;

            while (count < N && currentFrame) {
                job();
                count++;
            }

            newFrame.await();
            currentFrame = true;
        }

    } finally {
        lock.unlock();
    }
}

public void handleForGate() {
    Thread handler = new Thread(() -> {
        while (true) {
            try {
                Thread.sleep(1 * 900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                currentFrame = false;

                lock.lock();
                try {
                    newFrame.signal();
                } finally {
                    lock.unlock();
                }
            }
        }
    });
    handler.start();
}

}

SergeZ
источник
0

Apache Camel также поддерживает поставляется с механизмом Throttler следующим образом:

from("seda:a").throttle(100).asyncDelayed().to("seda:b");
gtonic
источник
0

Это обновление кода LeakyBucket выше. Это работает для более чем 1000 запросов в секунду.

import lombok.SneakyThrows;
import java.util.concurrent.TimeUnit;

class LeakyBucket {
  private long minTimeNano; // sec / billion
  private long sched = System.nanoTime();

  /**
   * Create a rate limiter using the leakybucket alg.
   * @param perSec the number of requests per second
   */
  public LeakyBucket(double perSec) {
    if (perSec <= 0.0) {
      throw new RuntimeException("Invalid rate " + perSec);
    }
    this.minTimeNano = (long) (1_000_000_000.0 / perSec);
  }

  @SneakyThrows public void consume() {
    long curr = System.nanoTime();
    long timeLeft;

    synchronized (this) {
      timeLeft = sched - curr + minTimeNano;
      sched += minTimeNano;
    }
    if (timeLeft <= minTimeNano) {
      return;
    }
    TimeUnit.NANOSECONDS.sleep(timeLeft);
  }
}

и unittest для выше:

import com.google.common.base.Stopwatch;
import org.junit.Ignore;
import org.junit.Test;

import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

public class LeakyBucketTest {
  @Test @Ignore public void t() {
    double numberPerSec = 10000;
    LeakyBucket b = new LeakyBucket(numberPerSec);
    Stopwatch w = Stopwatch.createStarted();
    IntStream.range(0, (int) (numberPerSec * 5)).parallel().forEach(
        x -> b.consume());
    System.out.printf("%,d ms%n", w.elapsed(TimeUnit.MILLISECONDS));
  }
}
peterreilly
источник
что minTimeNanoзначит здесь? Вы можете объяснить?
вспышка
0

Вот немного продвинутая версия простого ограничителя скорости

/**
 * Simple request limiter based on Thread.sleep method.
 * Create limiter instance via {@link #create(float)} and call {@link #consume()} before making any request.
 * If the limit is exceeded cosume method locks and waits for current call rate to fall down below the limit
 */
public class RequestRateLimiter {

    private long minTime;

    private long lastSchedAction;
    private double avgSpent = 0;

    ArrayList<RatePeriod> periods;


    @AllArgsConstructor
    public static class RatePeriod{

        @Getter
        private LocalTime start;

        @Getter
        private LocalTime end;

        @Getter
        private float maxRate;
    }


    /**
     * Create request limiter with maxRate - maximum number of requests per second
     * @param maxRate - maximum number of requests per second
     * @return
     */
    public static RequestRateLimiter create(float maxRate){
        return new RequestRateLimiter(Arrays.asList( new RatePeriod(LocalTime.of(0,0,0),
                LocalTime.of(23,59,59), maxRate)));
    }

    /**
     * Create request limiter with ratePeriods calendar - maximum number of requests per second in every period
     * @param ratePeriods - rate calendar
     * @return
     */
    public static RequestRateLimiter create(List<RatePeriod> ratePeriods){
        return new RequestRateLimiter(ratePeriods);
    }

    private void checkArgs(List<RatePeriod> ratePeriods){

        for (RatePeriod rp: ratePeriods ){
            if ( null == rp || rp.maxRate <= 0.0f || null == rp.start || null == rp.end )
                throw new IllegalArgumentException("list contains null or rate is less then zero or period is zero length");
        }
    }

    private float getCurrentRate(){

        LocalTime now = LocalTime.now();

        for (RatePeriod rp: periods){
            if ( now.isAfter( rp.start ) && now.isBefore( rp.end ) )
                return rp.maxRate;
        }

        return Float.MAX_VALUE;
    }



    private RequestRateLimiter(List<RatePeriod> ratePeriods){

        checkArgs(ratePeriods);
        periods = new ArrayList<>(ratePeriods.size());
        periods.addAll(ratePeriods);

        this.minTime = (long)(1000.0f / getCurrentRate());
        this.lastSchedAction = System.currentTimeMillis() - minTime;
    }

    /**
     * Call this method before making actual request.
     * Method call locks until current rate falls down below the limit
     * @throws InterruptedException
     */
    public void consume() throws InterruptedException {

        long timeLeft;

        synchronized(this) {
            long curTime = System.currentTimeMillis();

            minTime = (long)(1000.0f / getCurrentRate());
            timeLeft = lastSchedAction + minTime - curTime;

            long timeSpent = curTime - lastSchedAction + timeLeft;
            avgSpent = (avgSpent + timeSpent) / 2;

            if(timeLeft <= 0) {
                lastSchedAction = curTime;
                return;
            }

            lastSchedAction = curTime + timeLeft;
        }

        Thread.sleep(timeLeft);
    }

    public synchronized float getCuRate(){
        return (float) ( 1000d / avgSpent);
    }
}

И юнит-тесты

import org.junit.Assert;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class RequestRateLimiterTest {


    @Test(expected = IllegalArgumentException.class)
    public void checkSingleThreadZeroRate(){

        // Zero rate
        RequestRateLimiter limiter = RequestRateLimiter.create(0);
        try {
            limiter.consume();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void checkSingleThreadUnlimitedRate(){

        // Unlimited
        RequestRateLimiter limiter = RequestRateLimiter.create(Float.MAX_VALUE);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) < 1000));
    }

    @Test
    public void rcheckSingleThreadRate(){

        // 3 request per minute
        RequestRateLimiter limiter = RequestRateLimiter.create(3f/60f);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 3; i++ ){

            try {
                limiter.consume();
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( ((ended - started) >= 60000 ) & ((ended - started) < 61000));
    }



    @Test
    public void checkSingleThreadRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);

        long started = System.currentTimeMillis();
        for ( int i = 0; i < 1000; i++ ){

            try {
                limiter.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        long ended = System.currentTimeMillis();

        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ));
    }

    @Test
    public void checkMultiThreadedRateLimit(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

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

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreaded32RateLimit(){

        // 0,2 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(0.2f);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(8);
        ExecutorService exec = Executors.newFixedThreadPool(8);

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

            tasks.add( exec.submit(() -> {
                for (int i1 = 0; i1 < 2; i1++) {

                    try {
                        limiter.consume();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

    @Test
    public void checkMultiThreadedRateLimitDynamicRate(){

        // 100 request per second
        RequestRateLimiter limiter = RequestRateLimiter.create(100);
        long started = System.currentTimeMillis();

        List<Future<?>> tasks = new ArrayList<>(10);
        ExecutorService exec = Executors.newFixedThreadPool(10);

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

            tasks.add( exec.submit(() -> {

                Random r = new Random();
                for (int i1 = 0; i1 < 100; i1++) {

                    try {
                        limiter.consume();
                        Thread.sleep(r.nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }) );
        }

        tasks.stream().forEach( future -> {
            try {
                future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        });

        long ended = System.currentTimeMillis();
        System.out.println( "Current rate:" + limiter.getCurRate() );
        Assert.assertTrue( (ended - started) >= ( 10000 - 100 ) );
    }

}
Леонид Астахов
источник
Код довольно прост. Вы просто создаете лимитер с maxRate или с периодами и скоростью. А потом просто звоните потреблять каждый запрос. Всякий раз, когда скорость не превышена, ограничитель возвращается немедленно или ждет некоторое время, прежде чем вернуться к более низкой текущей частоте запросов. Он также имеет метод текущей ставки, который возвращает скользящее среднее текущей ставки.
Леонид Астахов
0

Мое решение: простой метод util, вы можете изменить его для создания класса-оболочки.

public static Runnable throttle (Runnable realRunner, long delay) {
    Runnable throttleRunner = new Runnable() {
        // whether is waiting to run
        private boolean _isWaiting = false;
        // target time to run realRunner
        private long _timeToRun;
        // specified delay time to wait
        private long _delay = delay;
        // Runnable that has the real task to run
        private Runnable _realRunner = realRunner;
        @Override
        public void run() {
            // current time
            long now;
            synchronized (this) {
                // another thread is waiting, skip
                if (_isWaiting) return;
                now = System.currentTimeMillis();
                // update time to run
                // do not update it each time since
                // you do not want to postpone it unlimited
                _timeToRun = now+_delay;
                // set waiting status
                _isWaiting = true;
            }
            try {
                Thread.sleep(_timeToRun-now);

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // clear waiting status before run
                _isWaiting = false;
                // do the real task
                _realRunner.run();
            }
        }};
    return throttleRunner;
}

Взять из JAVA Thread Debounce и Throttle

benbai123
источник