【发布时间】:2020-06-23 19:11:56
【问题描述】:
我正在尝试开发一种在经过最少时间后安排Runnable 的方法。
代码应从发出请求 开始并倒计时直到经过一段时间,然后执行Runnable。
但我还需要可以发出多个请求,并且对于每个新请求,延迟将在 Runnable 执行之前更新。
目标是实现以下行为:
当用户滚动JList 时,JList 的JScrollPane 的垂直滚动条中的调整侦听器将在执行Runnable 之前请求延迟。
每次用户滚动时都会发出一个新请求,因此会更新延迟。
请求立即返回,以便 EDT 被阻塞最少的时间。
因此,Runnable 的等待和执行应该发生在不同的Thread(而不是 EDT)中。
在经过最少时间后,从上次发出的请求开始,Runnable 被执行。
我需要这种行为,因为JList 将包含成千上万的图像缩略图。
我不想预加载JList 中的所有缩略图,因为它们可能不适合内存。
我也不想在用户滚动时加载缩略图,因为他可以进行任意快速滚动让我说。
因此,我只想在用户在JList 中的单个位置等待/定居一段时间(例如 500 毫秒、1 秒或介于两者之间)后开始加载缩略图。
我所尝试的是用工人Threads 创建一个完全手工的调度程序。
跟随我的努力,在cmets中有相关解释:
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.LongConsumer;
public class SleepThenActScheduler {
public class WorkerThread extends Thread {
//How long will we be waiting:
private final TimeUnit sleepUnit;
private final long sleepAmount;
public WorkerThread(final TimeUnit sleepUnit,
final long sleepAmount) {
this.sleepUnit = sleepUnit;
this.sleepAmount = sleepAmount;
}
public TimeUnit getSleepUnit() {
return sleepUnit;
}
public long getSleepAmount() {
return sleepAmount;
}
@Override
public void run() {
try {
if (sleepUnit != null)
sleepUnit.sleep(sleepAmount); //Wait for the specified time.
synchronized (SleepThenActScheduler.this) {
if (t == this && whenDone != null) { //If we are the last request:
//Execute the "Runnable" in this worker thread:
whenDone.accept(System.currentTimeMillis() - start);
//Mark the operation as completed:
whenDone = null;
t = null;
}
}
}
catch (final InterruptedException ix) {
//If interrupted while sleeping, simply do nothing and terminate.
}
}
}
private LongConsumer whenDone; //This is the "Runnable" to execute after the time has elapsed.
private WorkerThread t; //This is the last active thread.
private long start; //This is the start time of the first request made.
public SleepThenActScheduler() {
whenDone = null;
t = null;
start = 0; //This value does not matter.
}
public synchronized void request(final TimeUnit sleepUnit,
final long sleepAmount,
final LongConsumer whenDone) {
this.whenDone = Objects.requireNonNull(whenDone); //First perform the validity checks and then continue...
if (t == null) //If this is a first request after the runnable executed, then:
start = System.currentTimeMillis(); //Log the starting time.
else //Otherwise we know a worker thread is already running, so:
t.interrupt(); //stop it.
t = new WorkerThread(sleepUnit, sleepAmount);
t.start(); //Start the new worker thread.
}
}
它的用法看起来像下面的代码(如果可能的话,我想在你可能的答案中保持相关性):
SleepThenActScheduler sta = new SleepThenActScheduler();
final JScrollPane listScroll = new JScrollPane(jlist);
listScroll.getVerticalScrollBar().addAdjustmentListener(adjustmentEvent -> {
sta.request(TimeUnit.SECONDS, 1, actualElapsedTime -> {
//Code for loading some thumbnails...
});
});
但是这段代码会为每个请求创建一个新的Thread(并中断最后一个请求)。
我不知道这是否是一个好习惯,所以我也尝试使用单个 Thread 循环睡眠,直到从最后一次发出请求开始经过请求的时间:
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.LongConsumer;
public class SleepThenActThread extends Thread {
public static class TimeAmount implements Comparable<TimeAmount> {
private final TimeUnit unit;
private final long amount;
public TimeAmount(final TimeUnit unit,
final long amount) {
this.unit = unit;
this.amount = amount;
}
public void sleep() throws InterruptedException {
/*Warning: does not take into account overflows...
For example what if we want to sleep for Long.MAX_VALUE days?...
Look at the implementation of TimeUnit.sleep(...) to see why I am saying this.*/
if (unit != null)
unit.sleep(amount);
}
public TimeAmount add(final TimeAmount tammt) {
/*Warning: does not take into account overflows...
For example what if we want to add Long.MAX_VALUE-1 days with something else?...*/
return new TimeAmount(TimeUnit.NANOSECONDS, unit.toNanos(amount) + tammt.unit.toNanos(tammt.amount));
}
@Override
public int compareTo(final TimeAmount tammt) {
/*Warning: does not take into account overflows...
For example what if we want to compare Long.MAX_VALUE days with something else?...*/
return Long.compare(unit.toNanos(amount), tammt.unit.toNanos(tammt.amount));
}
}
private static TimeAmount requirePositive(final TimeAmount t) {
if (t.amount <= 0) //+NullPointerException.
throw new IllegalArgumentException("Insufficient time amount.");
return t;
}
private LongConsumer runnable;
private TimeAmount resolution, total;
public SleepThenActThread(final TimeAmount total,
final TimeAmount resolution) {
this.resolution = requirePositive(resolution);
this.total = requirePositive(total);
}
public synchronized void setResolution(final TimeAmount resolution) {
this.resolution = requirePositive(resolution);
}
public synchronized void setTotal(final TimeAmount total) {
this.total = requirePositive(total);
}
public synchronized void setRunnable(final LongConsumer runnable) {
this.runnable = Objects.requireNonNull(runnable);
}
public synchronized TimeAmount getResolution() {
return resolution;
}
public synchronized TimeAmount getTotal() {
return total;
}
public synchronized LongConsumer getRunnable() {
return runnable;
}
public synchronized void request(final TimeAmount requestedMin,
final LongConsumer runnable) {
/*In order to achieve requestedMin time to elapse from this last made
request, we can simply add the requestedMin time to the total time:*/
setTotal(getTotal().add(requestedMin));
setRunnable(runnable);
if (getState().equals(Thread.State.NEW))
start();
}
@Override
public void run() {
try {
final long startMillis = System.currentTimeMillis();
TimeAmount current = new TimeAmount(TimeUnit.NANOSECONDS, 0);
while (current.compareTo(getTotal()) < 0) {
final TimeAmount res = getResolution();
res.sleep();
current = current.add(res);
}
getRunnable().accept(System.currentTimeMillis() - startMillis);
}
catch (final InterruptedException ix) {
}
}
}
(注意:第二种方法没有完全调试,但我想你明白了。)
而且它的用法会像下面的代码:
SleepThenActThread sta = new SleepThenActThread(new TimeAmount(TimeUnit.SECONDS, 1), new TimeAmount(TimeUnit.MILLISECONDS, 10));
final JScrollPane listScroll = new JScrollPane(jlist);
listScroll.getVerticalScrollBar().addAdjustmentListener(adjustmentEvent -> {
sta.request(new TimeAmount(TimeUnit.SECONDS, 1), actualElapsedTime -> {
//Code for loading some thumbnails...
});
});
但我也不知道这是否是一个好习惯,而且我猜这也会消耗更多的 CPU 时间。
我的问题不是针对最生态的解决方案,而是是否存在一种更好/更正式的方式来实现这一目标,同时减少骚动/代码。
例如,我应该使用java.util.Timer、javax.swing.Timer 还是ScheduledExecutorService?但是怎么做?
我猜java.util.concurrent 包中的内容应该是答案。
我并不真正关心延迟的超精确度,正如你想象的那样。
cmets 中关于实现相同目标的其他方法的任何建议也是好的。
我并不是真的要求调试,但我也不认为应该将这个问题转移到 Code Review,因为我要求的是替代/更好的解决方案。
我希望它使用 Java 8(及更高版本,如果 8 不可能的话)。
谢谢。
【问题讨论】:
-
使用 java.swing.Timer 并在发出新请求时调用 timer.restart()。
-
@FredK 感谢您的评论。听起来很简单。我不知道,我没想到会这么简单。 :) 我会测试它。
-
GUI 用户是否可以使用其他 JCompronents 选择一个或多个选项,这样他就不必滚动浏览数千张图像?
-
@GilbertLeBlanc 问题是用户将在从目录加载这些图像后将它们一一分类。我的意思是他们不会以任何方式预先分类。如果是的话,我确实可以让他先选择一个类别,然后再给他看图片。
-
根据您的评论,一次加载 50 张左右的图像并让应用程序为进行分类的用户提供短暂的休息时间可能会更好的用户体验。
标签: java multithreading swing sleep event-dispatch-thread