简体   繁体   English

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

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

I am trying to develop a means of scheduling a Runnable after a least amount of time has elapsed.我正在尝试开发一种在经过最少时间后安排Runnable的方法。 The code should start with a request being made and count down until an amount of time has elapsed, and then execute the Runnable .代码应该从发出请求开始倒计时,直到经过一段时间,然后执行Runnable But I also need that there can be more than one requests made, and for each new request the delay will be renewed before the Runnable is executed.但是我还需要可以发出多个请求,并且对于每个新请求,在执行Runnable之前将更新延迟。

The goal is to achieve the following behaviour: When the user scrolls a JList , an adjustment listener in the vertical scroll-bar of the JList 's JScrollPane will request the delay before the Runnable is executed.目标是实现以下行为:当用户滚动JListJListJScrollPane的垂直滚动条中的调整侦听器将在执行Runnable之前请求延迟。 Each time the user scrolls a new request is made, so the delay is renewed.每次用户滚动时都会发出一个新请求,因此延迟会更新。 The request returns immediately so that the EDT is blocked for the least amount of time.请求立即返回,以便EDT被阻塞的时间最短。 So the waiting and executing of the Runnable should occur in a different Thread (than the EDT).所以Runnable的等待和执行应该发生在不同的Thread (而不是 EDT)。 After a least amount of time has elapsed, from the last made request, the Runnable is executed.经过最少的时间后,从上次发出的请求开始,将执行Runnable

I need this behaviour because the JList will contain many thousands of thumbnails of images.我需要这种行为,因为JList将包含成千上万的图像缩略图。 I don't want to pre-load all the thumbnails in the JList because they might not fit into memory.我不想在JList预加载所有缩略图,因为它们可能不适合内存。 I don't want to load thumbnails as the user scrolls either, because he can make arbitrary fast scrolls let me put it.我也不想在用户滚动时加载缩略图,因为他可以随意快速滚动让我放它。 So I only want to start loading thumbnails after the user waits/settles in a single location in the JList for an amount of time (say for example 500 ms, 1 second, or something between).因此,我只想在用户在JList中的单个位置等待/稳定一段时间(例如 500 毫秒、1 秒或介于两者之间)后开始加载缩略图。

What I have tried is to create an entirely handmade scheduler with worker Thread s.我尝试过的是使用 worker Thread创建一个完全手工制作的调度程序。 Follows my effort, with relative explanations in the comments:按照我的努力,在评论中有相关解释:

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

And the usage of it will seem like the following code (which I would like to remain relevant in your possible answers if possible):它的用法看起来像以下代码(如果可能,我希望在您可能的答案中保持相关性):

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

But this code creates a new Thread for each request (and interrupts the last one).但是这段代码为每个请求创建一个新Thread (并中断最后一个)。 I don't know if this is a good practice, so I have also tried using a single Thread which loops sleeping until the requested time has elapsed from the last made request:我不知道这是否是一个好的做法,所以我也尝试使用一个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) {
        }
    }
}

(Note: the second approach is not fully debugged, but I think you get the idea.) (注意:第二种方法没有完全调试,但我想你明白了。)

And the usage of it will seem like the following code:它的用法看起来像下面的代码:

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

But I don't know if this is a good practice either, and this is also consuming more CPU time I guess.但我也不知道这是否是一个好习惯,而且我猜这也会消耗更多的 CPU 时间。

My question though is not for the most ecological solution, but is if there exists a better/more-formal way of achieving this with less commotion/code.我的问题虽然不是最生态的解决方案,但是否存在更好/更正式的方法来实现这一目标,同时减少骚动/代码。 For example should I use a java.util.Timer , a javax.swing.Timer , or a ScheduledExecutorService ?例如,我应该使用java.util.Timerjavax.swing.Timer还是ScheduledExecutorService But how?但是如何? I'm guessing something in the java.util.concurrent package should be an answer.我猜java.util.concurrent包中的一些东西应该是一个答案。

I don't really care about super accuracy in the delay as you can imagine.你可以想象,我并不真正关心延迟的超级准确性。

Any recommendations in the comments about other approaches to achieve the same goal would also be good.评论中关于实现同一目标的其他方法的任何建议也很好。

I am not really asking for debugging, but I also don't think this question should be moved to Code Review because I'm asking for an alternative/better solution.我并不是真的要求调试,但我也不认为这个问题应该转移到代码审查,因为我要求一个替代/更好的解决方案。

I would prefer this to be in Java 8 (and above, if not possible with 8).我更希望它在 Java 8 中(如果不能在 8 中实现,则在更高版本中)。

Thank you.谢谢你。

Here's an example of using a Swing timer.这是使用 Swing 计时器的示例。 Pressing the button will restart the 2-second delay.按下按钮将重新启动 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);
               }
            });
         }
      });
   }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM