Reputation: 45
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
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 process
ed 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
exampleimport 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:
SwingWorker
s workUpvotes: 3
Reputation: 1048
Before I explain the solution to your problem, I would like to point out some issues with your code.
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.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