CodeGym /Các khóa học /JAVA 25 SELF /Quản lý vòng đời tiến trình

Quản lý vòng đời tiến trình

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Chờ tiến trình kết thúc: waitFor()

Khi bạn khởi chạy một tiến trình bên ngoài từ Java, nó “sống” theo cách riêng: tính toán gì đó, ghi ra console, đôi khi bị treo (xin chào, ping với cả triệu gói). Nhưng đôi khi chúng ta cần biết khi nào tiến trình này kết thúc, nó kết thúc như thế nào (thành công hay thất bại) và — nếu có gì trục trặc — có thể “giết” nó. Nói chung, giống ngoài đời: nếu bạn giao việc cho ai đó, tốt nhất nên biết họ có hoàn thành không, và có khả năng dừng lại nếu họ bỗng bắt đầu pha cà phê bất tận.

Khi bạn khởi chạy tiến trình qua ProcessBuilder, bạn nhận được một đối tượng kiểu Process. Đối tượng này giống như điều khiển từ xa: qua nó bạn có thể chờ tiến trình kết thúc, lấy mã thoát, kết thúc tiến trình, cũng như quản lý các luồng vào/ra.

Chờ tiến trình kết thúc như thế nào?

Để làm việc này có phương thức waitFor():

ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "3");
Process process = builder.start();

System.out.println("Đang chờ tiến trình kết thúc...");
int exitCode = process.waitFor(); // Chặn luồng hiện tại cho đến khi tiến trình kết thúc
System.out.println("Tiến trình kết thúc với mã: " + exitCode);

Điều gì xảy ra?

  • Chương trình khởi chạy một tiến trình bên ngoài (ping).
  • Sau đó, luồng nơi gọi waitFor() sẽ “ngủ” cho đến khi tiến trình bên ngoài kết thúc.
  • Khi tiến trình bên ngoài kết thúc, waitFor() trả về mã thoát (thông thường 0 — thành công, khác 0 — lỗi).

Làm sao biết mã thoát?

Phương thức waitFor() trả về mã này. Bạn cũng có thể lấy riêng qua exitValue():

int code = process.exitValue();

Nhưng hãy cẩn thận: nếu tiến trình vẫn chưa kết thúc, gọi exitValue() sẽ ném ngoại lệ IllegalThreadStateException. Vì vậy, thông thường ta chờ kết thúc bằng waitFor() trước, rồi mới xem mã.

2. Dừng tiến trình: destroy()destroyForcibly()

Đôi khi tiến trình bên ngoài chạy quá lâu hoặc bị treo. Ví dụ, ai đó chạy ping với 1000 gói, còn bạn thì có deadline trong 5 giây nữa. Cần một “nút đỏ”.

Kết thúc mềm: destroy()

Phương thức destroy() gửi tín hiệu kết thúc cho tiến trình (SIGTERM trong Unix, CTRL-BREAK trong Windows), và tiến trình có cơ hội kết thúc một cách chuẩn.

Process process = new ProcessBuilder("ping", "google.com").start();

Thread.sleep(2000); // Cho nó chạy 2 giây
process.destroy(); // Yêu cầu kết thúc

Kết thúc cứng: destroyForcibly()

Nếu tiến trình không phản hồi các yêu cầu lịch sự, có thể dùng phương thức destroyForcibly(). Giống như rút phích cắm khỏi ổ điện.

process.destroyForcibly();

Quan trọng: Sau khi gọi destroy() hoặc destroyForcibly(), cần chờ tiến trình kết thúc qua waitFor(). Đôi khi tiến trình cần thời gian để “nhận ra” việc bị kết thúc.

3. Chờ tiến trình với timeout

Trong Java 8+ xuất hiện phương thức có timeout:

boolean finished = process.waitFor(5, java.util.concurrent.TimeUnit.SECONDS);
if (finished) {
    System.out.println("Tiến trình đã kết thúc đúng hạn!");
} else {
    System.out.println("Tiến trình chưa kết thúc, đang hủy...");
    process.destroy();
}
  • Nếu tiến trình kết thúc trong 5 giây, phương thức trả về true.
  • Nếu không — trả về false, và bạn có thể hành động (ví dụ, kết thúc tiến trình).

Ví dụ: Chạy một lệnh lâu và thử “giết” nó:

ProcessBuilder builder = new ProcessBuilder("ping", "google.com", "-c", "10");
Process process = builder.start();

boolean finished = process.waitFor(2, java.util.concurrent.TimeUnit.SECONDS);
if (!finished) {
    System.out.println("Tiến trình chạy quá lâu, đang hủy...");
    process.destroy();
    // Có thể đợi đến khi kết thúc hoàn toàn
    process.waitFor();
}
System.out.println("Mã thoát: " + process.exitValue());

4. Thực hành: tiện ích nhỏ chạy lệnh với timeout

Hãy viết một chương trình đơn giản, chạy lệnh bên ngoài với timeout, chờ nó kết thúc, và khi vượt quá thời gian — kết thúc tiến trình.

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

public class ProcessTimeoutDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        Scanner scanner = new Scanner(System.in);

        System.out.println("Nhập lệnh để chạy (ví dụ: ping google.com):");
        String commandLine = scanner.nextLine();
        String[] command = commandLine.split(" ");

        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.start();

        System.out.println("Chờ bao nhiêu giây để kết thúc?");
        int timeout = scanner.nextInt();

        boolean finished = process.waitFor(timeout, TimeUnit.SECONDS);
        if (finished) {
            System.out.println("Tiến trình tự kết thúc. Mã thoát: " + process.exitValue());
        } else {
            System.out.println("Đã hết thời gian! Đang kết thúc tiến trình...");
            process.destroy();
            process.waitFor();
            System.out.println("Tiến trình đã bị kết thúc cưỡng bức. Mã thoát: " + process.exitValue());
        }
    }
}

