0

I have a swing application that is quite slow to start up because it has to load a thousand pictures into the GUI. It takes 10 seconds to start up.

It is a single thread application, how could I code multithread to speed up the task? The following code is in a for loop of 1000 iterations.

    ImageIcon icon = new ImageIcon(Files.readAllBytes(coverFile.toPath()));
    // ImageIcon icon = createImageIcon(coverFile);
    JLabel label = null;
    if (coverCount % 2 == 0) {
     label = createLabel(coverFile, movieFile, icon, SwingConstants.LEFT);
    } else {
     label = createLabel(coverFile, movieFile, icon, SwingConstants.CENTER);
    }
    box.add(label);

The images are being loaded and put into a Box sequentially. I have two difficulties if I want to do it multithread

  1. How does a thread return value to parent
  2. How to achieve non-blocking call back which add the image to the box sequentially

Thank you.

cssko
  • 3,027
  • 1
  • 18
  • 21
lamwaiman1988
  • 3,729
  • 15
  • 55
  • 87
  • Why do you think that multi threading would speed this up? – DontKnowMuchBut Getting Better Apr 30 '19 at 12:30
  • Note that background threading would prevent freezing of the GUI if done right, but still the bottleneck is the read-image code, which threading would not likely improve. Also note that you would likely use a call-back to notify the GUI when the thread has completed, such as via a SwingWorker's `done()` method or a `PropertyChangeListener` added to the worker thread, one that listens for worker completion. – DontKnowMuchBut Getting Better Apr 30 '19 at 12:31
  • 2
    Are all 1000 images displayed in the GUI at the same time? If not then only load the ones that are displaying currently, and load the rest on demand. – JonK Apr 30 '19 at 12:32
  • To add images sequentially, use the SwingWorker's publish/process method pair as per the standard tutorial. This would mean using a `SwingWorker` generic signature, or something similar (i.e., BufferedImage instead of Image, if desired). – DontKnowMuchBut Getting Better Apr 30 '19 at 12:33
  • @DontKnowMuchButGettingBetter Why would you think it would not speed up? The above code in a loop with 1k will surely not produce 100% disk utilization (or whatever the images are read from). And the images are probably decoded for internal usage which probably also benefits from parallelism. – cmoetzing Apr 30 '19 at 12:41
  • @cmoetzing: it depends on where the bottleneck resides. I would posit that the physical reading is a lot slower than the decoding, and this is not sped up via threading. – DontKnowMuchBut Getting Better Apr 30 '19 at 12:45
  • @cmoetzing: then again, [this answer](https://stackoverflow.com/questions/34280566/java-multithreading-with-imageio) suggests that multi-threading can help. – DontKnowMuchBut Getting Better Apr 30 '19 at 12:46
  • 1
    The real answer is: identify the bottle neck **first**. Measure what is going on, and where time is spent. – GhostCat Apr 30 '19 at 12:49
  • 1
    @GhostCat Generally true, but likely not relevant in this case: When a swing application loads 1000 images at startup and takes 10 seconds to start, then one can assume causality here. The question still lacks details that are necessary to suggest the most appropriate workaround, but some of the answers already show conceivable approaches. – Marco13 Apr 30 '19 at 13:05
  • 2
    @GhostCat One point that you may have reffered to definitely **is** relevant, though: Whether or not loading the images can be sped up via **multithreading** has to be evaluated. If the images are read from a hard disc, then accessing it with multiple threads might actually be *slower* than sequential access. (This is independent of using a background thread to prevent the UI from blocking) – Marco13 Apr 30 '19 at 13:08
  • 2
    Note that [`Toolkit.getImage(String)`](https://docs.oracle.com/en/java/javase/11/docs/api/java.desktop/java/awt/Toolkit.html#getImage(java.lang.String)) will load images asynchronously. The app. can load and display while the images are still loading. – Andrew Thompson Apr 30 '19 at 13:11
  • Hi, I have identify the bottleneck is the image loading already, because removing the image loading code made the GUI load immediately. – lamwaiman1988 Apr 30 '19 at 13:23
  • To emphasize Andrew Thompson’s point: replace the first line of your code with `ImageIcon icon = new ImageIcon(Toolkit.getDefaultToolkit().getImage(coverFile.toString()));`. The images will be loaded in the background; no need for you to create your own threads. – VGR Apr 30 '19 at 14:26

3 Answers3

2

How does a thread return value to parent

Use a call-back mechanism. For Swing that would mean using a SwingWorker and notifying the GUI of thread completion either in the worker's done() method or by adding a PropertyChangeListener to the worker, listening to the worker's "state" property, for when it changes to SwingWorker.StateValue.DONE

How to achieve non-blocking call back which add the image to the box sequentially

The SwingWorker has a publish/process method pair that allows sending data sequentially from the background thread via the publish method, and then handle the data sequentially on the event thread within the process method. This requires use of a SwingWorker<VOID, Image> or SwingWorker<VOID, Icon> or something similar, the 2nd generic parameter indicating the type of object sent via this mechanism.

For example:

public class MyWorker extends SwingWorker<Void, Icon> {
    @Override
    protected Void doInBackground() throws Exception {
        boolean done = false;
        while (!done) {
            // TODO: create some_input here -- possibly a URL or File
            Image image = ImageIO.read(some_input);
            Icon icon = new ImageIcon(image);
            publish(icon);

            // TODO: set done here to true when we ARE done
        }
        return null;
    }

    @Override
    protected void process(List<Icon> chunks) {
        for (Icon icon : chunks) {
            // do something with the icon here
            // on the event thread
        }
    }
}

And to use it within a GUI:

// may need constructor to pass GUI into worker
final MyWorker myWorker = new MyWorker();
myWorker.addPropertyChangeListener(evt -> {
    if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
        // We're done!
        // call get to trap errors
        try {
            myWorker.get();
        } catch (InterruptedException | ExecutionException e) {
            // TODO: real error handling needed here
            e.printStackTrace();
        }
    }
});
myWorker.execute(); // start worker in a background thread

For more on Swing concurrency, please check out Lesson: Concurrency in Swing

0

Multithreading will speed up the application but I think doing a lazy load is a better approach (you can do both). You can't be displaying all these images at the same time so I suggest you load the images that will be visible at the start and after that load the image as needed this will hugely increase your performance and use less memory/resource.

anthony yaghi
  • 532
  • 2
  • 10
0

If you really want to load all 1000 images:

It is enough to use one background thread, so that you don't slow down the main Swing Event loop thread.

Create a custom class which implements runnable, and has references to all the context to do the job. Like so:

public static class IconLoader implements Runnable{
    private List<File> movies;
    private File coverFile;
    private JPanel box;
    public IconLoader(JPanel box, File coverFile, List<File> movies) {
        this.box = box;
        this.coverFile = coverFile;
        this.movies = movies;
    }

    @Override
    public void run() {

        for(int coverCount=0;coverCount<movies.size();coverCount++) {
            try {
                final JLabel label;
                File movieFile = movies.get(coverCount);
                ImageIcon icon = new ImageIcon(Files.readAllBytes(coverFile.toPath()));
                // ImageIcon icon = createImageIcon(coverFile);

                if (coverCount % 2 == 0) {
                 label = createLabel(coverFile, movieFile, icon, SwingConstants.LEFT);
                } else {
                 label = createLabel(coverFile, movieFile, icon, SwingConstants.CENTER);
                }

                SwingUtilities.invokeLater( new Runnable() {
                    @Override
                    public void run() {
                        box.add(label);
                    }
                });
            }catch(IOException e) {
                e.printStackTrace();
            }
        }
    }

    private JLabel createLabel(File coverFile, File movieFile, ImageIcon icon, int direction) {
        //Create the label and return
        return null;
    }
}

Then start the loading process during your app initialization, by passing the runnable to a new thread, and starting the thread. Like so:

new Thread( new IconLoader(box, coverFile, movies) ).start();
Teddy
  • 4,009
  • 2
  • 33
  • 55