Jak wykonywać długotrwałe zadania w Swing'u
Odkąd zacząłem pisać w Javie bardziej złożone aplikacje, które musiały na przykład komunikować się z jakimiś webowymi usługami lub wykonywać dłuższe obliczenia, miewałem problemy z zamarzającym GUI. Próbowałem problem ten rozwiązać na wiele sposobów, jednak zawsze znalazło się coś, co nie działało tak jak powinno - jak chociażby labele lub pola tekstowe, które powinny wyświetlać informacje o postępie zadania, a przez cały czas aż do zakończenia zadania pozostawały niezmienione.
Skonsultowałem się z bratem Google i zapoznałem z kilkoma odnalezionymi przy jego pomocy linkami, dzięki którym udało mi się opracować wizję działania Swinga i wątków w Javie.
I tak oto wszystko rozbija się o EDT, czyli Event Dispatch Thread. Jest to wątek, w którym są odrysowywane okna wraz z zawartością oraz obsługiwane wywołane z GUI akcje i zdarzenia. Ma to swoje plusy, gdyż na przykład zmiana zawartości labela w reakcji na wciśnięcie przycisku odniesie natychmiastowy i widoczny skutek. Jednak, jeśli w akcji tego samego przycisku odpalimy algorytm obliczający liczbę PI, to okaże się, że okienka nie odpowiadają, gdyż wątek EDT odpowiedzialny za ich odrysowywanie i obsługę, jest zajęty obliczaniem PI.
Logicznym w takiej sytuacji wydawałoby się, aby w akcji przycisku odpalić nasz algorytm w osobnym wątku, który będzie działał równolegle z EDT. Gdy tak zrobimy okaże się, że faktycznie nasze okienko nie będzie już blokowane. Pojawi się jednak inny problem, gdy z wątku obliczającego algorytm będziemy chcieli na przykład zmienić wartość pola tekstowego zawierającego obliczoną dotychczas wartość liczby PI. Problem ten związany jest z tym, że metody zmieniające coś w GUI, takie jak na setText(…) powinny być wywoływane z poziomu EDT, gdyż wywołanie ich poza kontrolą tego wątku nie daje nam gwarancji, że odniosą natychmiastowy skutek. Rozwiązaniem tego problemu jest klasa SwingUtilities i jedna z jej metod, tj. invokeLater.
Poniżej złączam przerobiony przeze mnie kod przykładowej aplikacji, pokazujący jak powinno się wykonywać długotrwałe zadania w Swingu oraz jakich pomyłek należy się wystrzegać. Jako, że od Javy 1.6 mamy dostęp do klasy SwingWorker, która przeznaczona jest do wykonywania takich właśnie zadań, załączam również przykład wykorzystania tej klasy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import java.util.List;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
public class InvokeExample extends javax.swing.JFrame {
private javax.swing.JButton badWayOneButton;
private javax.swing.JButton badWayTwoButton;
private javax.swing.JButton goodNewWayButton;
private javax.swing.JButton goodOldWayButton;
private javax.swing.JLabel statusLabel;
public InvokeExample() {
initComponents();
}
@SuppressWarnings("unchecked")
private void initComponents() {
java.awt.GridBagConstraints gridBagConstraints;
goodOldWayButton = new javax.swing.JButton();
goodNewWayButton = new javax.swing.JButton();
badWayOneButton = new javax.swing.JButton();
badWayTwoButton = new javax.swing.JButton();
statusLabel = new javax.swing.JLabel();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setLocationByPlatform(true);
getContentPane().setLayout(new java.awt.GridBagLayout());
goodOldWayButton.setText("Dobrze (po staremu)");
goodOldWayButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
goodOldWayButtonActionPerformed(evt);
}
});
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.weightx = 0.25;
gridBagConstraints.insets = new java.awt.Insets(10, 10, 5, 5);
getContentPane().add(goodOldWayButton, gridBagConstraints);
goodNewWayButton.setText("Dobrze (po nowemu)");
goodNewWayButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
goodNewWayButtonActionPerformed(evt);
}
});
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.weightx = 0.25;
gridBagConstraints.insets = new java.awt.Insets(10, 5, 5, 5);
getContentPane().add(goodNewWayButton, gridBagConstraints);
badWayOneButton.setText("Pierwszy zły");
badWayOneButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
badWayOneButtonActionPerformed(evt);
}
});
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.weightx = 0.25;
gridBagConstraints.insets = new java.awt.Insets(10, 5, 5, 5);
getContentPane().add(badWayOneButton, gridBagConstraints);
badWayTwoButton.setText("Drugi zły");
badWayTwoButton.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
badWayTwoButtonActionPerformed(evt);
}
});
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.weightx = 0.25;
gridBagConstraints.insets = new java.awt.Insets(10, 5, 5, 10);
getContentPane().add(badWayTwoButton, gridBagConstraints);
statusLabel.setText("Gotowe");
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 1;
gridBagConstraints.gridwidth = 4;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.insets = new java.awt.Insets(5, 10, 10, 10);
getContentPane().add(statusLabel, gridBagConstraints);
pack();
}
private void goodOldWayButtonActionPerformed(java.awt.event.ActionEvent evt) {
statusLabel.setText("Proszę czekać...");
setButtonsEnabled(false);
// Wykonujemy coś, co zajmuje dużo czasu, więc odpalamy to w
// wątku i aktualizujemy stan, gdy zadanie zostane wykonane.
Thread worker = new Thread() {
public void run() {
// Coś co zajmuje dużo czasu ... w prawdziwym życiu, może to
// być na przykład pobieranie danych z bazy danych,
// wywołanie zdalnej metody, itp.
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
}
// Aktualizujemy status zadania wykorzystując invokeLater().
SwingUtilities.invokeLater(new Runnable() {
public void run() {
statusLabel.setText("Gotowe");
setButtonsEnabled(true);
}
});
}
};
worker.start(); // Uruchamiamy wątek, aby nie blokować EDT.
}
private void goodNewWayButtonActionPerformed(java.awt.event.ActionEvent evt) {
setButtonsEnabled(false);
// Wykonujemy to samo zadanie, tym razem korzystając z klasy
// SwingWorker'a.
SwingWorker worker = new SwingWorker() {
@Override
protected void process(List chunks) {
// Aktualizujemy status zadania wykorzystując fakt, że metoda
// process() wykonywana jest w EDT.
statusLabel.setText(chunks.get(0).toString());
}
@Override
protected Object doInBackground() throws Exception {
try {
publish("Proszę czekać...");
Thread.sleep(5000);
} catch (InterruptedException ex) {
}
return null;
}
@Override
protected void done() {
// Aktualizujemy status zadania wykorzystując fakt, że metoda
// done() wykonywana jest w ETD.
statusLabel.setText("Gotowe");
setButtonsEnabled(true);
}
};
worker.execute(); // Uruchamiamy SwingWorker'a, aby nie blokować EDT.
}
private void badWayOneButtonActionPerformed(java.awt.event.ActionEvent evt) {
statusLabel.setText("Proszę czekać...");
setButtonsEnabled(false);
// Wykonujemy to samo zadanie, tym razem jednak bez wykorzystania
// dodatkowego wątku.
try {
Thread.sleep(5000); // Głodzimy wątek EDT
} catch (InterruptedException ex) {
}
// Aktualizujemy status zadania.
statusLabel.setText("Gotowe");
setButtonsEnabled(true);
}
private void badWayTwoButtonActionPerformed(java.awt.event.ActionEvent evt) {
statusLabel.setText("Proszę czekać... ");
setButtonsEnabled(false);
// Przykład niewłaściwego wykorzystania invokeLater().
// Runnable nie powinien głodzić wątku EDT.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
// Dispatch thread is starving!
Thread.sleep(5000);
} catch (InterruptedException ex) {
}
// Aktualizujemy status zadania.
statusLabel.setText("Gotowe");
setButtonsEnabled(true);
}
});
}
// Pozwala włączać i wyłaczać przyciski.
private void setButtonsEnabled(boolean b) {
goodOldWayButton.setEnabled(b);
goodNewWayButton.setEnabled(b);
badWayOneButton.setEnabled(b);
badWayTwoButton.setEnabled(b);
}
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new InvokeExample().setVisible(true);
}
});
}
}
Tak wygląda okno testowej aplikacji: