Подтвердить что ты не робот

Внедрение debounce в Java

Для некоторого кода, который я пишу, я мог бы использовать хорошую общую реализацию debounce в Java.

public interface Callback {
  public void call(Object arg);
}

class Debouncer implements Callback {
    public Debouncer(Callback c, int interval) { ... }

    public void call(Object arg) { 
        // should forward calls with the same arguments to the callback c
        // but batch multiple calls inside `interval` to a single one
    }
}

Когда call() вызывается несколько раз в interval миллисекундах с тем же аргументом, функция обратного вызова должна вызываться ровно один раз.

Визуализация:

Debouncer#call  xxx   x xxxxxxx        xxxxxxxxxxxxxxx
Callback#call      x           x                      x  (interval is 2)
  • Есть ли что-то вроде этого в некоторой стандартной библиотеке Java?
  • Как бы вы это реализовали?
4b9b3361

Ответ 1

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

public class Debouncer <T> {
  private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
  private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();
  private final Callback<T> callback;
  private final int interval;

  public Debouncer(Callback<T> c, int interval) { 
    this.callback = c;
    this.interval = interval;
  }

  public void call(T key) {
    TimerTask task = new TimerTask(key);

    TimerTask prev;
    do {
      prev = delayedMap.putIfAbsent(key, task);
      if (prev == null)
        sched.schedule(task, interval, TimeUnit.MILLISECONDS);
    } while (prev != null && !prev.extend()); // Exit only if new task was added to map, or existing task was extended successfully
  }

  public void terminate() {
    sched.shutdownNow();
  }

  // The task that wakes up when the wait time elapses
  private class TimerTask implements Runnable {
    private final T key;
    private long dueTime;    
    private final Object lock = new Object();

    public TimerTask(T key) {        
      this.key = key;
      extend();
    }

    public boolean extend() {
      synchronized (lock) {
        if (dueTime < 0) // Task has been shutdown
          return false;
        dueTime = System.currentTimeMillis() + interval;
        return true;
      }
    }

    public void run() {
      synchronized (lock) {
        long remaining = dueTime - System.currentTimeMillis();
        if (remaining > 0) { // Re-schedule task
          sched.schedule(this, remaining, TimeUnit.MILLISECONDS);
        } else { // Mark as terminated and invoke callback
          dueTime = -1;
          try {
            callback.call(key);
          } finally {
            delayedMap.remove(key);
          }
        }
      }
    }  
  }

Ответ 2

Здесь моя реализация:

public class Debouncer {
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    private final ConcurrentHashMap<Object, Future<?>> delayedMap = new ConcurrentHashMap<>();

    /**
     * Debounces {@code callable} by {@code delay}, i.e., schedules it to be executed after {@code delay},
     * or cancels its execution if the method is called with the same key within the {@code delay} again.
     */
    public void debounce(final Object key, final Runnable runnable, long delay, TimeUnit unit) {
        final Future<?> prev = delayedMap.put(key, scheduler.schedule(new Runnable() {
            @Override
            public void run() {
                try {
                    runnable.run();
                } finally {
                    delayedMap.remove(key);
                }
            }
        }, delay, unit));
        if (prev != null) {
            prev.cancel(true);
        }
    }

    public void shutdown() {
        scheduler.shutdownNow();
    }
}

Пример использования:

final Debouncer debouncer = new Debouncer();
debouncer.debounce(Void.class, new Runnable() {
    @Override public void run() {
        // ...
    }
}, 300, TimeUnit.MILLISECONDS);

Ответ 3

Я не знаю, существует ли он, но его нужно просто реализовать.

class Debouncer implements Callback {

  private CallBack c;
  private volatile long lastCalled;
  private int interval;

  public Debouncer(Callback c, int interval) {
     //init fields
  }

  public void call(Object arg) { 
      if( lastCalled + interval < System.currentTimeMillis() ) {
        lastCalled = System.currentTimeMillis();
        c.call( arg );
      } 
  }
}

Конечно, этот пример немного упрощает его, но это более или менее все, что вам нужно. Если вы хотите сохранить отдельные тайм-ауты для разных аргументов, вам понадобится Map<Object,long>, а не только long, чтобы отслеживать последнее время выполнения.

Ответ 4

Следующая реализация работает над потоками, основанными на обработчиках (например, основной поток пользовательского интерфейса или в IntentService). Он ожидает, что он будет вызван только из потока, на котором он создан, и он также будет запускать его действие в этом потоке.

public class Debouncer
{
    private CountDownTimer debounceTimer;
    private Runnable pendingRunnable;

    public Debouncer() {

    }

    public void debounce(Runnable runnable, long delayMs) {
        pendingRunnable = runnable;
        cancelTimer();
        startTimer(delayMs);
    }

    public void cancel() {
        cancelTimer();
        pendingRunnable = null;
    }

    private void startTimer(final long updateIntervalMs) {

        if (updateIntervalMs > 0) {

            // Debounce timer
            debounceTimer = new CountDownTimer(updateIntervalMs, updateIntervalMs) {

                @Override
                public void onTick(long millisUntilFinished) {
                    // Do nothing
                }

                @Override
                public void onFinish() {
                    execute();
                }
            };
            debounceTimer.start();
        }
        else {

            // Do immediately
            execute();
        }
    }

