今天爲了測試寫了個小程序,一個小窗口,點擊按鈕時會複製一個大文件,通過Process執行cmd命令,然後界面顯示“正在複製”,複製完了讀取文件大小並顯示“複製完成”。先開始我在按鈕事件裏面這麼寫:
JButton b=new JButton("點擊啓動");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
try {
l.setText("正在複製");
l.setBounds(127, 50, 125, 19);
Process p=Runtime.getRuntime().exec("cmd /c copy /y \"D:\\工具\\系統鏡像\\deepin-15.11-64位.iso\" \"E:\\中轉\\deepin-15.11-64位.iso\"");
System.out.println(new File("E:\\中轉\\deepin-15.11-64位.iso").length());
l.setText("複製完成");
} catch (IOException e) {
e.printStackTrace();
}
}
});
我想讓它在複製的時候顯示“正在複製”,完成後顯示“複製完成”,並顯示覆制後文件大小。
結果點擊按鈕後直接顯示覆制完成,顯示大小爲0。
原來點擊按鈕時,Process p...語句及下面語句一起執行了,還沒等複製完,他就顯示“複製完成”並讀取大小了。我想要等Process執行完了再執行下面的讀取大小和顯示“複製完成”字樣,當然Process類裏面有waitFor()方法,放在Process下面,就可以實現等待命令執行完了再向下走,於是我改成這樣:
JButton b=new JButton("點擊啓動");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
try {
l.setText("正在複製");
l.setBounds(127, 50, 125, 19);
Process p=Runtime.getRuntime().exec("cmd /c copy /y \"D:\\工具\\系統鏡像\\deepin-15.11-64位.iso\" \"E:\\中轉\\deepin-15.11-64位.iso\"");
p.waitFor(); //執行命令後等待期完成再向下執行
System.out.println(new File("E:\\中轉\\deepin-15.11-64位.iso").length());
l.setText("複製完成");
} catch (Exception e) {
e.printStackTrace();
}
}
});
結果我發現,窗口連字都不顯示了,按鈕也卡住了無法操作。查查資料,發現是waitFor()導致進程阻塞。參考JDK文檔得知:如有必要,要等到由該 Process 對象表示的進程已經終止。如果已終止該子進程,此方法立即返回。但是直接調用這個方法會導致當前線程阻塞,直到退出子進程。對此JDK文檔上還有如此解釋:因爲本地的系統對標準輸入和輸出所提供的緩衝池有效,所以錯誤的對標準輸出快速的寫入何從標準輸入快速的讀入都有可能造成子進程的所,甚至死鎖。
所以問題出在緩衝區這個地方:可執行程序的標準輸出比較多,而運行窗口的標準緩衝區不夠大,所以發生阻塞。
先要知道,當Runtime對象調用exec(命令)後,JVM會啓動一個子進程,該進程會與JVM進程建立三個管道連接:標準輸入,標準輸出和標準錯誤流。
程序通過標準輸入流會不斷向標準輸出流和標準錯誤流寫數據,而JVM不讀取的話,當緩衝區滿之後將無法繼續寫入數據,最終造成阻塞在waitFor()這裏。
一、解決waitFor()線程阻塞
先來解決waitFor()阻塞的問題。其實很簡單,只要新建兩個線程,一個讀取標準輸入,一個讀取標準錯誤,在waitFor()命令之前讀出窗口的標準輸出緩衝區和標準錯誤流的內容不就行了嗎?
於是改代碼爲這樣:
JButton b=new JButton("點擊啓動");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
try {
l.setText("正在複製");
l.setBounds(127, 50, 125, 19);
Process p=Runtime.getRuntime().exec("cmd /c copy /y \"D:\\工具\\系統鏡像\\deepin-15.11-64位.iso\" \"E:\\中轉\\deepin-15.11-64位.iso\"");
new Thread() { //線程1:讀取標準輸入流
public void run() {
InputStreamReader isr=new InputStreamReader(p.getInputStream()); //獲取標準輸入流
BufferedReader br=new BufferedReader(isr);
try {
while(br.readLine()!=null) {
//只要有數據就一直讀取,但不執行任何操作
}
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
br.close(); //最後記得關閉讀取流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread() { //線程2:讀取標準錯誤流
public void run() {
InputStreamReader isr=new InputStreamReader(p.getErrorStream()); //獲取標準錯誤流
BufferedReader br=new BufferedReader(isr);
try {
while(br.readLine()!=null) {
//只要有數據就一直讀取,但不執行任何操作
}
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
br.close(); //最後記得關閉讀取流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
p.waitFor(); //執行命令後等待期完成再向下執行
System.out.println(new File("E:\\中轉\\deepin-15.11-64位.iso").length());
l.setText("複製完成");
} catch (Exception e) {
e.printStackTrace();
}
}
});
這樣,過一會發現顯示覆制完成了並且顯示了正確的文件大小。
二、GUI文字刷新問題
但是還是有個問題:點擊按鈕的時候應該顯示“正在複製”,結果點擊按鈕什麼都沒顯示。複製完成後,waitFor()語句後面的才正常執行。查了資料才發現,JButton的事件裏面語句其實都是在一個線程裏面按順序執行的。而在這個按鈕事件裏面,設置JLabel文字和waitFor()語句在一起,waitFor()爲了等待Process執行完便使線程暫時停止,導致設置文字顯示語句沒有機會執行,也就是說waitFor()和上面的設置文字語句發生衝突。那有人會問:剛剛不是新建兩個線程了嗎?
剛剛建的線程只是用於讀取標準輸入和標準錯誤流使得語句可以順利執行下去,不讓他卡死在waitFor()那裏,使得程序可以向下執行。但是設置文字的語句和waitFor()還是在一個線程裏面啊!他們兩個也會相互作用。
既然這樣,我只好又新建一個線程,把waitFor()和設置文字語句分開,先設置文字“正在複製”並執行Process,然後新建一個線程,把剛剛下面語句全部裝進新線程:
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
try {
l.setText("正在複製");
l.setBounds(127, 50, 125, 19);
Process p=Runtime.getRuntime().exec("cmd /c copy /y \"D:\\工具\\系統鏡像\\deepin-15.11-64位.iso\" \"E:\\中轉\\deepin-15.11-64位.iso\"");
new Thread() { //爲後續再建一線程
public void run() {
new Thread() { //線程1:讀取標準輸入流
public void run() {
InputStreamReader isr=new InputStreamReader(p.getInputStream()); //獲取標準輸入流
BufferedReader br=new BufferedReader(isr);
try {
while(br.readLine()!=null) {
//只要有數據就一直讀取,但不執行任何操作
}
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
br.close(); //最後記得關閉讀取流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread() { //線程2:讀取標準錯誤流
public void run() {
InputStreamReader isr=new InputStreamReader(p.getErrorStream()); //獲取標準錯誤流
BufferedReader br=new BufferedReader(isr);
try {
while(br.readLine()!=null) {
//只要有數據就一直讀取,但不執行任何操作
}
} catch(Exception e) {
e.printStackTrace();
} finally {
try {
br.close(); //最後記得關閉讀取流
} catch (IOException e) {
e.printStackTrace();
}
}
}
}.start();
try {
p.waitFor();
p.destroy();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(new File("E:\\中轉\\deepin-15.11-64位.iso").length());
l.setText("複製完成");
}
}.start();
} catch (Exception e) {
e.printStackTrace();
}
}
});
上面是一個線程裏面又新建了兩個線程,要仔細看清,理清思路。
這樣,兩個問題都解決了!
總之,解決這些問題的關鍵,就是通過新建線程,把會發生阻塞的,會相互死鎖的語句分開即可!