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

gthanop :

I am trying to develop a means of scheduling a Runnable after a least amount of time has elapsed. The code should start with a request being made and count down until an amount of time has elapsed, and then execute the 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.

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. 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. So the waiting and executing of the Runnable should occur in a different Thread (than the EDT). After a least amount of time has elapsed, from the last made request, the Runnable is executed.

I need this behaviour because the JList will contain many thousands of thumbnails of images. I don't want to pre-load all the thumbnails in the JList because they might not fit into memory. 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).

What I have tried is to create an entirely handmade scheduler with worker Threads. 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). 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:

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.

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? But how? I'm guessing something in the java.util.concurrent package should be an answer.

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

Thank you.

FredK :

Here's an example of using a Swing timer. Pressing the button will restart the 2-second delay.

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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=216694&siteId=1