在 Java 中实现 debounce

2022-09-01 12:45:33

对于我正在编写的一些代码,我可以使用Java中一个很好的通用实现。debounce

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标准库中?
  • 您将如何实现它?

答案 1

请考虑以下线程安全解决方案。请注意,锁定粒度在密钥级别,因此仅调用相同的密钥会相互阻止。它还处理在调用 call(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);
          }
        }
      }
    }  
  }

和回调接口:

public interface Callback<T> {
    public void call(T t);
}

答案 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);