1. ストリームの読み書きで発生するデッドロック
最も厄介な不具合のひとつが、見た目は正しく実装しているのにプロセスがハングして終了しないケースです。よくある原因は、外部プロセスの出力またはエラー出力のバッファあふれです。両方のストリーム(stdout と stderr)を読まないと、誰もバッファからデータを取り出さないため、出力しようとした時点でプロセスが「ブロック」されてしまいます。
悪い例
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// stdout だけを読み、stderr は無視している!
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
コマンドが stderr に何かを書き込む場合(例えば java -version はほとんど常にそこへ書きます)、そのストリームがあふれてプロセスがハングします。
正しいやり方
両方のストリームを並行に読みましょう(別スレッドや ExecutorService を使う):
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// stdout を読む
Thread stdoutThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("[stdout] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
// stderr を読む
Thread stderrThread = new Thread(() -> {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getErrorStream()))) {
String line;
while ((line = reader.readLine()) != null) {
System.err.println("[stderr] " + line);
}
} catch (IOException e) {
e.printStackTrace();
}
});
stdoutThread.start();
stderrThread.start();
process.waitFor();
stdoutThread.join();
stderrThread.join();
まとめ:
stderr を読まないと — プロセスはハングし得ます。
stdout を読まなくても — 同様にハングし得ます。
両方のストリームを読みましょう — それが安定動作への近道です。
2. エンコーディングの問題
外部プロセスは、あなたの Java プログラムの既定とは異なる文字エンコーディングでテキストを出力することがあります。正しいエンコーディングを指定しないと、テキストが文字化けします(特にキリル文字で顕著)。
誤りの例
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
このコードはシステムの既定エンコーディングを使います。外部プロセスが例えば UTF-8 で出力し、あなたの側が Windows-1251 で読むと、キリル文字が文字化けします。
正しいやり方
必要なエンコーディングが分かっているなら明示的に指定しましょう:
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
自信がない場合は、そのプログラムのドキュメントを確認するか、いくつかの候補を試してみてください。
ヒント: Windows のコンソール系ユーティリティは CP866 や Windows-1251 を使うことが多く、Linux では UTF-8 が一般的です。
3. プラットフォーム差異
ある OS で動くコマンドが、別の OS には存在しないことがあります。例えば、ls は Linux/Mac にありますが、Windows にはありません(そこでは dir)。コマンド構文、パス区切り、引用符も異なります。
誤りの例
ProcessBuilder builder = new ProcessBuilder("ls", "-l");
builder.start(); // Windows では: "ls" が見つからない!
正しいやり方
String os = System.getProperty("os.name").toLowerCase();
ProcessBuilder builder;
if (os.contains("win")) {
builder = new ProcessBuilder("cmd", "/c", "dir");
} else {
builder = new ProcessBuilder("ls", "-l");
}
ファイルパス: パスについて困らないよう、「/」や「\」の代わりに File.separator を使いましょう。
4. 権限の問題
一部のコマンドは管理者権限を必要とします(システムファイルの削除、ネットワーク設定の変更など)。権限が不足していると、コマンドはエラーで終了するか、そもそも起動できません。
例
ProcessBuilder builder = new ProcessBuilder("rm", "-rf", "/root/secret.txt");
Process process = builder.start();
// ... Permission denied エラーが出るはず
正しいやり方
- 実行するコマンドに昇格権限が必要か確認する。
- process.exitValue() でプロセスの戻りコードを扱う。
- stderr を読む — 多くの場合そこにエラー原因があります(例: 「Permission denied」)。
5. リソースリーク
プロセスのストリーム(InputStream、OutputStream、ErrorStream)を閉じないと、ぶら下がってリソースを占有し、終了をブロックすることすらあります。同様に、プロセス自体を終了させないと、システム上の「ゾンビ」になることがあります。
誤りの例
Process process = builder.start();
// ... 処理はするが、ストリームを閉じない!
正しいやり方
ストリームには try-with-resources を使いましょう:
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
// 出力を読む
}
プロセスの処理が終わったら、適切に停止させましょう:
process.destroy(); // プロセスを終了させる(まだ生きている場合)
注意: ストリームを閉じないと、メモリリーク、ハング、システムの不調につながる可能性があります(特に大量のプロセスを起動する場合は危険)。
6. 対話処理でのデッドロック
対話的なやり取りでは相互ブロックに陥りやすいです: Java は応答待ち、外部プロセスはあなたからの入力待ち、という状態になり、結果として両者が沈黙します。あるいは入力だけ送って出力を読まないと、外部プログラム側のバッファがいつかあふれて書き込みが止まります。
これを避けるには、責務を分離します: 一方のスレッドは読み取り、もう一方は書き込みを担当させます。こうすると並行にやり取りでき、どちらも相手をブロックしません。加えて、終了後にストリームを開いたままにしないようにし、必ず閉じて不要なリソースを解放してください。
GO TO FULL VERSION