Java更多的庫謎題82:啤酒爆炸

字號:

這一章的許多謎題都涉及到了多線程,而這個謎題涉及到了多進程。如果你用一行命令行帶上參數(shù)slave去運行這個程序,它會打印什么呢?如果你使用的命令行不帶任何參數(shù),它又會打印什么呢?
    public class BeerBlast{
     static final String COMMAND = "java BeerBlast slave";
     public static void main(String[] args) throws Exception{
     if(args.length == 1 && args[0].equals("slave")) {
     for(int i = 99; i > 0; i--){
     System.out.println( i +
     " bottles of beer on the wall" );
     System.out.println(i + " bottles of beer");
     System.out.println(
     "You take on down, pass it around,");
     System.out.println( (i-1) +
     " bottles of beer on the wall");
     System.out.println();
     }
     }else{
     // Master
     Process process = Runtime.getRuntime().exec(COMMAND);
     int exitValue = process.waitFor();
     System.out.println("exit value = " + exitValue);
     }
     }
    }
    如果你使用參數(shù)slave來運行該程序,它就會打印出那首激動人心的名為”99 Bottles of Beer on the Wall”的童謠的歌詞,這沒有什么神秘的。如果你不使用該參數(shù)來運行這個程序,它會啟動一個slave進程來打印這首歌謠,但是你看不到slave進程的輸出。主進程會等待slave進程結(jié)束,然后打印出slave進程的退出值(exit value)。根據(jù)慣例,0值表示正常結(jié)束,所以0就是你可能期望該程序打印的東西。如果你運行了程序,你可能會發(fā)現(xiàn)該程序只會懸掛在那里,不會打印任何東西,看起來slave進程好像永遠都在運行著。所以你可能會覺得你應該一直都能聽到”99 Bottles of Beer on the Wall”這首童謠,即使是這首歌被唱走調(diào)了也是如此,但是這首歌只有99句,而且,電腦是很快的,你假設的情況應該是不存在的,那么這個程序出了什么問題呢?
    這個秘密的線索可以在Process類的文檔中找到,它敘述道:“由于某些本地平臺只提供有限大小的緩沖,所以如果未能迅速地讀取子進程(subprocess)的輸出流,就有可能會導致子進程的阻塞,甚至是死鎖” [Java-API]。這恰好就是這里所發(fā)生的事情:沒有足夠的緩沖空間來保存這首冗長的歌謠。為了確保slave進程能夠結(jié)束,父進程必須排空(drain)它的輸出流,而這個輸出流從master線程的角度來看是輸入流。下面的這個工具方法會在后臺線程中完成這項工作:
     static void drainInBackground(final InputStream is) {
     new Thread(new Runnable(){
     public void run(){
     try{
     while( is.read() >= 0 );
     } catch(IOException e){
     // return on IOException
     }
     }
     }).start();
     }
    如果我們修改原有的程序,在等待slave進程之前調(diào)用這個方法,程序就會打印出0:
     }else{ // Master
     Process process = Runtime.getRuntime().exec(COMMAND);
     drainInBackground(process.getInputStream());
     int exitValue = process.waitFor();
     System.out.println("exit value = " + exitValue);
     }
    這里的教訓是:為了確保子進程能夠結(jié)束,你必須排空它的輸出流;對于錯誤流(error stream)也是一樣,而且它可能會更麻煩,因為你無法預測進程什么時候會傾倒(dump)一些輸出到這個流中。在5.0版本中,加入了一個名為ProcessBuilder的類用于排空這些流。它的redirectErrorStream方法將各個流合并起來,所以你只需要排空這一個流。如果你決定不合并輸出流和錯誤流,你必須并行地(concurrently)排空它們。試圖順序化地(sequentially)排空它們會導致子進程被掛起。
    多年以來,很多程序員都被這個缺陷所刺痛。這里對于API設計者們的教訓是,Process類應該避免這個錯誤,也許應該自動地排空輸出流和錯誤流,除非用戶表示要讀取它們。更一般的講,API應該設計得更容易做出正確的事,而很難或不可能做出錯誤的事