Java: Khối bảo vệ
Các luồng thường phải phối hợp hành động cùng nhau. Cách thức phối hợp phổ biến nhất là khối bảo vệ. Một khối như vậy bắt đầu bằng cách kiểm tra một điều kiện là phải đúng trước khi khối có thể tiến hành. Có một số bước để thực hiện điều này một cách chính xác.
Giả sử, guardedJoy() là một phương thức mà không phải được xử lý cho đến khi một biến chia sẻ có tên joy được thiết lập bởi một luồng khác. Như một phương thức có thể, theo lý thuyết, ta chi cần lặp cho đến khi điều kiện được thoả mãn, nhưng việc dùng vòng lặp là lãng phí, vì nó thực hiện liên tục trong khi chờ đợi.
public void guardedJoy() {
// Simple loop guard. Wastes
// processor time. Don't do this!
while(!joy) {}
System.out.println("Joy has been achieved!");
}
Việc dùng khối bảo vệ sẽ quả hơn bằng cách gọi phương thức Object.wait()
để đình chỉ luồng hiện hành. Phương thức wait() sẽ không được thực hiện cho đến khi luồng khác đưa ra một thông báo rằng một số sự kiện đặc biệt có thể xảy ra - mặc dù không nhất thiết phải là sự kiện của luồng này:
public synchronized void guardedJoy() {
// This guard only loops once for each special event, which may not
// be the event we're waiting for.
while(!joy) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Joy and efficiency have been achieved!");
}
Lưu ý: Luôn luôn gọi phương thức wait() bên trong một vòng lặp để kiểm tra điều kiện được chờ đợi. Đừng cho rằng các gián đoạn là dành cho các điều kiện cụ thể bạn đang chờ đợi, hoặc rằng điều kiện vẫn còn đúng.
Cũng giống như nhiều phương thức treo thực thi khác, wait() có thể ném ngoại lệ InterruptedException
. Trong ví dụ này, chúng ta có thể bỏ qua ngoại lệ đó, tức là chỉ cần quan tâm đến giá trị của biến joy.
Tại sao lại đồng bộ hóa cho phương thức guardedJoy()
? Giả sử d
là đối tượng chúng tôi đang sử dụng để gọi wait(). Khi một luồng d.wait
, thì nó phải sở hữu khóa nội tại cho d
- nếu không một lỗi sẽ xảy ra. Gọi wait() bên trong một phương thức đồng bộ hóa là cách đơn giản để có được các khóa nội tại.
Khi wait() được gọi thì luồng sẽ giải phóng khóa và treo thực thi. Tại một số thời điểm trong tương lai, luồng khác sẽ gọi khóa này và gọi Object.notifyAll
, nó sẽ thông báo cho tất cả các luồng rằng cần chờ khóa đó vì một điều gì đó quan trọng đã xảy ra:
public synchronized notifyJoy() {
joy = true;
notifyAll();
}
Sau khi luồng thứ hai giải phóng khóa, thì luồng đầu tiên sẽ lấy lại khóa và tổng hợp lại bằng cách trả về từ lời gọi wait().
Lưu ý: Có một phương thức thông báo thứ hai là notify(), trong đó nó sẽ đánh thức luồng. Do notify() không cho phép ta xác định được luồng được đánh thức, nên nó chỉ có ích trong các ứng dụng song song - đó là các chương trình với một số lượng lớn các luồng, tất cả làm việc tương tự nhau. Trong một ứng dụng như vậy, bạn không quan tâm luồng nào được đánh thức.
Bây giờ ta thử sử dụng khối bảo vệ để tạo ra một ứng dụng có tên Sản xuất-Tiêu dùng. Đây là loại ứng dụng chia sẻ dữ liệu giữa hai luồng: luồng producer tạo ra dữ liệu, và luồng comsumer sử dụng dữ liệu. Hai luồng này giao tiếp với nhau bằng cách sử dụng một đối tượng chia sẻ. Ở đây sự phối hợp giữa hai luồng là cần thiết: luồng consumer phải không được cố lấy dữ liệu trước khi luồng producer cho phép nó, và luồng producer phải không được cố cung cấp dữ liệu mới nếu luồng consumer chưa truy xuất các dữ liệu cũ.
Trong ví dụ này, dữ liệu là một loạt các tin nhắn văn bản, được chia sẻ thông qua một đối tượng có kiểu lớp Drop:
public class Drop {
// Message sent from producer
// to consumer.
private String message;
// True if consumer should wait
// for producer to send message,
// false if producer should wait for
// consumer to retrieve message.
private boolean empty = true;
public synchronized String take() {
// Wait until message is
// available.
while (empty) {
try {
wait();
} catch (InterruptedException e) {}
}
// Toggle status.
empty = true;
// Notify producer that
// status has changed.
notifyAll();
return message;
}
public synchronized void put(String message) {
// Wait until message has
// been retrieved.
while (!empty) {
try {
wait();
} catch (InterruptedException e) {}
}
// Toggle status.
empty = false;
// Store message.
this.message = message;
// Notify consumer that status
// has changed.
notifyAll();
}
}
Luồng producer được định nghĩa trong lớp Producer sẽ gửi một loạt các tin nhắn quen thuộc. Chuỗi "DONE" chỉ ra rằng tất cả các tin nhắn đã được gửi đi. Để mô phỏng tính chất không thể đoán trước của các ứng dụng thực tế, luồng producer sẽ tạm dừng trong khoảng thời gian ngẫu nhiên giữa các tin nhắn.
import java.util.Random;
public class Producer implements Runnable {
private Drop drop;
public Producer(Drop drop) {
this.drop = drop;
}
public void run() {
String importantInfo[] = {
"Mares eat oats",
"Does eat oats",
"Little lambs eat ivy",
"A kid will eat ivy too"
};
Random random = new Random();
for (int i = 0;
i < importantInfo.length;
i++) {
drop.put(importantInfo[i]);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
drop.put("DONE");
}
}
Luồng consumer được định nghĩa trong lớp Consumer, luồng này có nhiệm vụ lấy các thông điệp và in chúng ra, cho đến khi nó lấy được chuỗi "DONE". Luồng này cũng tạm dừng trong khoảng thời gian ngẫu nhiên.
import java.util.Random;
public class Consumer implements Runnable {
private Drop drop;
public Consumer(Drop drop) {
this.drop = drop;
}
public void run() {
Random random = new Random();
for (String message = drop.take();
! message.equals("DONE");
message = drop.take()) {
System.out.format("MESSAGE RECEIVED: %s%n", message);
try {
Thread.sleep(random.nextInt(5000));
} catch (InterruptedException e) {}
}
}
}
Cuối cùng, đây là luồng chính, được định nghĩa tại lớp
, nó có nhiệm vụ khởi động các luồng producer và consumer.ProducerConsumerExample
public class ProducerConsumerExample {
public static void main(String[] args) {
Drop drop = new Drop();
(new Thread(new Producer(drop))).start();
(new Thread(new Consumer(drop))).start();
}
}