【问题标题】:implementing debounce in Java在 Java 中实现去抖动
【发布时间】:2011-01-20 00:10:10
【问题描述】:

对于我正在编写的一些代码,我可以在 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 标准库中是否已经存在(类似的东西)?
  • 您将如何实现它?

【问题讨论】:

标签: java algorithm


【解决方案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);
}

【讨论】:

  • @levinalex:刚刚修复了 call(..) 中的一个错误。添加循环是为了确保我们永远不会有不在地图中的计划任务。
  • Eyal,我有一个问题。假设间隔为 5 秒。如果每 3 秒调用一次,则回调将永远不会运行。对吗?
  • @AlexanderSuraphel:没错。假设你使用同一个key,执行时间会一次次延迟。
  • 您应该在try 块之后添加catch,因为任何异常都会取消去抖动时调用的回调
  • 这个类怎么用?
【解决方案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&lt;Object,long&gt; 而不仅仅是 long 来跟踪上次执行时间。

    【讨论】:

    • 我需要的是相反的。回调应该在每组调用的 end 处调用。 (我想用它来实现this)这似乎需要线程/超时
    • @levinalex 我仍然认为你可以让它以这种方式工作,但如果你不这样做,请不要使用线程,而是使用 TimerScheduledExecutorService,这样会更清洁、更安全.
    • 感谢这些。我现在正努力做到这一点。 (我以前从未做过 Java 并发)
    • 计时器似乎正是我所需要的。
    【解决方案4】:

    我的实现,非常易于使用,2 个用于 debounce 和 throttle 的 util 方法,将你的 runnable 传递给它以获得 debounce/throttle runnable

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

    【讨论】:

      【解决方案5】:

      这是我的实现,Java:

      SimpleDebounce.java

      
      import android.os.Handler;
      
      public class SimpleDebounce {
          protected Handler handler;
          protected IAfterDelay iAfterDelay;
          protected long last_time_invoke = 0;
          protected long delay;
      
          public SimpleDebounce() {
              this.handler = new Handler();
          }
      
          public SimpleDebounce(long delay, IAfterDelay iAfterDelay) {
              this();
              this.delay = delay;
              this.iAfterDelay = iAfterDelay;
          }
      
          public void after(long delay, IAfterDelay iAfterDelay) {
              this.delay = delay;
              this.iAfterDelay = iAfterDelay;
              this.iAfterDelay.loading(true);
              this.handler.removeCallbacks(execute);
              this.last_time_invoke = System.currentTimeMillis();
              this.handler.postDelayed(execute, delay);
          }
      
          public void cancelDebounce() {
              if (handler != null && iAfterDelay != null) {
                  handler.removeCallbacks(execute);
                  iAfterDelay.loading(false);
              }
          }
      
          public interface IAfterDelay {
              void fire();
      
              void loading(boolean state);
          }
      
          protected Runnable execute = () -> {
              if (System.currentTimeMillis() > (last_time_invoke + delay - 500)) {
                  if (iAfterDelay != null) {
                      iAfterDelay.loading(false);
                      iAfterDelay.fire();
                  }
              }
          };
      
      }
      
      

      MainActivity.java

      
      public class MainActivity extends AppCompatActivity {
      
         private SimpleDebounce simpleDebounce;
         private long waitForMS = 5000;
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_my_stocks);
      
              simpleDebounce = new SimpleDebounce();
      
              // You can click this button as many time as you want
              // It will reset the time and fire after ${waitForMS} milisecons
              // and in case you pressed this in the middle again, it will reset the time
              someButtonWhichStartsThis.setOnClickListener(e -> {
                 simpleDebounce.after(waitForMS, new SimpleDebounce.IAfterDelay() {
                      @Override
                      public void fire() {
                         // Your job...
                      }
      
                      @Override
                      public void loading(boolean state) {
                          if (state) turnOnProgress();
                          else turnOffProgress();
                      }
                });
              });
      
              // stop the future fire in the middle, if you want to
              someButtonWhichStopsThis.setOnClickListener(e -> simpleDebounce.cancelDebounce());
      
          }
         
      
      }
      
      

      【讨论】:

      • 非常感谢您提供简单有效的解决方案。我申请了,我的购物车增加和减少数量都很好。
      【解决方案6】:

      这看起来可行:

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

      【讨论】:

      • 您是否曾经将对象添加到哈希映射中?而且,你永远不应该使用hashCode 作为键,因为它很容易产生冲突。更不用说不同类型的对象很容易拥有相等的哈希码,即使它们自己的哈希函数是完美的。
      • 修复了实际安排事情的答案。我会用什么来代替 hashCode?​​span>
      • 只需使用实际对象作为键(即Map&lt;Object, Timer&gt;)。 HashMap 然后在内部使用对象的哈希码快速跳转到包含您的项目(以及可能具有相同哈希码的其他项目)的存储桶,但之后忽略哈希码并将实际的 Object 与那个桶来找到匹配的。特长;每当您在代码中调用 hashCode() 时,您很可能做错了什么。
      • -1 这个方案不是线程安全的,即使是一个线程也不能满足要求。正如 Groo 声称的那样,具有相同哈希码的不同参数将取消彼此的任务。此外,创建多个计时器效率不高,也没有必要。
      • 人们可能应该使用stackoverflow.com/questions/18723112/… 中给出的解决方案,而不是我的黑客尝试。我真的不懂Java。
      【解决方案7】:

      以下实现适用于基于 Handler 的线程(例如主 UI 线程或 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;
              }
          }
      }
      

      【讨论】:

        【解决方案8】:

        这是我的工作实现:

        执行回调:

        public interface cbDebounce {
        
        void execute();
        
        }
        

        去抖:

        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's last keystroke
        }
        
        }
        

        输出

        • 搜索:H
        • 搜索:他
        • 搜索:HEL
        • 等待用户完成输入
        • 搜索:地狱
        • 搜索:HELLO
        • 现在正在执行搜索:HELLO

        【讨论】:

          【解决方案9】:

          我已经更新了@Eyal 的答案,以便能够在每次调用中配置去抖动时间,并使用可运行代码块而不是回调:

          import java.util.concurrent.ConcurrentHashMap;
          import java.util.concurrent.Executors;
          import java.util.concurrent.ScheduledExecutorService;
          import java.util.concurrent.TimeUnit;
          
          public class Debouncer<T> {
          
              private final ScheduledExecutorService sched = Executors.newScheduledThreadPool(1);
              private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<T, TimerTask>();
          
              public Debouncer() {
              }
          
              public void call(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
                  TimerTask task = new TimerTask(key, runnable, interval, timeUnit);
          
                  TimerTask prev;
                  do {
                      prev = delayedMap.putIfAbsent(key, task);
                      if (prev == null)
                          sched.schedule(task, interval, timeUnit);
                  } while (prev != null && !prev.extend());
              }
          
              public void terminate() {
                  sched.shutdownNow();
              }
          
              private class TimerTask implements Runnable {
                  private final T key;
                  private final Runnable runnable;
                  private final int interval;
                  private final TimeUnit timeUnit;
                  private long dueTime;
                  private final Object lock = new Object();
          
                  public TimerTask(T key, Runnable runnable, int interval, TimeUnit timeUnit) {
                      this.key = key;
                      this.runnable = runnable;
                      this.interval = interval;
                      this.timeUnit = timeUnit;
                      extend();
                  }
          
                  public boolean extend() {
                      synchronized (lock) {
                          if (dueTime < 0)
                              return false;
                          dueTime = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(interval, timeUnit);
                          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 {
                                  runnable.run();
                              } finally {
                                  delayedMap.remove(key);
                              }
                          }
                      }
                  }
              }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2013-11-17
            • 2017-07-01
            • 1970-01-01
            • 2022-06-16
            • 2019-12-31
            • 2021-10-28
            • 2021-09-08
            • 2021-07-09
            相关资源
            最近更新 更多