So I created a basic swing interface with a stop button, and clicking it should stop a counting thread. When the application starts, a thread instance would allocate a runnable
class which does a counting loop and logs it in the console. There is a method in the runnable
interface that sets the volatile variable to false that should basically stop the thread and I called it on the stop button, but why doesn't it stop the loop? Here is my code.
ParentContainer.java
public class ParentContainer extends JFrame implements ActionListener, WindowListener {
private static final long serialVersionUID = 1L;
private JButton btnStop;
private static CountRunnable cr;
public ParentContainer() {
Container cp = getContentPane();
cp.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 10));
btnStop = new JButton("Stop Counting");
cp.add(btnStop);
SymAction lSymAction = new SymAction();
btnStop.addActionListener(lSymAction);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setTitle("Counter");
setSize(200, 90);
setVisible(true);
}
public static void main(String[] args) {
// Run GUI codes in Event-Dispatching thread for thread safety
javax.swing.SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new ParentContainer(); // Let the constructor do the job
}
});
cr = new CountRunnable();
Thread t1 = new Thread(cr, "MyRunnable");
t1.start();
}
class SymAction implements ActionListener
{
public void actionPerformed(ActionEvent evt)
{
Object object = evt.getSource();
if (object == btnStop) { btnStop_actionPerformed(evt); }
}
}
void btnStop_actionPerformed(ActionEvent evt)
{
cr.stop();
}
@Override
public void windowActivated(WindowEvent arg0) { }
@Override
public void windowClosed(WindowEvent arg0) { }
@Override
public void windowClosing(WindowEvent arg0) { }
@Override
public void windowDeactivated(WindowEvent arg0) { }
@Override
public void windowDeiconified(WindowEvent arg0) { }
@Override
public void windowIconified(WindowEvent arg0) { }
@Override
public void windowOpened(WindowEvent arg0) { }
@Override
public void actionPerformed(ActionEvent arg0) { }
}
CountRunnable.java
public class CountRunnable implements Runnable {
public static volatile boolean keepRunning;
private int count = 1;
@Override
public void run() {
keepRunning = true;
while (keepRunning)
{
for (int i = 0; i < 100000; ++i) {
System.out.println(count + "");
++count;
// Suspend this thread via sleep() and yield control to other threads.
// Also provide the necessary delay.
try {
Thread.sleep(10); // milliseconds
}
catch (InterruptedException ex) {
}
}
}
}
public void stop()
{
keepRunning = false;
}
}
When making a stop condition, it is important that you check the condition frequently. At the moment, you have a loop that runs for a while, and you only see the result of your check at the end of the loop.
To make sure the loop ends mid-iteration, you can add the following check inside the loop.
if(!keepRunning) {
break;
}
There are different solutions you can also try. Because this pattern of stopping threads is frequently used, you can also use a SwingWorker
for your sub task this class gives a fair amount of useful utility methods for stopping, and for updating the gui thread safe from your subtask (so for example, you can show count in the gui instead of the command line)
Changing your code to use a SwingWorker
is easy, when extending SwingWorker
, it expects 2 elements, a "final result", and a "pending result".
Example of SwingWorker
:
SwingWorker<String, String> task = new SwingWorker<String, String>(){
private int count = 1;
@Override
public List<Integer> doInBackground() throws Exception {
while (!isCancelled()) {
for (int i = 0; i < 100000; ++i) {
publish(count + "");
++count;
Thread.sleep(10); // milliseconds
}
}
return count + "";
}
@Override
protected void process(List<Integer> chunks) {
gui.setText(chunk.get(chunk.size() - 1));
}
@Override
protected void done() {
try {
gui.setText(this.get());
} catch(InterruptedException | ExecutionException ex) {
ex.printSTackTrace();
}
}
}
task.execute();
// Later:
task.cancel(false);
Notice that canceling with false is recommended, because when doing it with true means that the thread executing the counter will be interrupted, causing the Thread.sleep() to throw an exception.
If the purpose of your task is only to count a number, it may be even easier to use a timer (Make sure you import the correct one) class instead, using the class is as easy as:
int delay = 10; //milliseconds
ActionListener taskPerformer = new ActionListener() {
int count = 0;
public void actionPerformed(ActionEvent evt) {
count++;
gui.setText(count + "");
// System.out.println(count + "");
}
};
Timer task = new Timer(delay, taskPerformer);
task.setCoalesce(false); // SOmetimes, executions can be missed because other programs
task.setRepeating(true);
task.start();
// Later:
task.stop();
Notice that with this timer solution, you can restart it again by calling start()
, unlike the other solutions I shown you.