Lập trình đa luồng và Gọi lại không đồng bộ trong Java

Trong quá trình phát triển phần mềm, lập trình viên ít khi phải tự tay triển khai các tác vụ đa luồng, tuy nhiên các thành phần và framework mà chúng ta sử dụng lại thường xuyên tận dụng công nghệ này. Do đó, việc nắm vững kiến thức về đa luồng trong Java là rất cần thiết, đặc biệt là trong các buổi phỏng vấn.

Các giao diện và lớp cốt lõi trong Java để xử lý đa luồng bao gồm: Thread, Runnable, Callable, Future, FutureTask, và Executors.

  • thread.run(): Chứa logic chính mà luồng sẽ thực thi.
  • thread.join(): Chờ cho đến khi luồng kết thúc.

Khi nhìn lại các chương trình đã viết, có thể thấy rằng đa luồng liên quan đến các hoạt động ở tầng thấp như đọc/ghi bộ nhớ và CPU. Ngôn ngữ Java đã cố gắng đóng gói và trừu tượng hóa những phức tạp này để giúp lập trình viên dễ dàng hơn. Thách thức chính của đa luồng là đồng bộ hóa giữa các luồng và đảm bảo an toàn khi thao tác với các biến chia sẻ. Java cung cấp các từ khóa synchronizedvolatile để giải quyết vấn đề này, cùng với framework Executor.

  • synchronized: Đặt một khóa (lock) trên một đối tượng hoặc một khối mã, đảm bảo rằng chỉ một luồng có thể truy cập vào nó tại một thời điểm. Cơ chế này được quản lý bởi JVM.
  • volatile: Đảm bảo tính khả kiến (visibility) của biến giữa các luồng. Nó làm cho bộ nhớ và cache của CPU luôn đồng bộ.

Đối với các hoạt động đồng thời, Java cung cấp một loạt các lớp và giao diện mạnh mẽ trong gói java.util.concurrent.

Ví dụ, ScheduledExecutorService là một dịch vụ thực thi các tác vụ theo lịch trình. Nó rất hữu ích cho các hệ thống phân tán cần gửi tín hiệu "keep-alive" định kỳ. Cách sử dụng cơ bản có thể được tóm tắt như sau:

// Tạo một dịch vụ lên lịch
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// Tác vụ cần thực thi
Runnable task = () -> System.out.println("Gửi tín hiệu keep-alive...");

// Lên lịch thực thi tác vụ sau 5 giây, sau đó lặp lại mỗi 60 giây
scheduler.scheduleAtFixedRate(task, 5, 60, TimeUnit.SECONDS);

Các pool kết nối cơ sở dữ liệu như c3p0 hay DBCP cũng là những ứng dụng thực tế của đa luồng. Chúng giúp quản lý và tái sử dụng các kết nối một cách hiệu quả.

// Tạo một nguồn dữ liệu kết nối từ tệp cấu hình mặc định
ComboPooledDataSource dataSource = new ComboPooledDataSource();

// Hoặc, tải cấu hình cụ thể từ tệp c3p0-config.xml
// ComboPooledDataSource dataSource = new ComboPooledDataSource("tenCauHinh");

Trong lập trình hiện đại, các framework như Netty sử dụng rộng rãi các cơ chế gọi lại không đồng bộ (asynchronous callbacks) thông qua ChannelFuture. Việc tự triển khai cơ chế này có thể khá phức tạp, nhưng Java 8 đã giới thiệu lớp CompletableFuture để đơn giản hóa quá trình này.

Ví dụ về cách sử dụng CompletableFuture:

// Tạo các CompletableFuture cho các tác vụ riêng lẻ
CompletableFuture<Result> task1 = CompletableFuture.supplyAsync(() -> processTaskA());
CompletableFuture<Result> task2 = CompletableFuture.supplyAsync(() -> processTaskB());

// Chờ cho tất cả các tác vụ hoàn thành
CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2);

// Sau khi tất cả hoàn thành, có thể lấy kết quả
allTasks.thenRun(() -> {
    Result resultA = task1.join();
    Result resultB = task2.join();
    // Xử lý kết quả...
});

Một mẫu thiết kế phổ biến trong lập trình đa luồng Java là kết hợp Executor với CountDownLatch. Executor chịu trách nhiệm thực thi các tác vụ, trong khi CountDownLatch được sử dụng để đợi cho đến khi tất cả các tác vụ hoàn thành.

Thẻ: Java Đa luồng completablefuture ExecutorService Gọi lại không đồng bộ

Đăng vào ngày 22 tháng 6 lúc 07:59