Giao tiếp Giữa Các Luồng
(Inter-thread Communication)

Giao tiếp giữa các luồng (inter-thread communication) là một khái niệm quan trọng trong Java, cho phép các luồng hợp tác và phối hợp thực hiện các nhiệm vụ. Quá trình này liên quan đến việc sử dụng các phương thức cụ thể như wait(), notify(), và notifyAll() để quản lý việc truy cập vào tài nguyên dùng chung, nhằm tránh xung đột và đảm bảo thực thi chính xác.

Quá trình giao tiếp giữa các luồng

Trong lập trình đa luồng, các luồng cần cơ chế để phối hợp và chia sẻ tài nguyên một cách an toàn. Java cung cấp các phương thức như wait(), notify(), và notifyAll() để quản lý quá trình này. Cơ chế hoạt động dựa trên việc chiếm khóa, chờ điều kiện, nhận tín hiệu và tiếp tục thực thi. Dưới đây là các bước chính trong quá trình giao tiếp giữa các luồng:

1. Các luồng cố gắng chiếm khóa

↳ Khi một luồng cần truy cập vào tài nguyên dùng chung, nó phải chiếm được khóa (còn gọi là monitor) liên quan đến đối tượng đó.

2. Khóa được một luồng chiếm được

↳ Chỉ có một luồng có thể giữ khóa tại một thời điểm. Khi một luồng chiếm được khóa, nó có thể tiếp tục thực hiện các thao tác của mình.

3. Luồng chuyển sang trạng thái chờ

↳ Nếu luồng cần đợi một điều kiện nào đó trước khi tiếp tục, nó sẽ gọi phương thức wait() trên đối tượng đó. Điều này khiến luồng giải phóng khóa và chuyển sang trạng thái chờ.

4. Phương thức notify() hoặc notifyAll() được gọi

↳ Một luồng khác, sau khi hoàn thành nhiệm vụ của nó, có thể gọi phương thức notify() hoặc notifyAll() trên đối tượng. Điều này báo hiệu cho các luồng đang chờ rằng chúng có thể thức dậy và cố gắng chiếm lại khóa.

5. Luồng chuyển sang trạng thái được thông báo (Runnable)

↳ Khi một luồng được thông báo, nó chuyển từ trạng thái chờ sang trạng thái sẵn sàng thực thi (runnable), nghĩa là nó sẵn sàng chạy khi chiếm được lại khóa.

6. Luồng chiếm khóa và thực thi

↳ Sau khi được thông báo, luồng cố gắng chiếm lại khóa. Khi chiếm được khóa, nó tiếp tục thực thi từ vị trí đã dừng lại.

7. Luồng giải phóng khóa và thoát

↳ Sau khi hoàn thành nhiệm vụ, luồng giải phóng khóa, làm cho khóa đó sẵn có cho các luồng khác, và thoát khỏi trạng thái monitor của đối tượng.

Tại sao các phương thức wait(), notify(), và notifyAll() được định nghĩa trcng lớp Object, không phải lớp Thread?

Các phương thức wait(), notify(), và notifyAll() liên quan đến cơ chế khóa, và vì mọi đối tượng trong Java đều có một khóa nội tại, nên các phương thức này được gắn với lớp Object, không phải lớp Thread. Khóa được gắn với chính đối tượng đó, vì vậy các phương thức này được định nghĩa trong lớp Object, cho phép bất kỳ luồng nào giữ khóa của một đối tượng có thể gọi các phương thức này.

Sự khác biệt giữa các phương thức wait() và sleep() trong Java:

Tính năngsleep()wait()
Mục đíchTạm dừng luồng trong một khoảng thời gian xác địnhCho phép luồng chờ đợi một sự kiện cụ thể
Phương thứcTĩnh của lớp ThreadThuộc lớp Object
Trạng thái luồngTIMED_WAITINGWAITING
KhóaKhông liên quanLiên quan đến khóa của đối tượng
Cách đánh thứcTự động sau khi hết thời gianBởi notify() hoặc notifyAll()
Sử dụngTạo độ trễ, tạm dừng thực thiĐồng bộ hóa các luồng, chờ đợi điều kiện
Vị trí gọiBất kỳ đâu trong codeBên trong một khối synchronized
Xử lý ngoại lệNém InterruptedException nếu bị gián đoạnNém InterruptedException nếu bị gián đoạn

