Bài viết bằng tiếng Việt

# Java - Giao tiếp giữa các luồng

Chào mừng, các nhà lập trình Java mới nhất! Hôm nay, chúng ta sẽ bắt đầu hành trình thú vị vào thế giới giao tiếp giữa các luồng trong Java. Tôi, như một giáo viên khoa học máy tính bạn thân thiện, sẽ hướng dẫn bạn qua chủ đề thú vị này. Vậy hãy lấy ly cà phê yêu thích của bạn, thoải mái ngồi, và hãy bắt đầu!

Java - Inter-thread Communication

## Giao tiếp giữa các luồng là gì?

Hãy tưởng tượng bạn đang tham gia một cuộc đua chuyển chiến. Bạn phải truyền bóng cho đồng đội của mình vào thời điểm chính xác. Đó chính là điều gì giao tiếp giữa các luồng trong thế giới lập trình. Nó là cách mà các luồng khác nhau trong một chương trình Java giao tiếp với nhau, tập hợp hành động và chia sẻ thông tin.

## Tại sao giao tiếp giữa các luồng quan trọng?

Giao tiếp giữa các luồng rất quan trọng để tạo ra các chương trình hiệu quả, đồng bộ hóa. Nếu không có nó, các luồng của chúng ta sẽ như những người chạy đua trong các cuộc đua riêng biệt, không thể hợp tác hoặc chia sẻ tài nguyên hiệu quả.

## Các phương thức được sử dụng cho giao tiếp giữa các luồng

Java cung cấp một số phương thức cho giao tiếp giữa các luồng. Hãy xem chúng trong bảng dưới đây:

Phương thức Mô tả
wait() Gây ra luồng hiện tại chờ đợi cho đến khi một luồng khác gọi phương thức notify() hoặc notifyAll() cho đối tượng này
notify() Thức dậy một luồng đang chờ đợi trên bộ giám sát của đối tượng này
notifyAll() Thức dậy tất cả các luồng đang chờ đợi trên bộ giám sát của đối tượng này

Bây giờ, hãy phân tích chúng và xem cách chúng hoạt động trong thực tế!

### Phương thức wait()

Phương thức wait() như là việc nói với một luồng, "Này, nghỉ ngơi đến khi ai đó chạm vào bạn." Dưới đây là cách nó hoạt động:

synchronized(object) {
    while(condition) {
        object.wait();
    }
}

Trong đoạn mã này:

  1. Chúng ta đầu tiên đồng bộ hóa trên một đối tượng để đảm bảo an toàn luồng.
  2. Chúng ta kiểm tra điều kiện trong một vòng lặp while.
  3. Nếu điều kiện là đúng, chúng ta gọi wait(), khiến luồng tạm dừng và chờ thông báo.

### Phương thức notify()

Phương thức notify() như là việc chạm vào một luồng đang chờ đợi và nói, "Thức dậy! Đến lượt bạn rồi." Dưới đây là cách sử dụng nó:

synchronized(object) {
    // Thay đổi điều kiện
    condition = true;
    object.notify();
}

Trong đoạn mã này:

  1. Chúng ta đồng bộ hóa trên cùng một đối tượng như trong cuộc gọi wait().
  2. Chúng ta thay đổi điều kiện mà luồng đang chờ đợi đang kiểm tra.
  3. Chúng ta gọi notify() để thức dậy một luồng đang chờ đợi.

### Phương thức notifyAll()

Phương thức notifyAll() như là việc kêu, "Mọi người thức dậy!" Nó được sử dụng khi bạn muốn cảnh báo tất cả các luồng đang chờ đợi. Dưới đây là một ví dụ:

synchronized(object) {
    // Thay đổi điều kiện
    condition = true;
    object.notifyAll();
}

Phương thức này hoạt động tương tự như notify(), nhưng thức dậy tất cả các luồng đang chờ đợi thay vì chỉ một.

## Ví dụ thực tế: Bài toán Nhà sản xuất - Khách hàng