Giải thích:

  • Người dùng nhập lệnh cần chạy.
  • Chương trình chờ trong thời gian đã chỉ định.
  • Nếu tiến trình không kết thúc — hủy nó.

Mẹo: Trên Windows, ping mặc định ping 4 lần, còn trên Linux — vô hạn. Với Linux/Mac, hãy thêm tham số -c 5 (ví dụ, ping google.com -c 5).

5. Lấy mã thoát: nó là gì và để làm gì?

Trong thế giới dòng lệnh, quy ước là: nếu lệnh kết thúc thành công, nó trả về mã 0. Nếu có vấn đề — là bất kỳ mã nào khác.

  • Trong các hệ thống kiểu Unix: 0 = thành công, 1 trở lên = lỗi.
  • Trong Windows — gần như tương tự.

Bạn có thể dùng mã này để ra quyết định trong chương trình Java:

int exitCode = process.waitFor();
if (exitCode == 0) {
    System.out.println("Thành công!");
} else {
    System.out.println("Lỗi! Mã: " + exitCode);
}

6. Kết thúc các tiến trình bị treo

Đôi khi tiến trình có thể bị treo — ví dụ, chờ bạn nhập gì đó, hoặc không thể kết nối mạng. Nếu bỏ mặc, nó sẽ “treo” trong hệ thống và âm thầm tiêu tốn tài nguyên.

Để tránh điều đó, nên đặt một giới hạn kiên nhẫn cho tiến trình. Phương thức waitFor(timeout, unit) cho phép chỉ chờ trong một khoảng thời gian nhất định. Nếu tiến trình không kịp — hãy mạnh dạn kết thúc nó.

Sau khi gọi destroy(), đừng chỉ “khoát tay” mà hãy chắc chắn tiến trình thực sự đã dừng. Để làm vậy, gọi lại waitFor(). Nếu vẫn không được — có biện pháp cuối cùng, destroyForcibly(). Nó chấm dứt mọi thứ không cần thêm lời.

Ví dụ hủy hai bước

boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished) {
    process.destroy();
    if (!process.waitFor(2, TimeUnit.SECONDS)) {
        process.destroyForcibly();
    }
}

Đặc thù khi làm việc với Process: cần nhớ gì

Khi tiến trình đã kết thúc, đừng vội quên nó. Hãy xem đầu ra của nó — cả đầu ra thường và đầu ra lỗi. Có thể có thứ gì đó quan trọng: thông báo lỗi, cảnh báo, hoặc đơn giản là xác nhận mọi thứ đã diễn ra suôn sẻ.

Sau đó nhất định hãy đóng tất cả các luồng liên quan — InputStream, OutputStreamErrorStream. Giống như tắt đèn khi rời phòng: tiết kiệm tài nguyên và giữ hệ thống gọn gàng.

Và nhớ: nếu bạn dừng tiến trình một cách cưỡng bức, nó có thể không kịp ghi đầu ra một cách chuẩn chỉnh. Không sao — đó là hành vi tự nhiên trong những tình huống như vậy.

7. Ví dụ: chạy một lệnh lâu và kết thúc nó

Giả sử chúng ta muốn chạy một tiến trình lâu (ví dụ, ping với số lượng gói lớn) và kết thúc nó sau 3 giây.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.concurrent.TimeUnit;

public class KillLongProcessDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        String[] command = {"ping", "google.com"};
        // Dành cho Linux/Mac: {"ping", "google.com", "-c", "100"}
        ProcessBuilder builder = new ProcessBuilder(command);
        Process process = builder.start();

        // Đọc đầu ra của tiến trình trong một luồng riêng
        Thread readerThread = new Thread(() -> {
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.out.println(line);
                }
            } catch (IOException e) {
                // Bỏ qua
            }
        });
        readerThread.start();

        // Chờ 3 giây
        if (!process.waitFor(3, TimeUnit.SECONDS)) {
            System.out.println("Tiến trình chạy quá lâu, đang hủy...");
            process.destroy();
            process.waitFor();
        }
        readerThread.join(); // Đợi đọc xong đầu ra
        System.out.println("Tiến trình đã kết thúc. Mã thoát: " + process.exitValue());
    }
}

8. Các lỗi thường gặp khi quản lý tiến trình

Lỗi số 1: Không chờ tiến trình kết thúc. Nếu bạn khởi chạy tiến trình và rồi quên mất (không gọi waitFor()), tiến trình có thể “treo” trong hệ thống ngay cả sau khi chương trình Java của bạn đã kết thúc. Các luồng vào/ra có thể không được đóng, dẫn đến rò rỉ bộ nhớ và descriptor tệp.

Lỗi số 2: Kết thúc tiến trình không đúng cách. Gọi destroy() nhưng không chờ kết thúc qua waitFor(). Kết quả là tiến trình có thể trở thành “zombie” (đặc biệt trong các hệ thống Unix).

Lỗi số 3: Đợi mã thoát trước khi tiến trình kết thúc. Gọi exitValue() trước khi tiến trình kết thúc dẫn đến ngoại lệ IllegalThreadStateException.

Lỗi số 4: Không xử lý ngoại lệ. Các phương thức làm việc với tiến trình có thể ném IOExceptionInterruptedException. Đừng quên bắt hoặc khai báo trong chữ ký phương thức.

Lỗi số 5: Không đọc đầu ra của tiến trình. Nếu không đọc stdoutstderr, tiến trình có thể bị treo do tràn bộ đệm đầu ra.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION