Tại sao phải chờ () luôn ở trong khối được đồng bộ hóa


257

Chúng ta đều biết rằng để gọi Object.wait(), cuộc gọi này phải được đặt trong khối được đồng bộ hóa, nếu không IllegalMonitorStateExceptionsẽ bị ném. Nhưng lý do để thực hiện hạn chế này là gì? Tôi biết rằng wait()sẽ phát hành màn hình, nhưng tại sao chúng ta cần phải có được màn hình một cách rõ ràng bằng cách làm cho khối cụ thể được đồng bộ hóa và sau đó phát hành màn hình bằng cách gọi wait()?

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?

Câu trả lời:


232

Điều này wait()chỉ có ý nghĩa khi có một notify(), vì vậy, luôn luôn là về giao tiếp giữa các luồng và cần đồng bộ hóa để hoạt động chính xác. Người ta có thể lập luận rằng điều này nên được ngầm định, nhưng điều đó sẽ không thực sự có ích, vì lý do sau:

Về mặt ngữ nghĩa, bạn không bao giờ chỉ wait(). Bạn cần một số điều kiện để được chỉnh sửa, và nếu không, bạn đợi cho đến khi nó được. Vì vậy, những gì bạn thực sự làm là

if(!condition){
    wait();
}

Nhưng điều kiện đang được đặt bởi một luồng riêng biệt, vì vậy để có tác vụ này chính xác, bạn cần đồng bộ hóa.

Một vài điều nữa với nó, trong đó chỉ vì chủ đề của bạn bỏ chờ không có nghĩa là điều kiện bạn đang tìm kiếm là đúng:

  • Bạn có thể nhận được đánh thức giả (có nghĩa là một chuỗi có thể thức dậy sau khi chờ mà không nhận được thông báo) hoặc

  • Điều kiện có thể được thiết lập, nhưng một luồng thứ ba làm cho điều kiện sai trở lại vào thời điểm luồng chờ đợi thức dậy (và phản ứng lại màn hình).

Để giải quyết những trường hợp này, điều bạn thực sự cần luôn là một số biến thể của điều này:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

Tốt hơn hết, đừng lộn xộn với các nguyên thủy đồng bộ hóa và làm việc với các tóm tắt được cung cấp trong các java.util.concurrentgói.


3
Có một cuộc thảo luận chi tiết ở đây là tốt, nói về cơ bản là điều tương tự. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/...

1
btw, nếu bạn không bỏ qua cờ bị gián đoạn, vòng lặp cũng sẽ kiểm tra Thread.interrupted().
bestsss

2
Tôi vẫn có thể làm một cái gì đó như: while (! Condition) {sync (this) {Wait ();}} có nghĩa là vẫn còn một cuộc đua giữa việc kiểm tra điều kiện và chờ đợi ngay cả khi Wait () được gọi chính xác trong một khối được đồng bộ hóa. Vậy có lý do nào khác đằng sau sự hạn chế này, có lẽ là do cách nó được triển khai trong Java?
shrini1000

9
Một kịch bản khó chịu khác: điều kiện là sai, chúng ta sắp sửa chờ () và sau đó một luồng khác thay đổi điều kiện và gọi thông báo (). Bởi vì chúng tôi chưa chờ (), chúng tôi sẽ bỏ lỡ thông báo này (). Nói cách khác, kiểm tra và chờ đợi, cũng như thay đổi và thông báo phải là nguyên tử .

1
@Nullpulum: Nếu đó là một loại có thể được viết nguyên tử (chẳng hạn như boolean ngụ ý bằng cách sử dụng nó trực tiếp trong mệnh đề if) và không có sự phụ thuộc lẫn nhau với dữ liệu chia sẻ khác, bạn có thể thoát khỏi việc khai báo nó không ổn định. Nhưng bạn cần hoặc đồng bộ hóa để đảm bảo rằng bản cập nhật sẽ hiển thị kịp thời cho các luồng khác.
Michael Borgwardt

