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ăng | sleep() | wait() |
|---|---|---|
| Mục đích | Tạm dừng luồng trong một khoảng thời gian xác định | Cho phép luồng chờ đợi một sự kiện cụ thể |
| Phương thức | Tĩnh của lớp Thread | Thuộc lớp Object |
| Trạng thái luồng | TIMED_WAITING | WAITING |
| Khóa | Không liên quan | Liên quan đến khóa của đối tượng |
| Cách đánh thức | Tự động sau khi hết thời gian | Bởi notify() hoặc notifyAll() |
| Sử dụng | Tạ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ọi | Bất kỳ đâu trong code | Bên trong một khối synchronized |
| Xử lý ngoại lệ | Ném InterruptedException nếu bị gián đoạn | Né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à
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à
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.