Java Swing 和并发 - 在操作发生之前休眠请求
Java Swing and Concurrency - sleeping requests before an action takes place
我正在尝试开发一种在经过最短时间后安排 Runnable
的方法。
代码应该从发出 请求 开始,倒计时直到经过一段时间,然后执行 Runnable
。
但我还需要可以发出多个请求,并且对于每个新请求,延迟将在执行 Runnable
之前更新。
目标是实现以下行为:
当用户滚动 JList
时,JList
的 JScrollPane
的垂直滚动条中的调整侦听器将在执行 Runnable
之前请求延迟。
每次用户滚动时都会发出新的请求,因此延迟会更新。
立即请求 returns,以便 EDT 被阻塞的时间最短。
因此 Runnable
的等待和执行应该发生在不同的 Thread
(与 EDT 不同)。
经过最短时间后,从上次发出的请求开始,Runnable
被执行。
我需要这种行为,因为 JList
将包含数千个图像缩略图。
我不想预加载 JList
中的所有缩略图,因为它们可能不适合内存。
我也不想在用户滚动时加载缩略图,因为他可以随意快速滚动让我说。
因此,我只想在用户 waits/settles 之后在 JList
中的一个位置开始加载缩略图一段时间(例如 500 毫秒、1 秒或介于两者之间)。
我尝试的是创建一个完全 手工制作的 调度程序,其中包含工作人员 Thread
。
按照我的努力,评论中有相关解释:
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 时间。
虽然我的问题不是针对最生态的解决方案,但是否存在 better/more-formal 以更少 commotion/code 实现此目的的方法。
例如,我应该使用 java.util.Timer
, a javax.swing.Timer
, or a ScheduledExecutorService
吗?但是怎么办?
我猜 java.util.concurrent
包中的东西应该是答案。
我并不像你想象的那样真正关心延迟的超精确度。
评论中关于实现相同目标的其他方法的任何建议也很好。
我并不是真的要求调试,但我也不认为这个问题应该移至 Code Review,因为我要求 alternative/better 解决方案。
我希望它在 Java 8(及以上,如果不能用 8)。
谢谢。
下面是一个使用 Swing 定时器的例子。按下按钮将重新启动 2 秒延迟。
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Delay extends JPanel {
Timer timer;
int presses = 0;
public Delay() {
setLayout(new BorderLayout());
JButton b = new JButton("Sleep 2 seconds");
JLabel label = new JLabel("The app is currently asleep.");
add(b, BorderLayout.CENTER);
add(label, BorderLayout.SOUTH);
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
timer.restart();
presses++;
}
});
timer = new Timer(2000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
label.setText("Time expired after " + presses + " presses");
}
});
timer.start();
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
final JFrame jf = new JFrame();
JPanel panel = new Delay();
jf.add(panel);
jf.pack();
jf.setVisible(true);
jf.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(final WindowEvent arg0) {
System.exit(0);
}
});
}
});
}
}
我正在尝试开发一种在经过最短时间后安排 Runnable
的方法。
代码应该从发出 请求 开始,倒计时直到经过一段时间,然后执行 Runnable
。
但我还需要可以发出多个请求,并且对于每个新请求,延迟将在执行 Runnable
之前更新。
目标是实现以下行为:
当用户滚动 JList
时,JList
的 JScrollPane
的垂直滚动条中的调整侦听器将在执行 Runnable
之前请求延迟。
每次用户滚动时都会发出新的请求,因此延迟会更新。
立即请求 returns,以便 EDT 被阻塞的时间最短。
因此 Runnable
的等待和执行应该发生在不同的 Thread
(与 EDT 不同)。
经过最短时间后,从上次发出的请求开始,Runnable
被执行。
我需要这种行为,因为 JList
将包含数千个图像缩略图。
我不想预加载 JList
中的所有缩略图,因为它们可能不适合内存。
我也不想在用户滚动时加载缩略图,因为他可以随意快速滚动让我说。
因此,我只想在用户 waits/settles 之后在 JList
中的一个位置开始加载缩略图一段时间(例如 500 毫秒、1 秒或介于两者之间)。
我尝试的是创建一个完全 手工制作的 调度程序,其中包含工作人员 Thread
。
按照我的努力,评论中有相关解释:
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 时间。
虽然我的问题不是针对最生态的解决方案,但是否存在 better/more-formal 以更少 commotion/code 实现此目的的方法。
例如,我应该使用 java.util.Timer
, a javax.swing.Timer
, or a ScheduledExecutorService
吗?但是怎么办?
我猜 java.util.concurrent
包中的东西应该是答案。
我并不像你想象的那样真正关心延迟的超精确度。
评论中关于实现相同目标的其他方法的任何建议也很好。
我并不是真的要求调试,但我也不认为这个问题应该移至 Code Review,因为我要求 alternative/better 解决方案。
我希望它在 Java 8(及以上,如果不能用 8)。
谢谢。
下面是一个使用 Swing 定时器的例子。按下按钮将重新启动 2 秒延迟。
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Delay extends JPanel {
Timer timer;
int presses = 0;
public Delay() {
setLayout(new BorderLayout());
JButton b = new JButton("Sleep 2 seconds");
JLabel label = new JLabel("The app is currently asleep.");
add(b, BorderLayout.CENTER);
add(label, BorderLayout.SOUTH);
b.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
timer.restart();
presses++;
}
});
timer = new Timer(2000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
label.setText("Time expired after " + presses + " presses");
}
});
timer.start();
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
final JFrame jf = new JFrame();
JPanel panel = new Delay();
jf.add(panel);
jf.pack();
jf.setVisible(true);
jf.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(final WindowEvent arg0) {
System.exit(0);
}
});
}
});
}
}