dude123345
dude123345

Reputation: 45

How to update GUI continuously with multithreading

I have made a minimal representation of my full program to show what I need help with, the program consists of a JFrame, the main class, and a task class.

The task class generates random integers that are used to paint strings onto the GUI. I made it so that the user can use any number of threads that each perform this same task.

My goal is to display the ending "solution" which for now is not a real solution. But, as each thread generates integer values, I want to continuously update the GUI so that it can keep showing solutions until it reaches the final "true" solution.

How can I do this?

When I try to pass the GUI through it only updates once. I tried looking into Swing Workers but I want to use my own thread implementation, and I cannot figure out how to implement it correctly, or how exactly it would work in a concurrent implementation.

import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Task extends Thread {

    private static Lock taskLock = new ReentrantLock();
    private Random rand = new Random();
    private static int y;
    static volatile ArrayList<Integer> finallist = new ArrayList<>();
    private GUI g;

    Task(GUI g) {
        this.g = g;
    }

    private void generatePoint() {
        taskLock.lock();
        y = rand.nextInt(1000);
        taskLock.unlock();
    }

    public void run() {
        generatePoint();
        int copy = y;
        finallist.add(copy);
        g.setData(copy);
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {

        GUI g = new GUI();
        g.setVisible(true);
        Scanner input = new Scanner(System.in);
        System.out.println("Please input the number of Threads you want to create: ");
        int n = input.nextInt();
        System.out.println("You selected " + n + " Threads");

        Thread[] threads = new Thread[n];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Task(g);
            threads[i].start();
        }

        for (int j = 0; j < n; j++) {
            threads[j].join();
        }

        for(Integer s: Task.finallist) {
            if(s > 30) {
                g.setData(s);       //in full program will find true solution
            }
        }
    }
}

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

    public class GUI extends JFrame implements ActionListener {
        private boolean check;
        private int y;
        GUI()  {
            initialize();
        }

        private void initialize()  {
            check = false;
            y = 0;
            this.setLayout(new FlowLayout());
            this.setSize(1000,1000);
            this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        }

        @Override
        public void actionPerformed(ActionEvent e) {

        }

        @Override
        public void paint(Graphics g) {
            super.paint(g);
            if(check) {
                g.drawString("here",500,y);
            }
        }

        void setData(int y) {
            check = true;
            this.y = y;
            repaint();
        }

    }

Upvotes: 0

Views: 889

Answers (2)

MadProgrammer
MadProgrammer

Reputation: 347184

One of the important aspects you need to keep in mind is that Swing is not thread safe, this means you should not update the UI or update any value that the UI is reliant on from outside of the context Event Dispatching Thread.

There are a number of ways you deal with this. SwingUtilities.invokeLater, but you really should only use it if the number of times it would be called are small or the time between them is relatively larger (seconds). The main reason for this, is it's very easy to overload the EDT, which can cause the UI to "stall".

Another approach is to use Swing Timer, but this is really only useful if you want to update the UI on a regular based (like animation).

Another approach is to use a SwingWorker. This is a "fancy" wrapper around a Executor which allows you to perform work on a background thread and then publish the results to the EDT which can then safely be processed for the UI.

But why do this? Well, one of things that SwingWorker does is, if your Thread is generating a lot of data in a small amount of time, it will squeeze a number of results into a List and reduce the number of calls made back to the UI, helping maintain some level of performance.

However, in your case, it's probably just a really nice exercise, as you are creating a single thread/worker to do a single job and not getting a single worker/thread to do all the jobs.

In that case, I might consider using a Swing Timer to probe a model of the data instead of having the Thread/worker try and update the UI itself, this way you gain control over the number of updates which are occurring to the UI.

There are pros and cons to all these and you will need to investigate which one(s) work best for your particular scenario

SwingWorker example

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;

public class GUI {

    public static void main(String[] args) {
        new GUI();
    }