Phương thức giao tiếp luồng trong Java

Trong Java, các luồng có thể giao tiếp với nhau bằng cách sử dụng ba phương thức của lớp Object: wait(), notify(), và notifyAll(). Các phương thức này giúp đồng bộ hóa luồng, cho phép một luồng chờ và luồng khác đánh thức khi điều kiện thích hợp được đáp ứng.

1. Phương thức wait()

↳ Chức năng: Đặt luồng hiện tại vào trạng thái chờ cho đến khi một luồng khác gọi phương thức notify() hoặc notifyAll() trên đối tượng mà luồng đang chờ.

↳ Sử dụng: Phương thức wait() cần phải được gọi từ bên trong một khối đồng bộ hóa (synchronized block). Khi một luồng gọi phương thức wait(), nó giải phóng khóa của đối tượng và chờ cho đến khi được thông báo.

Cú pháp

public class Thread
  extends Object
  implements Runnable

2. Phương thức notify()

↳ Chức năng: Thông báo cho một luồng đang chờ trên đối tượng mà nó có thể tiếp tục thực hiện. Chỉ một luồng sẽ được thông báo và tiếp tục hoạt động.

↳ Sử dụng: Phương thức notify() cũng phải được gọi từ bên trong một khối đồng bộ hóa (synchronized block). Phương thức này đánh thức một trong các luồng đang chờ, cho phép nó tiếp tục hoạt động.

Cú pháp

synchronized (object) {
    // thay đổi điều kiện
    object.notify();
}

3. Phương thức notifyAll()

↳ Chức năng: Thông báo cho tất cả các luồng đang chờ trên đối tượng để tiếp tục thực hiện. Tất cả các luồng đang chờ sẽ được đánh thức và kiểm tra lại điều kiện của chúng.

↳ Sử dụng: Giống như phương thức notify(), phương thức notifyAll() cần được gọi từ bên trong một khối đồng bộ hóa (synchronized block). Sự khác biệt là phương thức notifyAll() sẽ đánh thức tất cả các luồng đang chờ thay vì chỉ một.

Cú pháp

synchronized (object) {
    // thay đổi điều kiện
    object.notifyAll();
}

Dưới đây là ví dụ về cách sử dụng các phương thức wait(), notify(), và notifyAll() trong Java để thực hiện giao tiếp giữa các luồng (inter-thread communication).

Ví dụ 1

Tình huống: Chúng ta có hai luồng. Một luồng sẽ sản xuất dữ liệu và một luồng khác sẽ tiêu thụ dữ liệu. Chúng ta sẽ sử dụng phương thức wait() và notify() để đảm bảo rằng dữ liệu chỉ được tiêu thụ khi nó đã sẵn sàng và ngược lại.

Ví dụ: Example.java

class DataBuffer {
    private int data;
    private boolean hasData = false;

    // Sản xuất dữ liệu
    public synchronized void produce(int newData) throws InterruptedException {
        while (hasData) {
            wait(); // Chờ khi đã có dữ liệu
        }
        data = newData;
        hasData = true;
        System.out.println("Sản xuất dữ liệu: " + data);
        notify(); // Thông báo rằng dữ liệu đã sẵn sàng
    }

    // Tiêu thụ dữ liệu
    public synchronized int consume() throws InterruptedException {
        while (!hasData) {
            wait(); // Chờ khi chưa có dữ liệu
        }
        hasData = false;
        System.out.println("Tiêu thụ dữ liệu: " + data);
        notify(); // Thông báo rằng dữ liệu đã được tiêu thụ
        return data;
    }
}