Hãy kết hợp tất cả những gì chúng ta đã học với một ví dụ kinh điển: Bài toán Nhà sản xuất - Khách hàng. Hãy tưởng tượng một bánh mì nơi một người (nhà sản xuất) sản xuất bánh mì, và một người khác (khách hàng) bán nó. Họ chia sẻ không gian giáp hạn chế.

Dưới đây là cách chúng ta có thể triển khai điều này trong Java:

class Bakery {
    private int breads = 0;
    private final int CAPACITY = 5;

    public synchronized void produceBread() throws InterruptedException {
        while (breads == CAPACITY) {
            System.out.println("Giáp đầy! Nhà sản xuất đang chờ...");
            wait();
        }
        breads++;
        System.out.println("Nhà sản xuất đã làm nên bánh mì. Tổng: " + breads);
        notify();
    }

    public synchronized void sellBread() throws InterruptedException {
        while (breads == 0) {
            System.out.println("Không có bánh mì! Khách hàng đang chờ...");
            wait();
        }
        breads--;
        System.out.println("Đã bán bánh mì. Còn lại: " + breads);
        notify();
    }
}

class Baker implements Runnable {
    private Bakery bakery;

    public Baker(Bakery bakery) {
        this.bakery = bakery;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                bakery.produceBread();
                Thread.sleep(1000); // Thời gian để nướng
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Seller implements Runnable {
    private Bakery bakery;

    public Seller(Bakery bakery) {
        this.bakery = bakery;
    }

    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                bakery.sellBread();
                Thread.sleep(1500); // Thời gian giữa các lần bán
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class BakeryDemo {
    public static void main(String[] args) {
        Bakery bakery = new Bakery();
        Thread baker = new Thread(new Baker(bakery));
        Thread seller = new Thread(new Seller(bakery));

        baker.start();
        seller.start();
    }
}

Hãy phân tích điều này:

  1. Chúng ta có một lớp Bakery quản lý số lượng bánh mì.
  2. Phương thức produceBread() đại diện cho nhà sản xuất sản xuất bánh mì. Nếu giáp đầy, nhà sản xuất sẽ chờ.
  3. Phương thức sellBread() đại diện cho khách hàng bán bánh mì. Nếu không có bánh mì, khách hàng sẽ chờ.
  4. Chúng ta sử dụng wait() khi điều kiện không phù hợp để sản xuất hoặc bán.
  5. Chúng ta sử dụng notify() sau khi sản xuất hoặc bán để cảnh báo luồng khác.
  6. Các lớp BakerSeller chạy trong các luồng riêng biệt, liên tục cố gắng sản xuất hoặc bán bánh mì.

Khi bạn chạy chương trình này, bạn sẽ thấy nhà sản xuất và khách hàng làm việc cùng nhau, chờ khi cần thiết, và cảnh báo nhau khi có thể tiếp tục. Như thể bạn đang xem một vũ đoạn điều kiện tốt!

## Kết luận

Và thế là, các bạn! Chúng ta đã đi qua thế giới giao tiếp giữa các luồng trong Java. Chúng ta đã thấy cách các luồng có thể tập hợp hành động bằng cách sử dụng wait(), notify(), và notifyAll(). Chúng ta thậm chí đã xây dựng một nhà sản xuất ảo để thấy các khái niệm này trong hành động!

Hãy nhớ, như trong ví dụ nhà sản xuất của chúng ta, giao tiếp giữa các luồng tốt đến từ sự cân bằng và điều phối. Nó liên quan đến việc biết khi nào làm việc, khi nào chờ, và khi nào cảnh báo người khác. Thành thạo điều này, và bạn sẽ đang trên đường tới việc tạo ra các chương trình Java hiệu quả và điều phối tốt.

Hãy tiếp tục tập luyện, giữ được sự tò mò, và hạnh phúc lập trình! Và nhớ, trong lập trình như trong cuộc sống, giao tiếp tốt là chìa khóa để đạt được thành công. Đến lần sau, đây là giáo viên khoa học máy tính bạn thân thiện, kính bài!

Credits: Image by storyset