Thiệt hại tiềm tàng là gì nếu có thể gọi ra wait()
bên ngoài một khối được đồng bộ hóa, giữ lại ngữ nghĩa của nó - đình chỉ chuỗi người gọi?
Chúng ta hãy minh họa những vấn đề chúng ta sẽ gặp phải nếu wait()
có thể được gọi bên ngoài một khối được đồng bộ hóa với một ví dụ cụ thể .
Giả sử chúng ta đã thực hiện một hàng đợi chặn (tôi biết, đã có một API trong :)
Lần thử đầu tiên (không đồng bộ hóa) có thể trông có gì đó dọc theo các dòng bên dưới
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data) {
buffer.add(data);
notify(); // Since someone may be waiting in take!
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) // don't use "if" due to spurious wakeups.
wait();
return buffer.remove();
}
}
Đây là những gì có thể có khả năng xảy ra:
Một chủ đề người tiêu dùng gọi take()
và thấy rằng buffer.isEmpty()
.
Trước khi chủ đề người tiêu dùng tiếp tục gọi wait()
, một chủ đề của nhà sản xuất xuất hiện và gọi đầy đủ give()
, đó là,buffer.add(data); notify();
Chủ đề người tiêu dùng bây giờ sẽ gọi wait()
(và bỏ lỡ cái notify()
vừa được gọi).
Nếu không may mắn, luồng sản xuất sẽ không sản xuất nhiều hơn give()
do thực tế là luồng tiêu dùng không bao giờ thức dậy và chúng tôi có khóa chết.
Khi bạn hiểu được vấn đề, giải pháp rất rõ ràng: Sử dụng synchronized
để đảm bảo notify
không bao giờ được gọi giữa isEmpty
và wait
.
Không đi sâu vào chi tiết: Vấn đề đồng bộ hóa này là phổ biến. Như Michael Borgwardt chỉ ra, chờ đợi / thông báo là tất cả về giao tiếp giữa các luồng, vì vậy bạn sẽ luôn luôn kết thúc với một điều kiện cuộc đua tương tự như mô tả ở trên. Đây là lý do tại sao quy tắc "chỉ chờ bên trong được đồng bộ hóa" được thi hành.
Một đoạn từ liên kết được đăng bởi @Willie tóm tắt nó khá tốt:
Bạn cần một sự đảm bảo tuyệt đối rằng người phục vụ và người thông báo đồng ý về trạng thái của vị ngữ. Người phục vụ kiểm tra trạng thái của vị ngữ tại một thời điểm hơi TRƯỚC KHI nó đi ngủ, nhưng nó phụ thuộc vào tính chính xác của vị từ là đúng khi KHI đi ngủ. Có một khoảng thời gian dễ bị tổn thương giữa hai sự kiện này, có thể phá vỡ chương trình.
Vị ngữ mà nhà sản xuất và người tiêu dùng cần phải đồng ý là trong ví dụ trên buffer.isEmpty()
. Và thỏa thuận được giải quyết bằng cách đảm bảo rằng việc chờ đợi và thông báo được thực hiện theo synchronized
khối.
Bài đăng này đã được viết lại dưới dạng một bài viết ở đây: Java: Tại sao phải chờ trong một khối được đồng bộ hóa