public class Example {
    public static void main(String[] args) {
        DataBuffer buffer = new DataBuffer();

        // Luồng sản xuất dữ liệu
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.produce(i);
                    Thread.sleep(1000); // Giả lập thời gian sản xuất
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // Luồng tiêu thụ dữ liệu
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.consume();
                    Thread.sleep(1500); // Giả lập thời gian tiêu thụ
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}

Kết quả của chương trình là

Sản xuất dữ liệu: 0
Tiêu thụ dữ liệu: 0
...
Sản xuất dữ liệu: 4
Tiêu thụ dữ liệu: 4

Trong ví dụ trên, Khi gọi phương thức wait() luồng hiện tại sẽ chờ và giải phóng khóa của đối tượng để các luồng khác có thể tiếp tục. Phương thức notify() được gọi sau khi sản xuất hoặc tiêu thụ dữ liệu để thông báo cho luồng khác rằng điều kiện đã thay đổi. Phương thức notify() sẽ làm một luồng chờ trên đối tượng tiếp tục thực thi.

Ví dụ 2

Nếu bạn có nhiều luồng chờ và bạn muốn thông báo cho tất cả các luồng chờ rằng điều kiện đã thay đổi, bạn có thể sử dụng notifyAll().

Ví dụ: Example.java

class DataBuffer {
    private int data;
    private boolean hasData = false;

    // Sản xuất dữ liệu
    public synchronized void produce(int newData) throws InterruptedException {
        while (hasData) {
            wait(); // Chờ khi đã có dữ liệu
        }
        data = newData;
        hasData = true;
        System.out.println("Sản xuất dữ liệu: " + data);
        notifyAll(); // Thông báo cho tất cả các luồng chờ
    }

    // Tiêu thụ dữ liệu
    public synchronized int consume() throws InterruptedException {
        while (!hasData) {
            wait(); // Chờ khi chưa có dữ liệu
        }
        hasData = false;
        System.out.println("Tiêu thụ dữ liệu: " + data);
        notifyAll(); // Thông báo cho tất cả các luồng chờ
        return data;
    }
}

public class Example {
    public static void main(String[] args) {
        DataBuffer buffer = new DataBuffer();

        // Luồng sản xuất dữ liệu
        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.produce(i);
                    Thread.sleep(1000); // Giả lập thời gian sản xuất
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // Luồng tiêu thụ dữ liệu
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    buffer.consume();
                    Thread.sleep(1500); // Giả lập thời gian tiêu thụ
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}

Kết quả của chương trình là

Sản xuất dữ liệu: 0
Tiêu thụ dữ liệu: 0
...
Sản xuất dữ liệu: 4
Tiêu thụ dữ liệu: 4

Trong ví dụ trên, phương thức notifyAll() thông báo cho tất cả các luồng đang chờ trên đối tượng rằng điều kiện đã thay đổi, không chỉ một luồng như notify().

Tóm tắt:

↳ Phương thức notify(): Thông báo cho một luồng ngẫu nhiên trong số các luồng đang chờ. Thích hợp khi chỉ có một luồng cần được thông báo hoặc khi thông báo cho một luồng là đủ.

↳ Phương thức notifyAll(): Thông báo cho tất cả các luồng đang chờ. Thích hợp khi có nhiều luồng chờ và bạn muốn tất cả các luồng đều được thông báo để kiểm tra điều kiện.

Việc chọn giữa phương thức notify() và notifyAll() phụ thuộc vào số lượng luồng chờ và cách bạn muốn quản lý thông báo cho các luồng đó. Sử dụng phương thức notifyAll() có thể tốn kém hơn về hiệu suất, đặc biệt khi có nhiều luồng chờ, vì tất cả các luồng đều được đánh thức và cần kiểm tra điều kiện của chúng.

Câu Nói Truyền Cảm Hứng

“Bắt đầu ở đâu không quan trọng, quan trọng là bạn sẵn sàng bắt đầu.” – W. Clement Stone

Không Gian Tích Cực

“Chúc bạn luôn giữ vững niềm tin và sức mạnh để vượt qua mọi thử thách trong cuộc sống.”