    public GUI() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane(10));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public static interface Consumer {

        void setData(int value);
    }

    public static class WorkerTask extends SwingWorker<Void, Integer> {

        private static Lock taskLock = new ReentrantLock();
        private Random rand = new Random();
        private static int y;
        // I'd prefer this was a model object passed into the worker
        static volatile ArrayList<Integer> finallist = new ArrayList<>();

        private Consumer consumer;

        public WorkerTask(Consumer consumer) {
            this.consumer = consumer;
        }

        private int generatePoint() {
            taskLock.lock();
            int copy = 0;
            try {
                copy = rand.nextInt(200);
                y = copy;
                // Array list isn't thread safe ;)
                finallist.add(copy);
            } finally {
                taskLock.unlock();
            }
            return copy;
        }

        @Override
        protected Void doInBackground() throws Exception {
            // And a random delay
            // You should now be able to see the text change
            Thread.sleep(rand.nextInt(1000));
            publish(generatePoint());
            return null;
        }

        @Override
        protected void process(List<Integer> chunks) {
            if (chunks.isEmpty()) {
                return;
            }
            int last = chunks.get(chunks.size() - 1);
            consumer.setData(last);
        }

    }

    public class TestPane extends JPanel {

        private boolean check;
        private int y;

        private CountDownLatch latch;

        public TestPane(int count) {
            latch = new CountDownLatch(count);
            for (int i = 0; i < count; i++) {
                WorkerTask task = new WorkerTask(new Consumer() {
                    @Override
                    public void setData(int value) {
                        // This is on the EDT, so it's safe to update the UI with
                        TestPane.this.setData(value);
                    }
                });
                task.addPropertyChangeListener(new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        WorkerTask task = (WorkerTask) evt.getSource();
                        if (task.getState() == SwingWorker.StateValue.DONE) {
                            latch.countDown();
                        }
                    }
                });
                task.execute();
            }

            // This only matters if you want the result to be delivered
            // on the EDT.  You could get away with using a Thread otherwise
            SwingWorker waiter = new SwingWorker<Void, List<Integer>>() {
                @Override
                protected Void doInBackground() throws Exception {
                    System.out.println("Waiting");
                    try {
                        latch.await();
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                    System.out.println("All done here, thanks");

                    publish(WorkerTask.finallist);

                    return null;
                }

                @Override
                protected void process(List<List<Integer>> chunks) {
                    if (chunks.isEmpty()) {
                        return;
                    }
                    List<Integer> values = chunks.get(chunks.size() - 1);
                    completed(values);
                }

            };
            waiter.execute();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);

            Graphics2D g2d = (Graphics2D) g.create();
            if (check) {
                g2d.drawString("here", 100, y);
            }
            g2d.dispose();
        }

        void setData(int y) {
            check = true;
            System.out.println(y);
            this.y = y;
            repaint();
        }

        protected void completed(List<Integer> values) {
            // And executed on the EDT
            System.out.println("All your value belong to us");
            for (int value : WorkerTask.finallist) {
                System.out.println(">> " + value);
            }
        }

    }

}

I recommend looking at:

Upvotes: 3

Merve Sahin
Merve Sahin

Reputation: 1048

Before I explain the solution to your problem, I would like to point out some issues with your code.

  1. You only do one operation in each thread, and it does not seem to be a long-running one (The run method is only called once, in case you don't know). I'm not sure if you just put it for the sake of having it as a placeholder, or if it's actually part of your code. But if you don't have a long running operation, is it worth doing it in a separate thread? Think about the trade-offs.
  2. No need to join the threads, as they will probably finish almost immediately in your code.

Now to the solution, this documentation explains that Swing is not thread-safe, but there is a way to make changes on the GUI. You should use this method to make any changes: SwingUtilities.invokeLater(..).

public static void main(String[] args) throws InterruptedException {

    GUI g = new GUI();
    g.setVisible(true);
    Scanner input = new Scanner(System.in);
    System.out.println("Please input the number of Threads you want to create: ");
    int n = input.nextInt();
    System.out.println("You selected " + n + " Threads");

    Thread[] threads = new Thread[n];
    for (int i = 0; i < threads.length; i++) {
        threads[i] = new Task(g);
        threads[i].start();
    }

    for (int j = 0; j < n; j++) {
        threads[j].join();
    }

    for(Integer s: Task.finallist) {
        if(s > 30) {
            // here you submit a Runnable to the event-dispatcher thread in which the Swing Application runs
            SwingUtilities.invokeLater(() -> {
                 g.setData(s);       
            });
        }
    }
}

The same should also be done in your run method:

public void run() {
    generatePoint();
    int copy = y;
    finallist.add(copy);
    SwingUtilities.invokeLater(() -> {
        g.setData(copy);       
    });
}

Upvotes: 1

Related Questions