1. Deadlock khi đọc/ghi luồng
Một trong những lỗi khó chịu nhất là khi tiến trình bị treo và không kết thúc, dù có vẻ như mọi thứ đã làm đúng. Nguyên nhân thường gặp là tràn bộ đệm của luồng đầu ra hoặc lỗi của tiến trình bên ngoài. Nếu không đọc cả hai luồng (stdout và stderr), tiến trình có thể “bị chặn” khi cố gắng ghi gì đó vì không ai lấy dữ liệu ra khỏi bộ đệm của nó.
Ví dụ về sự cố
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// Chỉ đọc stdout, còn stderr thì bỏ qua!
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
process.waitFor();
Nếu lệnh ghi gì đó vào stderr (ví dụ, java -version hầu như luôn ghi vào đó!), luồng này sẽ bị tràn bộ đệm và tiến trình bị treo.
Cách làm đúng
Hãy đọc cả hai luồng song song (bằng các thread riêng hoặc ExecutorService):
ProcessBuilder builder = new ProcessBuilder("java", "-version");
Process process = builder.start();
// Đọc 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();
}
});
// Đọc 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();
Kết luận:
Nếu không đọc stderr — tiến trình có thể treo.
Nếu không đọc stdout — cũng có thể treo.
Hãy đọc cả hai luồng — và mọi thứ sẽ suôn sẻ!
2. Vấn đề về mã hóa
Các tiến trình bên ngoài có thể dùng mã hóa khác với chương trình Java mặc định của bạn. Nếu không chỉ định đúng mã hóa, bạn sẽ nhận “ký tự rác” thay vì văn bản (đặc biệt dễ thấy với chữ Cyrillic).
Ví dụ lỗi
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
Đoạn mã này dùng mã hóa hệ thống mặc định. Nhưng nếu tiến trình bên ngoài ghi, chẳng hạn, bằng UTF-8, còn của bạn là Windows-1251, chữ Cyrillic sẽ biến thành ký tự lộn xộn.
Cách làm đúng
Hãy chỉ định rõ charset cần dùng nếu bạn biết nó:
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)
);
Nếu không chắc — hãy xem tài liệu của chương trình hoặc thử vài phương án.
Mẹo: trên Windows, các tiện ích dòng lệnh thường dùng CP866 hoặc Windows-1251, còn trên Linux — UTF-8.
3. Khác biệt giữa các nền tảng
Những lệnh chạy được trên một hệ điều hành có thể không tồn tại trên hệ khác. Ví dụ, ls có trên Linux/Mac, nhưng không có trên Windows (ở đó là dir). Cú pháp lệnh, dấu phân tách đường dẫn và dấu ngoặc kép cũng khác nhau.
Ví dụ lỗi
ProcessBuilder builder = new ProcessBuilder("ls", "-l");
builder.start(); // Trên Windows: không tìm thấy "ls"!
Cách làm đúng
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");
}
Đường dẫn tệp: hãy dùng File.separator thay vì “/” hoặc “\” để tránh rắc rối với đường dẫn.
4. Vấn đề về quyền truy cập
Một số lệnh yêu cầu quyền quản trị (xóa tệp hệ thống, thay đổi cấu hình mạng, v.v.). Nếu thiếu quyền, lệnh sẽ kết thúc với lỗi hoặc thậm chí không khởi chạy được.
Ví dụ
ProcessBuilder builder = new ProcessBuilder("rm", "-rf", "/root/secret.txt");
Process process = builder.start();
// ... sẽ nhận lỗi Permission denied
Cách làm đúng
- Kiểm tra xem lệnh của bạn có cần quyền nâng cao hay không.
- Xử lý mã thoát của tiến trình qua process.exitValue().
- Đọc stderr — ở đó thường có nguyên nhân lỗi (ví dụ: “Permission denied”).
5. Rò rỉ tài nguyên
Nếu không đóng các luồng của tiến trình (InputStream, OutputStream, ErrorStream), chúng có thể “treo”, chiếm tài nguyên và thậm chí chặn việc kết thúc. Tương tự, nếu không kết thúc chính tiến trình, nó có thể trở thành “zombie” trong hệ thống.
Ví dụ lỗi
Process process = builder.start();
// ... chạy nhưng không đóng các luồng!
Cách làm đúng
Hãy dùng try-with-resources cho các luồng:
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
// Đọc output
}
Sau khi xong việc với tiến trình, hãy dừng nó một cách đúng đắn:
process.destroy(); // Kết thúc tiến trình (nếu nó vẫn còn chạy)
Lưu ý: nếu không đóng các luồng — có thể xảy ra rò rỉ bộ nhớ, treo và vấn đề hệ thống (đặc biệt khi chạy nhiều tiến trình).
6. Deadlock khi tương tác
Khi trao đổi tương tác, rất dễ rơi vào tình huống khóa lẫn nhau: Java chờ phản hồi, còn tiến trình bên ngoài — chờ dữ liệu của bạn. Kết quả là cả hai đều “im lặng”. Hoặc bạn đã gửi dữ liệu nhưng không đọc phản hồi — đến lúc nào đó bộ đệm của chương trình bên ngoài bị đầy và nó dừng ghi.
Để tránh điều này, hãy tách nhiệm vụ: một thread phụ trách đọc, thread kia — ghi. Khi đó tương tác diễn ra song song và không ai chặn ai. Đồng thời đừng để mở các luồng sau khi kết thúc — hãy đóng chúng để hệ thống không giữ tài nguyên thừa.
GO TO FULL VERSION