事件派發(fā)線程(EDT)
理解SwingUtilities類作用的前提是先理解事件派發(fā)線程的概念捞镰。
當(dāng)運(yùn)行一個(gè) Swing 程序時(shí),會(huì)自動(dòng)創(chuàng)建三個(gè)線程。
1.主線程岸售,負(fù)責(zé)執(zhí)行main 方法几迄。
- toolkit 線程,負(fù)責(zé)捕捉系統(tǒng)事件冰评,比如鍵盤(pán)、鼠標(biāo)移動(dòng)等木羹,程序員不會(huì)有任何代碼在這個(gè)線程上執(zhí)行甲雅。Toolkit線程的作用是把自己捕獲的事件傳遞給第三個(gè)線程,也就是事件派發(fā)線程坑填。
- 事件派發(fā)線程(EDT抛人,Event Dispatcher Thread),顧名思義是用來(lái)派發(fā)事件(根據(jù)事件找到對(duì)應(yīng)的事件處理代碼)的線程脐瑰。EDT接收來(lái)自 toolkit 線程的事件妖枚,并且將這些事件組織成一個(gè)隊(duì)列,EDT的工作內(nèi)容就是將這個(gè)隊(duì)列中的事件按照順序派發(fā)給相應(yīng)的事件監(jiān)聽(tīng)器苍在,并且調(diào)用事件監(jiān)聽(tīng)器中的回調(diào)函數(shù)绝页,這也意味著,所有的事件處理代碼都是在EDT而不是主線程中執(zhí)行寂恬。
上面說(shuō)到EDT中維護(hù)了一個(gè)事件的隊(duì)列续誉,并且它們是按照順序派發(fā)的。由于事件派發(fā)是單線程的操作初肉,所以只有等待前面事件監(jiān)聽(tīng)器的回調(diào)函數(shù)執(zhí)行完畢酷鸦,才能夠執(zhí)行組件更新的操作,以及繼續(xù)派發(fā)后面的事件牙咏。這樣導(dǎo)致的一個(gè)后果就是:當(dāng)在一個(gè)事件監(jiān)聽(tīng)回調(diào)函數(shù)中做了耗時(shí)的操作臼隔,那么,界面會(huì)因此停住妄壶,并且界面上所有控件失效(不可觸發(fā))摔握。
解決這個(gè)問(wèn)題的方法是:在事件處理函數(shù)中將耗時(shí)的操作放到新線程(一般稱之為工作線程)中執(zhí)行,而不是讓其在EDT中執(zhí)行盯拱。比如下面的例子盒发。
案例
一個(gè)窗口,有一個(gè)按鈕和一個(gè)label狡逢。點(diǎn)擊按鈕宁舰,系統(tǒng)將做模仿導(dǎo)入數(shù)據(jù)的動(dòng)作,導(dǎo)入數(shù)據(jù)之前需要檢測(cè)數(shù)據(jù)的合法性奢浑。并且蛮艰,檢測(cè)數(shù)據(jù)和導(dǎo)入數(shù)據(jù)這兩個(gè)步驟都需要耗費(fèi)一定的時(shí)間。
如果沒(méi)有之前說(shuō)到的EDT的概念雀彼,那么你可能會(huì)這么做:
importBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
try{
lb.setText("1.檢查數(shù)據(jù)合法性...");
Thread.sleep(3000);//模仿檢測(cè)數(shù)據(jù)合法性
lb.setText("2.正在導(dǎo)入數(shù)據(jù)...");
Thread.sleep(4000);//模仿導(dǎo)入數(shù)據(jù)
lb.setText("3.導(dǎo)入成功!");
}catch (InterruptedException e1) {
e1.printStackTrace();
}
}
});
但是壤蚜,如果運(yùn)行一下的話即寡,會(huì)發(fā)現(xiàn)現(xiàn)象是這樣:點(diǎn)擊按鈕,界面卡住袜刷,按鈕變得不可觸發(fā)聪富,直到一段時(shí)間(7秒)之后界面顯示“3.導(dǎo)入成功”。期間并沒(méi)有顯示“1.檢查數(shù)據(jù)合法性”和“2.正在導(dǎo)入數(shù)據(jù)”著蟹。
這個(gè)現(xiàn)象印證了上面說(shuō)的理論:當(dāng)事件派發(fā)線程中正在執(zhí)行的事件監(jiān)聽(tīng)函數(shù)執(zhí)行完畢墩蔓,才能進(jìn)行UI組件的刷新操作,并且派發(fā)事件隊(duì)列中的下一個(gè)萧豆。
下面是修改后的代碼奸披,將耗時(shí)的操作放在一個(gè)新的工作線程中執(zhí)行:
importBtn.addActionListener(newActionListener() {
@Override
public voidactionPerformed(ActionEvent e) {
new Thread(new Runnable() {//開(kāi)辟一個(gè)工作線程
@Override
public void run() {
try {
lb.setText("1.檢查數(shù)據(jù)合法性...");
Thread.sleep(3000);//模仿檢測(cè)數(shù)據(jù)合法性
lb.setText("2.正在導(dǎo)入數(shù)據(jù)...");
Thread.sleep(4000);//模仿導(dǎo)入數(shù)據(jù)
lb.setText("3.導(dǎo)入成功!");
} catch(InterruptedException e1) {
e1.printStackTrace();
}
}
}).start();
}
});
主題
下面到了SwingUtilities的內(nèi)容。在swing編程中有一個(gè)編程原則:所有的界面相關(guān)的更新涮雷,都應(yīng)該在 EDT 上執(zhí)行阵面,否則會(huì)導(dǎo)致界面繪制出現(xiàn)不穩(wěn)定性錯(cuò)誤。這也就意味著上面代碼的lb.setText("2.正在導(dǎo)入數(shù)據(jù)...");應(yīng)該在EDT中洪鸭,而非新的工作線程中執(zhí)行样刷。這樣就出現(xiàn)了一個(gè)矛盾:耗時(shí)的操作必須要在工作線程中執(zhí)行,否則會(huì)出現(xiàn)界面刷新不及時(shí)和卡頓的現(xiàn)象览爵,而工作線程中的界面刷新代碼又會(huì)導(dǎo)致界面繪制的不穩(wěn)定颂斜。
SwingUtilities可以解決這個(gè)矛盾。SwingUtilities的invokeLater和invokeAndWait方法可以將一個(gè)可執(zhí)行對(duì)象(Runnable)實(shí)例追加到EDT的可執(zhí)行隊(duì)列中拾枣。那么最后的代碼應(yīng)該是這樣的:
importBtn.addActionListener(newActionListener() {
@Override
public voidactionPerformed(ActionEvent e) {
new Thread(new Runnable() {
@Override
public void run() {
try {
SwingUtilities.invokeLater(new Runnable() {
@Override
public voidrun() {
lb.setText("1.檢查數(shù)據(jù)合法性...");
}
});
Thread.sleep(3000);//模仿檢測(cè)數(shù)據(jù)合法性
SwingUtilities.invokeLater(new Runnable() {
@Override
public voidrun() {
lb.setText("2.正在導(dǎo)入數(shù)據(jù)...");
}
});
Thread.sleep(4000);//模仿導(dǎo)入數(shù)據(jù)
SwingUtilities.invokeLater(newRunnable() {
@Override
public voidrun() {
lb.setText("3.導(dǎo)入成功!");
}
});
} catch(InterruptedException e1) {
e1.printStackTrace();
}
}
}).start();
}
});
結(jié)論
通過(guò)上面內(nèi)容可以總結(jié)以下兩個(gè)swing編程原則:
所有的界面相關(guān)的更新沃疮,都應(yīng)該在 EDT 上執(zhí)行
而耗時(shí)的后臺(tái)運(yùn)行,不應(yīng)該在 EDT 上執(zhí)行
lambda表達(dá)式重寫(xiě)上面的代碼
importBtn.addActionListener(e -> {
new Thread(() -> {
try {
SwingUtilities.invokeLater(() -> lb.setText("1.檢查數(shù)據(jù)合法性..."));
Thread.sleep(3000);//模仿檢測(cè)數(shù)據(jù)合法性
SwingUtilities.invokeLater(()-> lb.setText("2.正在導(dǎo)入數(shù)據(jù)..."));
Thread.sleep(4000);//模仿導(dǎo)入數(shù)據(jù)
SwingUtilities.invokeLater(() -> lb.setText("3.導(dǎo)入成功!"));
} catch (InterruptedExceptione1) {
e1.printStackTrace();
}
}).start();
});