283

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:

  1. Một chủ đề người tiêu dùng gọi take()và thấy rằng buffer.isEmpty().

  2. 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();

  3. 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).

  4. 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 notifykhông bao giờ được gọi giữa isEmptywait.

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 synchronizedkhố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


Ngoài ra, để đảm bảo những thay đổi được thực hiện cho điều kiện được nhìn thấy ngay sau khi chờ đợi () kết thúc, tôi đoán vậy. Mặt khác, cũng là một khóa chết vì thông báo () đã được gọi.
Surya Wijaya Madjid

Thật thú vị, nhưng lưu ý rằng chỉ cần gọi đồng bộ hóa thực sự sẽ không giải quyết được các vấn đề như vậy do tính chất "không đáng tin cậy" của chờ đợi () và thông báo (). Đọc thêm tại đây: stackoverflow.com/questions/21439355/ . Lý do tại sao đồng bộ hóa là cần thiết trong kiến ​​trúc phần cứng (xem câu trả lời của tôi dưới đây).
Marcus

Nhưng nếu thêm return buffer.remove();vào trong khi chặn nhưng sau wait();đó, nó hoạt động?
BobJiang

@BobJiang, không, chủ đề có thể được đánh thức vì lý do khác hơn là ai đó gọi cho. Nói cách khác, bộ đệm có thể trống ngay cả sau khi waittrả về.
aioobe

Tôi chỉ có Thread.currentThread().wait();trong mainchức năng bao bọc bởi try-catch cho InterruptedException. Không có synchronizedkhối, nó cho tôi ngoại lệ tương tự IllegalMonitorStateException. Điều gì làm cho nó đạt đến trạng thái bất hợp pháp bây giờ? Nó hoạt động bên trong synchronizedkhối mặc dù.
Shashwat

12

@Rubball là đúng. Các wait()được gọi là, do đó các chủ đề có thể chờ đợi đối với một số điều kiện để xảy ra khi này wait()gọi xảy ra, các chủ đề được buộc phải từ bỏ khóa của nó.
Để từ bỏ một cái gì đó, bạn cần sở hữu nó đầu tiên. Chủ đề cần phải sở hữu khóa đầu tiên. Do đó, cần phải gọi nó trong một synchronizedphương thức / khối.

Có, tôi đồng ý với tất cả các câu trả lời trên liên quan đến thiệt hại / sự không nhất quán tiềm ẩn nếu bạn không kiểm tra điều kiện trong synchronizedphương thức / khối. Tuy nhiên, như @ shrini1000 đã chỉ ra, chỉ cần gọi wait()trong khối được đồng bộ hóa sẽ không ngăn chặn sự không nhất quán này xảy ra.

Đây là một bài đọc tốt ..


5
@Popeye Giải thích 'đúng' đúng cách. Nhận xét của bạn là không sử dụng cho bất cứ ai.
Hầu tước Lorne

4

Vấn đề có thể gây ra nếu bạn không đồng bộ hóa trước wait()như sau:

  1. Nếu luồng thứ 1 đi vào makeChangeOnX()và kiểm tra điều kiện while, và nó là true( x.metCondition()trả về false, có nghĩa x.conditionfalse) vì vậy nó sẽ vào bên trong nó. Sau đó, ngay trước wait()phương thức, một luồng khác đi đến setConditionToTrue()và đặt x.conditionthành truenotifyAll().
  2. Sau đó, chỉ sau đó, luồng thứ 1 sẽ nhập wait()phương thức của anh ta (không bị ảnh hưởng bởi những notifyAll()gì đã xảy ra vài phút trước đó). Trong trường hợp này, luồng thứ 1 sẽ chờ đợi một luồng khác thực hiện setConditionToTrue(), nhưng điều đó có thể không xảy ra nữa.

Nhưng nếu bạn đặt synchronizedtrước các phương thức thay đổi trạng thái đối tượng, điều này sẽ không xảy ra.

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}

2

Chúng ta đều biết rằng các phương thức Wait (), notify () và notify ALL () được sử dụng cho truyền thông liên luồng. Để loại bỏ tín hiệu bị bỏ lỡ và các vấn đề đánh thức giả, luồng chờ luôn chờ trong một số điều kiện. ví dụ-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

