Swing多線程編碼過(guò)程中的誤區(qū)
很多學(xué)JAVA程序員都是從Swing開始的,但很多人對(duì)AWT GUI線程的機(jī)制并沒(méi)有太深的了解,或者說(shuō)一直都只了解線程的概念,而不了解AWT對(duì)線程的使用。我發(fā)現(xiàn)很多人碰到線程阻塞的問(wèn)題,就通過(guò)調(diào)用 SwingUtilities.invokeLater()來(lái)解決。
其實(shí)這是很容易造成誤會(huì)的地方:
- 不要以為Swing 是多線程的,實(shí)際上Swing 的UI是單線程的
- 不要以為SwingUtilities.的兩個(gè)invoke是多線程,實(shí)際上它還是單線程的
- 不要以為invokeLater的意思是當(dāng)前線程執(zhí)行完再執(zhí)行目標(biāo)線程;以為invokeAndWait的意思是等待目標(biāo)線程執(zhí)行完再執(zhí)行當(dāng)前線程,實(shí)際上壓根就不是那么回事
問(wèn)題代碼1:大意是在按下某個(gè)按鈕的時(shí)候調(diào)用一個(gè)遠(yuǎn)程服務(wù)
- JButton button = new JButton();
- button.addActionListener(new ActionListener(){
- @Override
- public void actionPerformed(ActionEvent e) {
- invokeRemoteService();//可能需要等待
- }
- });
在swing系統(tǒng)中,有一個(gè)頂級(jí)的java.awt.Container(可能是一個(gè)JFrame或JDialog實(shí)例),負(fù)責(zé)啟動(dòng)一個(gè)EventDispatchThread線程,單線程,這個(gè)線程是負(fù)責(zé)處理UI事件的。
首先,界面Swing控件向EventDispatchThread的EventQueue提交一個(gè)event,由 EventDispatchThread負(fù)責(zé)調(diào)度各個(gè)event的執(zhí)行。例如,按下一個(gè)JButton的時(shí)候,JButton向EventQueue執(zhí)行 postEvent,提交一個(gè)ActionEvent。EventDispatchThread線程根據(jù)調(diào)度算法執(zhí)行到該event的時(shí)候,會(huì)調(diào)用 JButton上的processActionEvent,JButton再調(diào)用actionPerformed,這過(guò)程并沒(méi)有執(zhí)行任何new Thread().start()代碼,也就是說(shuō)JButton的ActionListener.actionPerformed()中的代碼完全是在 EventDispatchThread線程內(nèi)執(zhí)行的。
所以,假如我們?cè)谌魏蜛ctionListener、MouseListener等對(duì)象中編寫耗時(shí)的邏輯,那么整個(gè)Swing系統(tǒng)就會(huì)出現(xiàn)響應(yīng)遲鈍的現(xiàn)象,更有甚者,如果在這些Listener中執(zhí)行線程wait(),以等待另一個(gè)線程的鎖定資源或計(jì)算結(jié)果,那么實(shí)際上就是 EventDispatchThread線程被阻塞,整個(gè)系統(tǒng)界面就會(huì)處于無(wú)響應(yīng)狀態(tài),一點(diǎn)反應(yīng)都沒(méi)有。
以上是誤解1造成的,了解這個(gè)過(guò)程,就很容易看出上面這段代碼的問(wèn)題是什么原因了。解決的方法也倒比較簡(jiǎn)單,直接new Thread().start();就可以保證EventDispatchThread執(zhí)行到當(dāng)前方法的時(shí)候快速返回,以便可以去響應(yīng)來(lái)自用戶界面的其他事件。
問(wèn)題代碼2:大意是在按下某個(gè)按鈕的時(shí)候調(diào)用一個(gè)遠(yuǎn)程服務(wù),同時(shí)處理其他事情
- JButton button = new JButton();
- button.addActionListener(new ActionListener(){
- @Override
- public void actionPerformed(ActionEvent e) {
- //位置A
- SwingUtilities.invokeLater(new Runnable() {
- public void run() {
- //位置B
- invokeRemoteService();//可能需要等待
- }
- });
- doOtherThing();
- }
- });
這段代碼跟第一段代碼唯一的差別是doOtherThing()在invokeRemoteService ()完成之前就能夠得到執(zhí)行,所以造成了invokeRemoteService ()/doOtherThing()好像是在兩個(gè)線程里執(zhí)行的假象。實(shí)際上invokeLater是把目標(biāo)代碼打包成一個(gè)Event提交到 EventQueue去了,等到EventDispatchThread線程執(zhí)行完當(dāng)前代碼段的doOtherThing()后,再去執(zhí)行這個(gè) EventQueue中的Event,這時(shí)候就會(huì)執(zhí)行到這個(gè)invokeRemoteService ()方法。但是,實(shí)際上這兩個(gè)方法都是在EventDispatchThread中執(zhí)行的,并沒(méi)有任何其他Thread來(lái)執(zhí)行。于是,問(wèn)題1的問(wèn)題還是沒(méi)解決。實(shí)際上直接new Thread().start()方法就可以了,使用SwingUtilities完全是由于誤解造成的濫用。
測(cè)試方法,在位置A和位置B都加上下面這行代碼:
- System.out.println(Thread.currentThread().getId() + Thread.currentThread().getName());
返回的結(jié)果都是一樣的:
21AWT-EventQueue-0 21AWT-EventQueue-0
[討論]
一般情況下(除了系統(tǒng)啟動(dòng)時(shí)后臺(tái)創(chuàng)建的Daemon線程),系統(tǒng)的所有執(zhí)行功能邏輯和業(yè)務(wù)邏輯的線程都應(yīng)該是從界面操作觸發(fā)的。我們應(yīng)該清楚哪些需要或應(yīng)該放到EventDispatchThread中去執(zhí)行,哪些需要或應(yīng)該創(chuàng)建一個(gè)新線程去執(zhí)行,也需要清醒的知道自己當(dāng)前編寫的是屬于什么邏輯。
這個(gè)問(wèn)題我覺(jué)得應(yīng)該把代碼分成3層,第一層,UI層,包括UI控件上的Listener邏輯,這是應(yīng)該給EventDispatchThread 去執(zhí)行的,必須簡(jiǎn)短高效,快速return;這一層做不完的事情通過(guò)new Thread().start()交給下一層去做,我稱之為控制層;然后控制層再去調(diào)用具體的業(yè)務(wù)代碼,即第三層,業(yè)務(wù)層。所有由UI控件觸發(fā)的邏輯都應(yīng)該這么分。
另一個(gè)問(wèn)題是,Swing并不推薦在EventDispatchThread之外修改界面,那么,如果我們?cè)跇I(yè)務(wù)層需要repaint某個(gè)控件,或者updateUI應(yīng)該怎么辦呢,那就可以使用SwingUtilities來(lái)處理了,這才是正確使用SwingUtilities的場(chǎng)景,也是設(shè)計(jì)這個(gè)工具的目的。
原文鏈接:http://seaman.iteye.com/blog/608584
【編輯推薦】