Внедрение 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?
- Как бы вы это реализовали?
Ответы
Ответ 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