CodeGym /Các khóa học /JAVA 25 SELF /Xử lý lỗi trong I/O bất đồng bộ, hủy thao tác

Xử lý lỗi trong I/O bất đồng bộ, hủy thao tác

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

1. Xử lý lỗi trong các thao tác bất đồng bộ

Trong mã đồng bộ, mọi thứ khá đơn giản: nếu tệp không được tìm thấy hoặc không có quyền truy cập, bạn sẽ bắt ngoại lệ ngay trong try-catch. Trong mã bất đồng bộ, đặc biệt khi sử dụng callback (CompletionHandler), lỗi có thể xảy ra sau khi phương thức của bạn đã kết thúc — đâu đó trong pool luồng. Nếu không xử lý đúng, chương trình có thể hành xử khó lường: từ mất dữ liệu “im lặng” đến sập toàn bộ ứng dụng.

Lỗi được chuyển vào CompletionHandler như thế nào?

Trong interface CompletionHandler<V, A> có hai phương thức:

  • completed(V result, A attachment) — được gọi nếu thao tác thành công.
  • failed(Throwable exc, A attachment) — được gọi nếu xảy ra lỗi.

Ví dụ sử dụng:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncErrorDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("nonexistent.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Đọc thành công " + result + " byte");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Lỗi đọc tệp: " + exc.getMessage());
                    // Có thể ghi log, thông báo cho người dùng, hoặc ném lỗi đi tiếp
                }
            });
        } catch (IOException ex) {
            System.out.println("Lỗi mở tệp: " + ex.getMessage());
        }

        // Chờ một chút để thao tác bất đồng bộ kết thúc (trong ứng dụng thực tế hãy dùng CountDownLatch hoặc các cơ chế khác)
        Thread.sleep(500);
    }
}

Điều gì xảy ra ở đây?

  • Nếu tệp không tồn tại, phương thức failed sẽ được gọi với ngoại lệ tương ứng (NoSuchFileException).
  • Nếu thao tác hoàn tất thành công — completed sẽ được gọi.

Các lỗi thường gặp

  • Không tìm thấy tệp: NoSuchFileException
  • Không có quyền truy cập: AccessDeniedException
  • Lỗi đọc/ghi: các lớp con khác nhau của IOException
  • Sự cố bộ đệm: BufferOverflowException, BufferUnderflowException

Ghi log và thông báo cho người dùng

Lỗi trong callback bất đồng bộ — không phải lý do để hoảng loạn, nhưng cũng không nên giả vờ như không có gì xảy ra. Thực hành tốt là ghi log lỗi (ví dụ, qua Logger), và nếu điều đó quan trọng với người dùng — hiển thị thông báo hoặc gọi handler trong UI.

Ví dụ ghi log:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("Lỗi thao tác bất đồng bộ: " + exc);
    exc.printStackTrace();
}

Trong mã production hãy dùng các logger chuẩn (ví dụ, java.util.logging hoặc Log4j), thay vì System.err.

2. Hủy các thao tác bất đồng bộ

Khi nào cần hủy thao tác?

Đôi khi cần dừng một tác vụ bất đồng bộ ngay giữa chừng. Ví dụ, người dùng đổi ý và nhấn “Hủy” trong khi đang tải tệp. Hoặc cửa sổ chương trình đã đóng, và thao tác không còn ý nghĩa. Cũng có khi khi kết thúc ứng dụng chỉ cần giải phóng tài nguyên một cách gọn gàng.

Trong những trường hợp như vậy, I/O bất đồng bộ trong Java hỗ trợ hủy thông qua interface Future. Nhờ đó có thể dừng tác vụ đang chạy bất cứ lúc nào để không lãng phí tài nguyên.

Hủy thao tác bằng Future như thế nào?

Phương thức read hoặc write trong AsynchronousFileChannel trả về đối tượng Future<Integer>. Đối tượng này có phương thức cancel(boolean mayInterruptIfRunning).

Ví dụ: hủy việc đọc bất đồng bộ

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Chờ một chút rồi hủy thao tác
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Thao tác đọc đã bị hủy!");
            } else {
                System.out.println("Không thể hủy thao tác (có thể nó đã hoàn tất)");
            }
        }
    }
}

Những điểm quan trọng:

  • Chỉ có thể hủy các thao tác chưa hoàn tất.
  • Nếu thao tác đã xong — bạn sẽ không thể hủy.
  • Sau khi hủy, khi cố gọi get() trên Future này, sẽ ném ngoại lệ CancellationException.

Khi nào không thể hủy thao tác nữa?

Nếu tác vụ đã kịp hoàn tất — dù thành công hay lỗi — không thể dừng nó nữa, coi như chuyến tàu đã rời ga.