Sau đó, thông báo bộ chủ đề đã biến Biến thành đúng và thông báo.

Mỗi luồng có bộ đệm cục bộ của chúng, vì vậy tất cả các thay đổi đầu tiên được ghi ở đó và sau đó được chuyển dần sang bộ nhớ chính.

Nếu các phương thức này không được gọi trong khối được đồng bộ hóa, biến wasNotified sẽ không được đưa vào bộ nhớ chính và sẽ ở đó trong bộ đệm cục bộ của luồng để luồng chờ sẽ tiếp tục chờ tín hiệu mặc dù nó được đặt lại bằng cách thông báo luồng.

Để khắc phục các loại sự cố này, các phương thức này luôn được gọi bên trong khối được đồng bộ hóa, đảm bảo rằng khi khối được đồng bộ hóa bắt đầu thì mọi thứ sẽ được đọc từ bộ nhớ chính và sẽ được xóa vào bộ nhớ chính trước khi thoát khỏi khối được đồng bộ hóa.

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

Cảm ơn, hy vọng nó làm rõ.


1

Điều này về cơ bản phải làm với kiến ​​trúc phần cứng (tức là RAMbộ nhớ cache ).

Nếu bạn không sử dụng synchronizedcùng với wait()hoặc notify(), một luồng khác có thể vào cùng một khối thay vì chờ màn hình vào. Ngoài ra, khi truy cập vào một mảng không có khối được đồng bộ hóa, một luồng khác có thể không thấy sự thay đổi của nó ... thực sự một luồng khác sẽ không thấy bất kỳ thay đổi nào đối với mảng đó khi nó đã có một bản sao của mảng trong bộ đệm cấp x ( hay còn gọi là bộ nhớ cấp 1/2, cấp 3) của lõi CPU xử lý luồng.

Nhưng các khối được đồng bộ hóa chỉ là một mặt của huy chương: Nếu bạn thực sự truy cập một đối tượng trong bối cảnh được đồng bộ hóa từ bối cảnh không được đồng bộ hóa, đối tượng vẫn sẽ không được đồng bộ hóa ngay cả trong một khối được đồng bộ hóa, bởi vì nó giữ một bản sao của đối tượng trong bộ đệm của nó. Tôi đã viết về vấn đề này ở đây: https://stackoverflow.com/a/21462631Khi một khóa giữ một đối tượng không phải là cuối cùng, tham chiếu của đối tượng vẫn có thể được thay đổi bởi một chủ đề khác?

Hơn nữa, tôi tin rằng bộ nhớ cache cấp x chịu trách nhiệm cho hầu hết các lỗi thời gian chạy không thể lặp lại. Đó là bởi vì các nhà phát triển thường không học những thứ cấp thấp, như cách thức hoạt động của CPU hoặc cách phân cấp bộ nhớ ảnh hưởng đến việc chạy các ứng dụng: http://en.wikipedia.org/wiki/Memory_hierarchy

Nó vẫn là một câu đố tại sao các lớp lập trình không bắt đầu với phân cấp bộ nhớ và kiến ​​trúc CPU trước tiên. "Xin chào thế giới" sẽ không giúp đỡ ở đây. ;)


1
Chỉ cần phát hiện ra một trang web giải thích nó một cách hoàn hảo và chuyên sâu: javamex.com/tutorials/ Kẻ
Marcus

Hmm .. không chắc chắn tôi làm theo. Nếu bộ nhớ đệm là lý do duy nhất để đặt chờ và thông báo bên trong được đồng bộ hóa, tại sao đồng bộ hóa không được đặt trong quá trình thực hiện chờ / thông báo?
aioobe

Câu hỏi hay, vì chờ đợi / thông báo rất có thể là các phương thức được đồng bộ hóa ... có lẽ các nhà phát triển Java trước đây của Sun biết câu trả lời? Hãy xem liên kết ở trên, hoặc có thể điều này cũng sẽ giúp bạn: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

