I am making a Java Swing application. Here is a simplified program logic:
public class Data
{
//Data.
}
public class CustomPanel extends JPanel implements MouseListener
{
private Data dataReference;
@Override
public void paintComponent(Graphics g)
{
//Represent data as images.
}
@Override
public void mouseReleased(MouseEvent e)
{
new CustomSwingWorker().doInBackground();
}
}
public class CustomSwingWorker extends SwingWorker<Void, Void>
{
protected Void doInBackground() throws Exception
{
//Recalculate data. Very long process.
}
protected void done()
{
//Repaint GUI.
}
}
When a user presses a mouse button, new SwingWorker background thread is created to recalculate the data in order not to block the GUI. Hovewer, now two threads have access to this data. Let's assume a user presses a mouse button and then resizes the window. SwingWorker thread recalculates data and at the same time EDT repaints CustomPanel using mutual data.
I understand that I can use syncronized methods. But if I synchronize data getters and setters, EDT can become blocked. This way I am back with the same problem that I tried to aviod in the first place - blocking the EDT.
Also keep in mind that this is not a producer-consumer problem. GUI can safely repaint itself using the old data. But it should not repaint itself with data that is in the middle of recalculation.
My question is this: what is the common solution to this kind of problem? What synchronization techniques should be used to guard mutual data when it is used in EDT?
The linked question in the first comment of the question already provides a solution to the mutual data sharing problem you describe, which is that you can just copy the intermediate data from the SwingWorker
's thread to the EDT (via publish
).
Copying data obviously allocates more memory and requires extra time, but no matter how big objects Data
holds, the copy operation can be offloaded to the doInBackground
method of the SwingWorker
(when process
is called on the EDT it will have its own exclusive copy of the data), thus not freezing the EDT (from the user's perspective). Remember that immutable data should not be copied (thus preserving memory and time). Only intermediate data needs to be copied, ie only data which changes as the background work goes on. Actually you may don't even have to copy the whole data that changes but instead of this you could copy the difference of the changed data (but that would depend on how efficiently the difference could be applied on the data that the EDT has access to).
As far as I understand from your post you have images in the Data
class. In this case you could create the images inside the doInBackground
and publish
them for process
ing on the EDT. For an example (similar to the linked question), which is overly simplified (it doesn't even have getters and setters):
public class Data
{
//Let's assume Data holds a single image (which is actually the only thing which differs
//between publishes in the long background process for the sake of the example)
Image img;
}
public class CustomPanel extends JPanel implements MouseListener
{
private Data dataReference = new Data();
@Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
// Get/access image from dataReference and draw it:
g.drawImage(dataReference.img, 0, 0, this);
}
@Override
public void mouseReleased(MouseEvent e)
{
new CustomSwingWorker(this).execute();
}
public void updateImage(Image img)
{
// Set/add/put the image into the dataReference (we are on the EDT):
dataReference.img = img;
}
}
public class CustomSwingWorker extends SwingWorker<Void, Image>
{
private CustomPanel panel;
public CustomSwingWorker(CustomPanel panel)
{
this.panel = panel;
}
protected Void doInBackground() throws Exception
{
/*
For each step in the long process do:
Recalculate data.
Create a NEW image and draw on it.
publish() the new image and ignore it aftewards.
*/
}
protected void process(List<Image> images)
{
//Here we are on the EDT...
//Assume we are only interested in the last image each time (if multiple publishes occur before a single process method call):
panel.updateImage(images.get(images.size() - 1));
}
}
SwingWorker
needs for starting its background operation. An MRE in the question should help an/any answerer provide specific information oriented around your own specific problem/use-case, so that the solution is more obvious. In the sample code given in this answer there was no need to copy the panel's data to the SwingWorker
upon its construction. There is no data copying in the code provided above, only references passed around plus the allocation of new data which is processed on the background thread (see the body of doInBackground
method) and then published/passed to the EDT. If for any reason initialization data needs to be copied from the EDT to the SwingWorker
then one possibility could be its constructor, but don't take my word for your scenario since I believe you haven't provided enough clarifications/details on it.