    private void cancelTimer() {
        if (debounceTimer != null) {
            debounceTimer.cancel();
            debounceTimer = null;
        }
    }

    private void execute() {
        if (pendingRunnable != null) {
            pendingRunnable.run();
            pendingRunnable = null;
        }
    }
}

Ответ 5

Похоже, это может сработать:

class Debouncer implements Callback {
    private Callback callback;
    private Map<Integer, Timer> scheduled = new HashMap<Integer, Timer>();
    private int delay;

    public Debouncer(Callback c, int delay) {
        this.callback = c;
        this.delay = delay;
    }

    public void call(final Object arg) {
        final int h = arg.hashCode();
        Timer task = scheduled.remove(h);
        if (task != null) { task.cancel(); }

        task = new Timer();
        scheduled.put(h, task);

        task.schedule(new TimerTask() {
            @Override
            public void run() {
                callback.call(arg);
                scheduled.remove(h);
            }
        }, this.delay);
    }
}

Ответ 6

Моя реализация, очень простая в использовании, 2 утилитных метода для debounce и throttle, передайте туда свой runnable, чтобы получить runnable debounce/throttle

package basic.thread.utils;

public class ThreadUtils {
    /** Make a runnable become debounce
     * 
     * usage: to reduce the real processing for some task
     * 
     * example: the stock price sometimes probably changes 1000 times in 1 second,
     *  but you just want redraw the candlestick of k-line chart after last change+"delay ms"
     * 
     * @param realRunner Runnable that has something real to do
     * @param delay milliseconds that realRunner should wait since last call
     * @return
     */
    public static Runnable debounce (Runnable realRunner, long delay) {
        Runnable debounceRunner = 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) {
                    now = System.currentTimeMillis();
                    // update time to run each time
                    _timeToRun = now+_delay;
                    // another thread is waiting, skip
                    if (_isWaiting) return;
                    // set waiting status
                    _isWaiting = true;
                }
                try {
                    // wait until target time
                    while (now < _timeToRun) {
                        Thread.sleep(_timeToRun-now);
                        now = System.currentTimeMillis();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // clear waiting status before run
                    _isWaiting = false;
                    // do the real task
                    _realRunner.run();
                }
            }};
        return debounceRunner;
    }
    /** Make a runnable become throttle
     * 
     * usage: to smoothly reduce running times of some task
     * 
     * example: assume the price of a stock often updated 1000 times per second
     * but you want to redraw the candlestick of k-line at most once per 300ms
     * 
     * @param realRunner
     * @param delay
     * @return
     */
    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;
    }
}

Ответ 7

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

Выполнение обратного вызова:

public interface cbDebounce {

void execute();

}

Debouncer:

public class Debouncer {

private Timer timer;
private ConcurrentHashMap<String, TimerTask> delayedTaskMap;

public Debouncer() {
    this.timer = new Timer(true); //run as daemon
    this.delayedTaskMap = new ConcurrentHashMap<>();
}

public void debounce(final String key, final cbDebounce debounceCallback, final long delay) {
    if (key == null || key.isEmpty() || key.trim().length() < 1 || delay < 0) return;

    cancelPreviousTasks(); //if any

    TimerTask timerTask = new TimerTask() {
        @Override
        public void run() {
            debounceCallback.execute();
            cancelPreviousTasks();
            delayedTaskMap.clear();
            if (timer != null) timer.cancel();
        }
    };

    scheduleNewTask(key, timerTask, delay);
}

private void cancelPreviousTasks() {
    if (delayedTaskMap == null) return;

    if (!delayedTaskMap.isEmpty()) delayedTaskMap
            .forEachEntry(1000, entry -> entry.getValue().cancel());

    delayedTaskMap.clear();
}

private void scheduleNewTask(String key, TimerTask timerTask, long delay) {
    if (key == null || key.isEmpty() || key.trim().length() < 1 || timerTask == null || delay < 0) return;

    if (delayedTaskMap.containsKey(key)) return;

    timer.schedule(timerTask, delay);

    delayedTaskMap.put(key, timerTask);
}

}

Главная (для проверки)

public class Main {

private static Debouncer debouncer;

public static void main(String[] args) throws IOException, InterruptedException {
    debouncer = new Debouncer();
    search("H");
    search("HE");
    search("HEL");
    System.out.println("Waiting for user to finish typing");
    Thread.sleep(2000);
    search("HELL");
    search("HELLO");
}

private static void search(String searchPhrase) {
    System.out.println("Search for: " + searchPhrase);
    cbDebounce debounceCallback = () -> System.out.println("Now Executing search for: "+searchPhrase);
    debouncer.debounce(searchPhrase, debounceCallback, 4000); //wait 4 seconds after user last keystroke
}

}

Выход

  • Искать: H
  • Искать: HE
  • Искать: HEL
  • Ожидание, когда пользователь закончит печатать
  • Искать: АД
  • Искать: HELLO
  • Сейчас выполняется поиск: HELLO