Java Swing and Concurrency - спящие запросы до начала действия - PullRequest
1 голос
/ 11 марта 2020

Я пытаюсь разработать способ планирования Runnable по истечении наименьшего количества времени. Код должен начинаться с выполнения запроса и обратного отсчета до истечения определенного времени, а затем выполнить Runnable. Но мне также нужно, чтобы было сделано более одного запроса, и для каждого нового запроса задержка будет продлеваться до выполнения Runnable.

Цель состоит в том, чтобы добиться следующего поведения: Когда пользователь прокручивает JList, слушатель настройки в вертикальной полосе прокрутки JList JScrollPane запросит задержку перед выполнением Runnable. Каждый раз, когда пользователь прокручивает новый запрос, задержка возобновляется. Запрос немедленно возвращается, так что EDT блокируется на минимальное время. Таким образом, ожидание и выполнение Runnable должно происходить в другом Thread (чем EDT). По истечении наименьшего количества времени после последнего выполненного запроса Runnable выполняется.

Мне нужно такое поведение, поскольку JList будет содержать многие тысячи миниатюр изображений. Я не хочу предварительно загружать все миниатюры в JList, потому что они могут не помещаться в память. Я не хочу загружать миниатюры, когда пользователь прокручивает, потому что он может делать произвольные быстрые прокрутки, позвольте мне поставить его. Поэтому я хочу начать загружать миниатюры только после того, как пользователь ждет / обосновывается в одном месте в JList в течение некоторого времени (например, 500 мс, 1 секунда или что-то между).

Что Я попытался создать полностью 1010 * ручной планировщик с рабочим 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...
    });
});

Но я не знаю если это тоже хорошая практика, и она также потребляет больше процессорного времени, я думаю.

Мой вопрос, хотя и не для самого экологичного решения, но есть ли существует лучший / более формальный способ достижения это с меньшим количеством суматохи / кода. Например, я должен использовать java.util.Timer, javax.swing.Timer или ScheduledExecutorService? Но как? Я предполагаю, что что-то в пакете java.util.concurrent должно быть ответом.

Меня не особо волнует сверхточность задержки, как вы можете себе представить.

Любые рекомендации в комментариях о других подходах к достижению той же цели также были бы хорошими.

Я на самом деле не прошу отладки, но я также не думаю, что этот вопрос следует перенести в Код Просмотрите , потому что я прошу альтернативное / лучшее решение.

Я бы предпочел, чтобы это было в Java 8 (и выше, если невозможно с 8).

Спасибо.

1 Ответ

1 голос
/ 11 марта 2020

Вот пример использования таймера 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);
               }
            });
         }
      });
   }
}
...