Java Swing 和并发 - 在操作发生之前休眠请求

Java Swing and Concurrency - sleeping requests before an action takes place

我正在尝试开发一种在经过最短时间后安排 Runnable 的方法。 代码应该从发出 请求 开始,倒计时直到经过一段时间,然后执行 Runnable。 但我还需要可以发出多个请求,并且对于每个新请求,延迟将在执行 Runnable 之前更新。

目标是实现以下行为: 当用户滚动 JList 时,JListJScrollPane 的垂直滚动条中的调整侦听器将在执行 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);
               }
            });
         }
      });
   }
}