Ngoài ra, không phải mọi triển khai đều thật sự có thể ngắt thao tác ở cấp hệ điều hành. Ví dụ, khi làm việc với một số hệ thống tệp, “hủy” sẽ chỉ mang tính tượng trưng: thao tác vẫn tiếp tục chạy, còn bạn chỉ đơn giản bỏ qua kết quả.

3. Thực hành: xử lý lỗi và hủy

Ví dụ 1: xử lý lỗi khi đọc tệp không tồn tại

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;

public class AsyncErrorExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("no_such_file.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Thao tác hoàn tất thành công");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Lỗi khi đọc tệp: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("Lỗi khi mở tệp: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

Sẽ thấy gì trên console?

Lỗi khi mở tệp: no_such_file.txt

hoặc, nếu lỗi xảy ra khi đọc chứ không phải khi mở:

Lỗi khi đọc tệp: NoSuchFileException - no_such_file.txt

Ví dụ 2: hủy thao tác lâu và kết thúc đúng cách

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 MB

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Sau 50 ms thì hủy thao tác (để thử nghiệm)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Thao tác đọc đã được hủy!");
            } else {
                System.out.println("Không thể hủy thao tác (có lẽ nó đã hoàn tất)");
            }

            try {
                // Thử lấy kết quả (sẽ ném CancellationException)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("Bắt được CancellationException: thao tác thực sự đã bị hủy.");
            }
        }
    }
}

4. Best practices: cách làm cho đúng

Giải phóng tài nguyên ngay cả khi có lỗi

Hãy dùng try-with-resources để tự động đóng kênh:

try (AsynchronousFileChannel channel = /* open channel */ null) {
    // ...
}

Nếu dùng CompletionHandler, đừng quên đóng kênh khi tất cả thao tác đã kết thúc. Điều này đặc biệt quan trọng nếu thực hiện nhiều thao tác bất đồng bộ liên tiếp.

Đừng chặn UI/luồng chính

Các thao tác bất đồng bộ nhằm tránh chặn luồng chính. Đừng gọi future.get() trong luồng UI — nếu không, ý nghĩa của bất đồng bộ sẽ mất đi.

Ghi log mọi lỗi

Trong CompletionHandler luôn triển khai phương thức failed và ghi log (hoặc chuyển tiếp) mọi ngoại lệ.

Đảm bảo tất cả thao tác đã xong trước khi kết thúc chương trình

Nếu chương trình kết thúc trước khi thao tác xong, kết quả có thể bị mất. Khi demo trên console đôi khi phải dùng Thread.sleep(500), nhưng trong ứng dụng thực tế hãy dùng CountDownLatch, CompletableFuture hoặc các cơ chế đồng bộ khác.

Đừng quên việc hủy

Nếu thao tác không còn cần thiết (ví dụ, người dùng đóng cửa sổ), hãy hủy nó qua Future.cancel. Điều này tiết kiệm tài nguyên và tăng độ phản hồi của ứng dụng.

5. Lỗi thường gặp khi xử lý lỗi và hủy trong I/O bất đồng bộ

Lỗi №1: Bỏ qua phương thức failed trong CompletionHandler.
Nếu không triển khai xử lý lỗi, ứng dụng của bạn sẽ hành xử khó lường: lỗi bị “mất hút”, còn người dùng không hiểu vì sao không có gì xảy ra.

Lỗi №2: Không đóng kênh sau khi hoàn tất thao tác.
Quên đóng AsynchronousFileChannel — bạn sẽ gặp rò rỉ tài nguyên và có thể bị khóa tệp ở hệ điều hành.

Lỗi №3: Chờ kết quả của thao tác bất đồng bộ trong luồng chính.
Gọi future.get() trong luồng UI — giao diện “đóng băng”, và toàn bộ sự bất đồng bộ coi như mất tác dụng.

Lỗi №4: Cố hủy một thao tác đã hoàn tất.
Gọi cancel() quá muộn — thao tác đã hoàn tất, việc hủy sẽ không hiệu lực. Điều này không nghiêm trọng, nhưng có thể gây bối rối khi gỡ lỗi.

Lỗi №5: Không kiểm tra kết quả của việc hủy.
Gọi cancel(), nhưng không kiểm tra giá trị trả về và không xử lý CancellationException khi gọi get() — chương trình có thể sập hoặc hành xử kỳ lạ.

Lỗi №6: Không giải phóng tài nguyên khi có lỗi hoặc khi hủy.
Nếu không đóng kênh sau lỗi hoặc hủy, có thể xảy ra rò rỉ hoặc khóa tệp.

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