Một lý do có thể là: Trong những ngày đầu của Java, không có lỗi biên dịch khi không gọi được đồng bộ hóa trước khi thực hiện các hoạt động đa luồng này. Thay vào đó, chỉ có lỗi thời gian chạy (ví dụ: coderanch.com/t/239491/java-programmer-SCJP/certification/, ). Có lẽ họ thực sự nghĩ rằng @SUN rằng khi các lập trình viên gặp phải những lỗi này, họ sẽ được liên hệ, điều này có thể đã cho họ cơ hội bán thêm máy chủ của họ. Khi nào nó thay đổi? Có thể Java 5.0 hoặc 6.0, nhưng thực ra tôi không nhớ là phải trung thực ...
Marcus

TBH Tôi thấy một vài vấn đề với câu trả lời của bạn 1) câu thứ hai của bạn không có ý nghĩa: Nó không quan trọng đối tượng a thread có một khóa trên. Bất kể đối tượng nào hai luồng được đồng bộ hóa trên, tất cả các thay đổi đều được hiển thị. 2) Bạn nói một chủ đề khác "sẽ không" thấy bất kỳ thay đổi nào. Điều này nên là "có thể không" . 3) Tôi không biết lý do tại sao bạn đưa lên bộ nhớ cache cấp 1/2, ... Điều quan trọng ở đây là Mô hình bộ nhớ Java nói gì và được chỉ định trong JLS. Mặc dù kiến ​​trúc phần cứng có thể giúp hiểu lý do tại sao JLS nói những gì nó làm, nhưng nó nói đúng là không liên quan trong bối cảnh này.
aioobe

0

trực tiếp từ hướng dẫn tiên tri java này :

Khi một luồng gọi d.wait, nó phải sở hữu khóa nội tại cho d - nếu không sẽ xảy ra lỗi. Gọi chờ trong một phương thức được đồng bộ hóa là một cách đơn giản để có được khóa nội tại.


Từ câu hỏi mà tác giả đưa ra, dường như tác giả câu hỏi không hiểu rõ về những gì tôi đã trích dẫn từ hướng dẫn. Ngoài ra, câu trả lời của tôi, giải thích "Tại sao".
Bóng lăn

0

Khi bạn gọi notify () từ một đối tượng t, java sẽ thông báo cho một phương thức t.wait () cụ thể. Nhưng, làm thế nào để java tìm kiếm và thông báo một phương thức chờ cụ thể.

java chỉ nhìn vào khối mã được đồng bộ hóa bởi đối tượng t. java không thể tìm kiếm toàn bộ mã để thông báo cho một t.wait () cụ thể.


0

theo tài liệu:

Chủ đề hiện tại phải sở hữu màn hình của đối tượng này. Các chủ đề phát hành quyền sở hữu của màn hình này.

wait()Phương thức đơn giản có nghĩa là nó giải phóng khóa trên đối tượng. Vì vậy, đối tượng sẽ chỉ bị khóa trong khối / phương thức được đồng bộ hóa. Nếu luồng nằm ngoài khối đồng bộ hóa có nghĩa là nó không bị khóa, nếu nó không bị khóa thì bạn sẽ giải phóng cái gì trên đối tượng?


0

Chỉ chờ trên đối tượng giám sát (đối tượng được sử dụng bởi khối đồng bộ hóa), Có thể có n số đối tượng giám sát trong toàn bộ hành trình của một luồng. Nếu Thread chờ bên ngoài khối đồng bộ hóa thì không có đối tượng giám sát và các luồng khác thông báo để truy cập cho đối tượng giám sát, vậy làm thế nào để luồng bên ngoài khối đồng bộ hóa sẽ biết rằng nó đã được thông báo. Đây cũng là một trong những lý do mà Wait (), notify () và notify ALL () nằm trong lớp đối tượng chứ không phải lớp thread.

Về cơ bản, đối tượng giám sát là tài nguyên chung ở đây cho tất cả các luồng và các đối tượng giám sát chỉ có thể có sẵn trong khối đồng bộ hóa.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.