Java: notify () vs. notify notify () một lần nữa


377

Nếu một Googles cho "sự khác biệt giữa notify()notifyAll()" thì rất nhiều lời giải thích sẽ xuất hiện (tách rời các đoạn javadoc). Tất cả sôi sục với số lượng các chuỗi chờ được đánh thức: một trong notify()và tất cả trong notifyAll().

Tuy nhiên (nếu tôi hiểu sự khác biệt giữa các phương thức này đúng), chỉ có một luồng luôn được chọn để tiếp tục theo dõi màn hình; trong trường hợp đầu tiên, cái được chọn bởi VM, trong trường hợp thứ hai, cái được chọn bởi bộ lập lịch xử lý luồng hệ thống. Các thủ tục lựa chọn chính xác cho cả hai (trong trường hợp chung) không được lập trình viên biết.

Sự khác biệt hữu ích giữa notify ()notify ALL () là gì? Tui bỏ lỡ điều gì vậy?


6
Các thư viện hữu ích để sử dụng cho tương tranh nằm trong các thư viện tương tranh. Tôi đề nghị đây là một lựa chọn tốt hơn trong hầu hết mọi trường hợp. Thư viện đồng thuận trước ngày Java 5.0 (trong đó chúng đã được thêm vào như là tiêu chuẩn vào năm 2004)
Peter Lawrey

4
Tôi không đồng ý với Peter. Thư viện đồng thời được triển khai bằng Java và có rất nhiều mã Java được thực thi mỗi khi bạn gọi lock (), Unlock (), v.v. Bạn có thể tự bắn vào chân mình bằng cách sử dụng thư viện đồng thời thay vì cũ synchronized, ngoại trừ một số , trường hợp sử dụng khá hiếm.
Alexander Ryzhov

2
Sự hiểu lầm chính dường như là thế này: ... chỉ có một luồng luôn được chọn để tiếp tục theo dõi màn hình; trong trường hợp đầu tiên, cái được chọn bởi VM, trong trường hợp thứ hai, cái được chọn bởi bộ lập lịch xử lý luồng hệ thống. Hàm ý là về cơ bản là giống nhau. Mặc dù hành vi như được mô tả là chính xác, nhưng điều còn thiếu là trong notifyAll()trường hợp này, _ các luồng khác sau lần đầu tiên vẫn còn thức và sẽ thu được màn hình, từng cái một. Trong notifytrường hợp, không có chủ đề nào khác thậm chí được đánh thức. Vì vậy, về mặt chức năng họ rất khác nhau!
BeeOnRope

1) Nếu nhiều luồng đang chờ trên một đối tượng và thông báo () chỉ được gọi một lần trên đối tượng đó. Ngoại trừ một trong những chủ đề chờ đợi, các chủ đề còn lại chờ mãi? 2) Nếu notify () chỉ được sử dụng, một trong nhiều luồng chờ sẽ bắt đầu thực thi. Nếu notifyall () được sử dụng, tất cả các luồng chờ được thông báo nhưng chỉ một trong số chúng bắt đầu thực thi, vậy việc sử dụng notifyall () ở đây là gì?
Chetan Gowda

@ChetanGowda Thông báo tất cả các luồng so với Thông báo chính xác chỉ một luồng tùy ý thực sự có sự khác biệt đáng kể cho đến khi sự khác biệt có vẻ tinh tế nhưng quan trọng đó tấn công chúng tôi. Khi bạn thông báo () chỉ 1 luồng, tất cả các luồng khác sẽ ở trạng thái chờ cho đến khi nhận được thông báo rõ ràng /tín hiệu. Thông báo cho tất cả, tất cả các luồng sẽ được thực hiện và hoàn thành theo thứ tự lần lượt mà không cần thông báo thêm - ở đây chúng ta nên nói rằng các luồng là blockedvà không waiting. Khi lệnh thực thi blockedcủa nó tạm thời bị treo cho đến khi một luồng khác nằm trong synckhối.
dùng104309

Câu trả lời:


248

Tuy nhiên (nếu tôi hiểu sự khác biệt giữa các phương thức này đúng), chỉ có một luồng luôn được chọn để tiếp tục theo dõi màn hình.

Đó là không đúng. o.notifyAll()đánh thức tất cả các chủ đề bị chặn trong o.wait()các cuộc gọi. Các chủ đề chỉ được phép quay lại o.wait()từng cái một, nhưng mỗi chủ đề sẽ lần lượt.


Nói một cách đơn giản, nó phụ thuộc vào lý do tại sao các chủ đề của bạn đang chờ để được thông báo. Bạn có muốn nói với một trong những chủ đề chờ đợi rằng có điều gì đó đã xảy ra, hoặc bạn muốn nói với tất cả chúng cùng một lúc?

Trong một số trường hợp, tất cả các chuỗi chờ có thể thực hiện hành động hữu ích sau khi chờ kết thúc. Một ví dụ sẽ là một tập hợp các luồng đang chờ một tác vụ nhất định kết thúc; khi nhiệm vụ kết thúc, tất cả các chuỗi chờ có thể tiếp tục với công việc của họ. Trong trường hợp như vậy, bạn sẽ sử dụng notifyAll () để đánh thức tất cả các chuỗi chờ cùng một lúc.

Một trường hợp khác, ví dụ như khóa loại trừ lẫn nhau, chỉ một trong số các luồng chờ có thể làm điều gì đó hữu ích sau khi được thông báo (trong trường hợp này có được khóa). Trong trường hợp như vậy, bạn muốn sử dụng notify () . Được triển khai đúng cách, bạn cũng có thể sử dụng notifyAll () trong tình huống này, nhưng bạn sẽ đánh thức các luồng không cần thiết dù sao đi nữa.


Trong nhiều trường hợp, mã chờ điều kiện sẽ được viết dưới dạng vòng lặp:

synchronized(o) {
    while (! IsConditionTrue()) {
        o.wait();
    }
    DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}

Theo cách đó, nếu một o.notifyAll()cuộc gọi đánh thức nhiều hơn một chuỗi chờ và cuộc gọi đầu tiên trở về từ trạng thái sẽ o.wait()khiến tình trạng ở trạng thái sai, thì các luồng khác được đánh thức sẽ quay lại chờ.


29
nếu bạn thông báo chỉ một luồng nhưng nhiều luồng đang chờ trên một đối tượng, làm thế nào để VM xác định chuỗi nào sẽ thông báo?
lưỡng cư

6
Tôi không thể nói chắc chắn về thông số kỹ thuật Java, nhưng nhìn chung bạn nên tránh đưa ra các giả định về các chi tiết như vậy. Tôi nghĩ rằng bạn có thể giả định rằng VM sẽ làm điều đó một cách lành mạnh và chủ yếu là công bằng.
Liedman

15
Liedman sai nghiêm trọng, đặc tả Java tuyên bố rõ ràng rằng thông báo () không được đảm bảo là công bằng. tức là mỗi cuộc gọi để thông báo có thể đánh thức lại cùng một chuỗi (hàng đợi luồng trong màn hình KHÔNG PHẢI LÀ FAIR hoặc FIFO). Tuy nhiên, lịch trình được đảm bảo là công bằng. Đó là lý do tại sao trong hầu hết các trường hợp bạn có nhiều hơn 2 luồng, bạn nên thông báo cho AllAll.
Yann TM

45
@YannTM Tôi là tất cả đối với bệnh viêm gan mang tính xây dựng, nhưng tôi nghĩ giọng điệu của bạn hơi bất công. Tôi nói rõ ràng "không thể nói chắc chắn" và "tôi nghĩ". Dễ thôi, bạn đã bao giờ viết một cái gì đó bảy năm trước mà không đúng 100% chưa?
Liedman

10
Vấn đề là đây là câu trả lời được chấp nhận, nó không phải là một câu hỏi về niềm tự hào cá nhân. Nếu bạn biết bạn đã sai ngay bây giờ, vui lòng chỉnh sửa câu trả lời của bạn để nói và chỉ ra ví dụ xagyg sư phạm và câu trả lời đúng dưới đây.
Yann TM

330

Rõ ràng, notifyđánh thức (bất kỳ) một luồng trong tập chờ, notifyAllđánh thức tất cả các luồng trong tập chờ. Các cuộc thảo luận sau đây sẽ làm sáng tỏ bất kỳ nghi ngờ. notifyAllnên được sử dụng hầu hết thời gian. Nếu bạn không chắc chắn nên sử dụng cái nào, thì hãy sử dụng. notifyAllVui lòng xem giải thích sau.

Đọc rất kỹ và hiểu. Xin vui lòng gửi cho tôi một email nếu bạn có bất kỳ câu hỏi.

Nhìn vào nhà sản xuất / người tiêu dùng (giả định là một lớp ProducerConsumer với hai phương thức). NÓ LÀ MÔI GIỚI (vì nó sử dụng notify) - vâng, nó CÓ THỂ hoạt động - thậm chí hầu hết thời gian, nhưng nó cũng có thể gây ra bế tắc - chúng ta sẽ thấy tại sao:

public synchronized void put(Object o) {
    while (buf.size()==MAX_SIZE) {
        wait(); // called if the buffer is full (try/catch removed for brevity)
    }
    buf.add(o);
    notify(); // called in case there are any getters or putters waiting
}

public synchronized Object get() {
    // Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
    while (buf.size()==0) {
        wait(); // called if the buffer is empty (try/catch removed for brevity)
        // X: this is where C1 tries to re-acquire the lock (see below)
    }
    Object o = buf.remove(0);
    notify(); // called if there are any getters or putters waiting
    return o;
}

ĐẦU TIÊN

Tại sao chúng ta cần một vòng lặp trong khi chờ đợi?

Chúng ta cần một whilevòng lặp trong trường hợp chúng ta gặp tình huống này:

Người tiêu dùng 1 (C1) nhập khối được đồng bộ hóa và bộ đệm trống, do đó C1 được đặt trong bộ chờ (thông qua waitcuộc gọi). Người tiêu dùng 2 (C2) sắp nhập phương thức được đồng bộ hóa (tại điểm Y ở trên), nhưng Nhà sản xuất P1 đặt một đối tượng vào bộ đệm và sau đó gọi notify. Chuỗi chờ duy nhất là C1, vì vậy nó được đánh thức và bây giờ cố gắng lấy lại khóa đối tượng tại điểm X (ở trên).

Bây giờ C1 và C2 đang cố gắng để có được khóa đồng bộ hóa. Một trong số chúng (không đặc biệt) được chọn và nhập vào phương thức, cái còn lại bị chặn (không chờ đợi - nhưng bị chặn, cố lấy khóa trên phương thức). Giả sử C2 lấy khóa trước. C1 vẫn đang chặn (cố gắng lấy khóa tại X). C2 hoàn thành phương pháp và giải phóng khóa. Bây giờ, C1 mua lại khóa. Đoán xem, may mắn là chúng ta có một whilevòng lặp, bởi vì, C1 thực hiện kiểm tra vòng lặp (bảo vệ) và được ngăn chặn loại bỏ một phần tử không tồn tại khỏi bộ đệm (C2 đã có nó!). Nếu chúng ta không có while, chúng ta sẽ nhận được một IndexArrayOutOfBoundsExceptionC1 cố gắng loại bỏ phần tử đầu tiên khỏi bộ đệm!

HIỆN NAY,

Ok, bây giờ tại sao chúng ta cần thông báo?

Trong ví dụ về nhà sản xuất / người tiêu dùng ở trên, có vẻ như chúng ta có thể thoát khỏi notify. Có vẻ như vậy, bởi vì chúng tôi có thể chứng minh rằng những người bảo vệ trên các vòng chờ cho nhà sản xuất và người tiêu dùng là loại trừ lẫn nhau. Đó là, có vẻ như chúng ta không thể có một luồng chờ trong putphương thức cũng như getphương thức, bởi vì, để điều đó là đúng, thì điều sau đây sẽ phải là đúng:

buf.size() == 0 AND buf.size() == MAX_SIZE (giả sử MAX_SIZE không phải là 0)

TUY NHIÊN, điều này là không đủ tốt, chúng tôi CẦN sử dụng notifyAll. Hãy xem tại sao ...

Giả sử chúng ta có một bộ đệm có kích thước 1 (để làm cho ví dụ dễ theo dõi). Các bước sau đây dẫn chúng ta đến bế tắc. Lưu ý rằng BẤT K a một luồng nào được thông báo bằng thông báo, nó có thể không được xác định bởi JVM - đó là bất kỳ luồng chờ nào có thể được đánh thức. Cũng lưu ý rằng khi nhiều luồng đang chặn khi nhập vào một phương thức (nghĩa là cố gắng thu được khóa), thứ tự thu nhận có thể không xác định. Cũng cần nhớ rằng một luồng chỉ có thể ở một trong các phương thức tại một thời điểm - các phương thức được đồng bộ hóa chỉ cho phép một luồng được thực thi (tức là giữ khóa) bất kỳ phương thức (được đồng bộ hóa nào) trong lớp. Nếu chuỗi sự kiện sau xảy ra - kết quả bế tắc:

BƯỚC 1:
- P1 đặt 1 char vào bộ đệm

BƯỚC 2:
- P2 lần thử put- kiểm tra vòng chờ - đã là char - chờ

BƯỚC 3:
- Nỗ lực P3 put- kiểm tra vòng chờ - đã là char - chờ

BƯỚC 4:
- C1 cố gắng để có được 1 char
- C2 cố gắng để có được 1 khối char khi vào getphương thức
- C3 cố gắng để có được 1 khối char khi vào getphương thức

BƯỚC 5:
- C1 đang thực thi getphương thức - lấy phương thức char, gọi notify, thoát
- Phương notifythức đánh thức P2
- BUT, C2 nhập trước khi P2 có thể (P2 phải truy vấn khóa), vì vậy P2 chặn vào mục nhập putphương thức
- C2 kiểm tra vòng chờ, không còn ký tự trong bộ đệm, vì vậy hãy chờ
- C3 nhập phương thức sau C2, nhưng trước P2, kiểm tra vòng chờ, không còn ký tự trong bộ đệm, vì vậy hãy đợi

BƯỚC 6:
- NGAY BÂY GIỜ: có P3, C2 và C3 đang chờ!
- Cuối cùng P2 có được khóa, đặt char vào bộ đệm, thông báo cuộc gọi, thoát phương thức

BƯỚC 7:
- Thông báo của P2 đánh thức P3 (hãy nhớ bất kỳ luồng nào có thể được đánh thức)
- P3 kiểm tra điều kiện vòng chờ, đã có char trong bộ đệm, vì vậy hãy chờ.
- KHÔNG CÓ THÊM NHIỀU THÊM ĐỂ GỌI THÔNG BÁO VÀ BA BA MỌI THỜI GIAN TUYỆT VỜI!

GIẢI PHÁP: Thay thế notifybằng notifyAllmã nhà sản xuất / người tiêu dùng (ở trên).


1
finnw - P3 phải kiểm tra lại điều kiện vì các notifynguyên nhân khiến P3 (luồng được chọn trong ví dụ này) tiến hành từ điểm nó đang chờ (tức là trong whilevòng lặp). Có những ví dụ khác không gây ra bế tắc, tuy nhiên, trong trường hợp này, việc sử dụng notifykhông đảm bảo mã không có bế tắc. Việc sử dụng notifyAllkhông.
xagyg

4
@marcus Rất gần. Với notifyAll, mỗi luồng sẽ yêu cầu khóa (mỗi lần một luồng), nhưng lưu ý rằng sau khi một luồng đã lấy lại khóa và thực hiện phương thức (và sau đó thoát ra) ... luồng tiếp theo sẽ phản hồi khóa, kiểm tra "while" và sẽ quay trở lại "chờ" (tùy thuộc vào điều kiện tất nhiên). Vì vậy, thông báo đánh thức một chủ đề - khi bạn nêu chính xác. notifyAll đánh thức tất cả các luồng và mỗi luồng phản ứng lại khóa một lần - kiểm tra tình trạng của "while" và thực hiện lại phương thức hoặc "chờ" lại.
xagyg

1
@xagyg, bạn đang nói về một kịch bản trong đó mỗi nhà sản xuất chỉ có một char duy nhất để lưu trữ? Nếu vậy, ví dụ của bạn là chính xác, nhưng IMO không thú vị lắm. Với các bước bổ sung tôi đề xuất, bạn có thể bế tắc cùng một hệ thống, nhưng với số lượng đầu vào không giới hạn - đó là cách các mẫu như vậy thường được sử dụng trong thực tế.
eran

3
@codeObserver Bạn đã hỏi: "Sẽ gọi notifyAll () dẫn đến nhiều luồng chờ kiểm tra điều kiện while () cùng một lúc .. và do đó, có khả năng trước khi bị sa thải, 2 luồng đã thoát khỏi nó gây ra outOfBound ngoại lệ ?." Không, điều này là không thể, vì mặc dù nhiều luồng sẽ thức dậy, họ không thể kiểm tra điều kiện while cùng một lúc. Mỗi người đều được yêu cầu lấy lại khóa (ngay sau khi chờ) trước khi họ có thể nhập lại phần mã và kiểm tra lại trong khi đó. Do đó, từng cái một.
xagyg

4
@xagyg ví dụ hay. Đây là chủ đề từ câu hỏi ban đầu; chỉ để thảo luận Bế tắc là một vấn đề thiết kế imo (sửa tôi nếu tôi sai). Bởi vì bạn có một khóa được chia sẻ bởi cả đặt và nhận. Và JVM không đủ thông minh để gọi put sau khi phát hành khóa và ngược lại. Khóa chết xảy ra vì đặt đánh thức một đặt khác, nó tự đặt lại để chờ () vì while (). Làm cho hai lớp (và hai khóa) hoạt động? Vì vậy, hãy đặt {synchonized (get)}, get {(synchonized (put)}. Nói cách khác, get sẽ thức chỉ đặt và put sẽ thức dậy chỉ.
Jay

43

Sự khác biệt hữu ích:

  • Sử dụng thông báo () nếu tất cả các chuỗi chờ của bạn có thể hoán đổi cho nhau (thứ tự chúng thức dậy không thành vấn đề) hoặc nếu bạn chỉ có một chuỗi chờ. Một ví dụ phổ biến là nhóm luồng được sử dụng để thực hiện các công việc từ hàng đợi - khi một công việc được thêm vào, một trong các luồng được thông báo để đánh thức, thực hiện công việc tiếp theo và quay trở lại giấc ngủ.

  • Sử dụng notifyAll () cho các trường hợp khác trong đó các luồng chờ có thể có các mục đích khác nhau và có thể chạy đồng thời. Một ví dụ là một hoạt động bảo trì trên một tài nguyên được chia sẻ, trong đó nhiều luồng đang chờ hoạt động hoàn tất trước khi truy cập vào tài nguyên.


19

Tôi nghĩ rằng nó phụ thuộc vào cách tài nguyên được sản xuất và tiêu thụ. Nếu 5 đối tượng công việc có sẵn cùng một lúc và bạn có 5 đối tượng người tiêu dùng, sẽ có nghĩa là đánh thức tất cả các luồng bằng cách sử dụng notifyAll () để mỗi đối tượng có thể xử lý 1 đối tượng công việc.

Nếu bạn chỉ có sẵn một đối tượng công việc, điểm nào sẽ đánh thức tất cả các đối tượng người tiêu dùng để chạy đua với một đối tượng đó? Người đầu tiên kiểm tra công việc có sẵn sẽ nhận được nó và tất cả các luồng khác sẽ kiểm tra và thấy họ không có gì để làm.

Tôi tìm thấy một lời giải thích tuyệt vời ở đây . Nói ngắn gọn:

Phương thức notify () thường được sử dụng cho nhóm tài nguyên , trong đó có một số lượng "người tiêu dùng" hoặc "công nhân" tùy ý lấy tài nguyên, nhưng khi một tài nguyên được thêm vào nhóm, chỉ một trong những người tiêu dùng hoặc công nhân chờ đợi có thể giao dịch với nó. Phương thức notifyAll () thực sự được sử dụng trong hầu hết các trường hợp khác. Nghiêm khắc, cần phải thông báo cho người phục vụ về một điều kiện có thể cho phép nhiều người phục vụ tiến hành. Nhưng điều này thường khó biết. Vì vậy, theo nguyên tắc chung, nếu bạn không có logic cụ thể nào cho việc sử dụng notify (), thì có lẽ bạn nên sử dụng notifyAll () , vì thường rất khó để biết chính xác chủ đề nào sẽ chờ trên một đối tượng cụ thể và tại sao.


11

Lưu ý rằng với các tiện ích đồng thời, bạn cũng có sự lựa chọn giữa signal()signalAll()vì các phương thức này được gọi là có. Vì vậy, câu hỏi vẫn còn hiệu lực ngay cả với java.util.concurrent.

Doug Lea đưa ra một điểm thú vị trong cuốn sách nổi tiếng của mình : nếu một notify()Thread.interrupt()xảy ra cùng một lúc, thông báo có thể thực sự bị mất. Nếu điều này có thể xảy ra và có ý nghĩa kịch tính notifyAll()là một lựa chọn an toàn hơn mặc dù bạn phải trả giá trên cao (đánh thức quá nhiều chủ đề hầu hết thời gian).


10

Tóm tắt ngắn gọn:

Luôn luôn thích notify notify () hơn thông báo () trừ khi bạn có một ứng dụng song song ồ ạt trong đó một số lượng lớn các luồng đều làm điều tương tự.

Giải trình:

thông báo () [...] đánh thức một chuỗi. Vì notify () không cho phép bạn chỉ định luồng được đánh thức, nên nó chỉ hữu ích trong các ứng dụng song song ồ ạt - nghĩa là các chương trình có số lượng luồng lớn, tất cả đều thực hiện các công việc tương tự. Trong một ứng dụng như vậy, bạn không quan tâm chủ đề nào được đánh thức.

nguồn: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html

So sánh notify () với notifyAll () trong tình huống được mô tả ở trên: một ứng dụng song song ồ ạt trong đó các luồng đang làm điều tương tự. Nếu bạn gọi notifyAll () trong trường hợp đó, notifyAll () sẽ tạo ra sự thức dậy (tức là lên lịch) cho một số lượng lớn các luồng, nhiều trong số chúng không cần thiết (vì chỉ một luồng thực sự có thể tiến hành, cụ thể là luồng sẽ được cấp giám sát đối tượng chờ () , thông báo () hoặc notifyAll () được gọi), do đó gây lãng phí tài nguyên máy tính.

Vì vậy, nếu bạn không có một ứng dụng mà một số lượng lớn các bài làm điều tương tự đồng thời, thích notifyAll () trên thông báo () . Tại sao? Bởi vì, như những người dùng khác đã trả lời trong diễn đàn này, hãy thông báo ()

đánh thức một luồng duy nhất đang chờ trên màn hình của đối tượng này. [...] Sự lựa chọn là tùy ý và xảy ra theo quyết định của việc thực hiện.

nguồn: API Java SE8 ( https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify-- )

Hãy tưởng tượng bạn có một ứng dụng tiêu dùng của nhà sản xuất nơi người tiêu dùng sẵn sàng (tức là chờ () ing) để tiêu thụ, nhà sản xuất đã sẵn sàng (tức là chờ () ing) để sản xuất và hàng đợi các mặt hàng (sẽ được sản xuất / tiêu thụ) trống rỗng. Trong trường hợp đó, thông báo () có thể chỉ đánh thức người tiêu dùng và không bao giờ là nhà sản xuất vì lựa chọn thức dậy là tùy ý . Chu trình tiêu dùng của nhà sản xuất sẽ không có tiến triển gì mặc dù các nhà sản xuất và người tiêu dùng đã sẵn sàng sản xuất và tiêu thụ tương ứng. Thay vào đó, một người tiêu dùng bị đánh thức (tức là rời khỏi trạng thái chờ () ), không lấy một mục ra khỏi hàng đợi vì nó trống và thông báo cho một người tiêu dùng khác để tiếp tục.

Ngược lại, notifyAll () đánh thức cả người sản xuất và người tiêu dùng. Sự lựa chọn người được lên lịch phụ thuộc vào người lập lịch. Tất nhiên, tùy thuộc vào việc triển khai của người lập lịch, người lập lịch cũng có thể chỉ lên lịch cho người tiêu dùng (ví dụ: nếu bạn chỉ định mức độ ưu tiên rất cao cho người tiêu dùng). Tuy nhiên, giả định ở đây là mức độ nguy hiểm của việc lập lịch trình chỉ lập lịch cho người tiêu dùng thấp hơn mức độ nguy hiểm của JVM chỉ đánh thức người tiêu dùng bởi vì bất kỳ trình lập lịch được thực hiện hợp lý nào đều không đưa ra quyết định tùy tiện . Thay vào đó, hầu hết các thực thi lịch trình thực hiện ít nhất một số nỗ lực để ngăn chặn đói.


9

Đây là một ví dụ. Chạy nó Sau đó thay đổi một trong các notifyAll () thành thông báo () và xem điều gì sẽ xảy ra.

Lớp ProducerConsumerExample

public class ProducerConsumerExample {

    private static boolean Even = true;
    private static boolean Odd = false;

    public static void main(String[] args) {
        Dropbox dropbox = new Dropbox();
        (new Thread(new Consumer(Even, dropbox))).start();
        (new Thread(new Consumer(Odd, dropbox))).start();
        (new Thread(new Producer(dropbox))).start();
    }
}

Lớp học Dropbox

public class Dropbox {

    private int number;
    private boolean empty = true;
    private boolean evenNumber = false;

    public synchronized int take(final boolean even) {
        while (empty || evenNumber != even) {
            try {
                System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
                wait();
            } catch (InterruptedException e) { }
        }
        System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
        empty = true;
        notifyAll();

        return number;
    }

    public synchronized void put(int number) {
        while (!empty) {
            try {
                System.out.println("Producer is waiting ...");
                wait();
            } catch (InterruptedException e) { }
        }
        this.number = number;
        evenNumber = number % 2 == 0;
        System.out.format("Producer put %d.%n", number);
        empty = false;
        notifyAll();
    }
}

Lớp tiêu dùng

import java.util.Random;

public class Consumer implements Runnable {

    private final Dropbox dropbox;
    private final boolean even;

    public Consumer(boolean even, Dropbox dropbox) {
        this.even = even;
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            dropbox.take(even);
            try {
                Thread.sleep(random.nextInt(100));
            } catch (InterruptedException e) { }
        }
    }
}

Lớp nhà sản xuất

import java.util.Random;

public class Producer implements Runnable {

    private Dropbox dropbox;

    public Producer(Dropbox dropbox) {
        this.dropbox = dropbox;
    }

    public void run() {
        Random random = new Random();
        while (true) {
            int number = random.nextInt(10);
            try {
                Thread.sleep(random.nextInt(100));
                dropbox.put(number);
            } catch (InterruptedException e) { }
        }
    }
}

8

Từ Joshua Bloch, chính Giáo sư Java trong phiên bản Java hiệu quả thứ 2:

"Mục 69: Ưu tiên các tiện ích đồng thời chờ đợi và thông báo".


16
Các lý do tại sao là quan trọng hơn so với nguồn.
Pacerier

2
@Pacerier Nói tốt. Tôi sẽ quan tâm nhiều hơn đến việc tìm hiểu lý do là tốt. Một lý do có thể là sự chờ đợi và thông báo trong lớp đối tượng được dựa trên một biến điều kiện ngầm. Vì vậy, trong ví dụ về nhà sản xuất và người tiêu dùng tiêu chuẩn ..... cả nhà sản xuất và người tiêu dùng sẽ chờ đợi trong cùng một điều kiện có thể dẫn đến bế tắc như được giải thích bởi xagyg trong câu trả lời của ông. Vì vậy, cách tiếp cận tốt hơn là sử dụng 2 biến điều kiện như được giải thích trong docs.oracle.com/javase/7/docs/api/java/util/concản/locks/iêu
rahul

6

Tôi hy vọng điều này sẽ xóa một số nghi ngờ.

notify () : Phương thức notify () đánh thức một luồng đang chờ khóa (luồng đầu tiên được gọi là Wait () trên khóa đó).

notifyAll () : Phương thức notifyAll () đánh thức tất cả các luồng đang chờ khóa; JVM chọn một trong các luồng từ danh sách các luồng đang chờ khóa và đánh thức luồng đó.

Trong trường hợp một luồng duy nhất chờ khóa, không có sự khác biệt đáng kể giữa notify () và notifyAll (). Tuy nhiên, khi có nhiều hơn một luồng đang chờ khóa, trong cả notify () và notifyAll (), luồng chính xác được đánh thức nằm dưới sự kiểm soát của JVM và bạn không thể lập trình điều khiển đánh thức một luồng cụ thể.

Thoạt nhìn, có vẻ như chỉ nên gọi notify () để đánh thức một luồng; nó có vẻ không cần thiết để đánh thức tất cả các chủ đề. Tuy nhiên, vấn đề với notify () là luồng được đánh thức có thể không phải là luồng thích hợp để đánh thức (luồng có thể đang chờ một số điều kiện khác hoặc điều kiện vẫn không được thỏa mãn cho luồng đó, v.v.). Trong trường hợp đó , thông báo () có thể bị mất và không có luồng nào khác sẽ thức dậy có khả năng dẫn đến một loại bế tắc (thông báo bị mất và tất cả các luồng khác đang chờ thông báo mãi mãi).

Để tránh vấn đề này , tốt hơn hết là gọi notifyAll () khi có nhiều hơn một luồng đang chờ khóa (hoặc nhiều hơn một điều kiện trong đó chờ đợi được thực hiện). Phương thức notifyAll () đánh thức tất cả các luồng, vì vậy nó không hiệu quả lắm. tuy nhiên, mất hiệu suất này là không đáng kể trong các ứng dụng trong thế giới thực.


6

Có ba trạng thái cho một chủ đề.

  1. WAIT - Chủ đề không sử dụng bất kỳ chu kỳ CPU
  2. BLOCKED - Chuỗi bị chặn khi cố lấy màn hình. Nó vẫn có thể đang sử dụng các chu kỳ CPU
  3. CHẠY - Chủ đề đang chạy.

Bây giờ, khi một thông báo () được gọi, JVM chọn một luồng và chuyển chúng sang trạng thái BLOCKED và do đó sang Trạng thái CHẠY vì không có cạnh tranh cho đối tượng giám sát.

Khi một notifyAll () được gọi, JVM chọn tất cả các luồng và chuyển tất cả chúng sang trạng thái BLOCKED. Tất cả các luồng này sẽ có được khóa của đối tượng trên cơ sở ưu tiên. Điều này có thể có được màn hình trước tiên sẽ có thể chuyển sang trạng thái CHẠY trước và cứ thế.


Chỉ cần giải thích tuyệt vời.
royatirek

5

Tôi rất ngạc nhiên khi không ai đề cập đến vấn đề "mất thức tỉnh" khét tiếng (google nó).

Về cơ bản:

  1. nếu bạn có nhiều luồng chờ trong cùng một điều kiện và,
  2. nhiều luồng có thể khiến bạn chuyển từ trạng thái A sang trạng thái B và,
  3. nhiều luồng có thể khiến bạn chuyển từ trạng thái B sang trạng thái A (thường là các luồng giống như trong 1.) và,
  4. chuyển từ trạng thái A sang B nên thông báo cho các luồng trong 1.

THÌ bạn nên sử dụng notifyAll trừ khi bạn có thể đảm bảo rằng việc đánh thức bị mất là không thể.

Một ví dụ phổ biến là hàng đợi FIFO đồng thời trong đó: nhiều enqueuers (1. và 3. ở trên) có thể chuyển hàng đợi của bạn từ trống sang nhiều dequeuer (2. ở trên) có thể chờ điều kiện "hàng đợi không trống" -> không trống nên thông báo cho dequeuers

Bạn có thể dễ dàng viết một xen kẽ các hoạt động, trong đó, bắt đầu từ một hàng đợi trống, 2 enqueuers và 2 dequeuer tương tác và 1 enqueuer sẽ vẫn ngủ.

Đây là một vấn đề có thể so sánh với vấn đề bế tắc.


Tôi xin lỗi, xagyg giải thích chi tiết. Tên của vấn đề là "mất thức tỉnh"
NickV

@Abhay Bansal: Tôi nghĩ rằng bạn đang thiếu thực tế là condition.wait () phát hành khóa và nó được phản hồi bởi chủ đề thức dậy.
NickV

4

notify()sẽ đánh thức một chủ đề trong khi notifyAll()sẽ đánh thức tất cả. Theo tôi biết không có trung gian. Nhưng nếu bạn không chắc chắn những gì notify()sẽ làm cho chủ đề của bạn, hãy sử dụng notifyAll(). Làm việc một cách say mê mọi giờ.


4

Tất cả các câu trả lời trên đều đúng, theo như tôi có thể nói, vì vậy tôi sẽ nói với bạn điều gì đó khác. Đối với mã sản xuất, bạn thực sự nên sử dụng các lớp trong java.util.conc hiện. Có rất ít họ không thể làm cho bạn, trong lĩnh vực đồng thời trong java.


4

notify()cho phép bạn viết mã hiệu quả hơn notifyAll().

Hãy xem xét đoạn mã sau được thực thi từ nhiều luồng song song:

synchronized(this) {
    while(busy) // a loop is necessary here
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notifyAll();
}

Nó có thể được thực hiện hiệu quả hơn bằng cách sử dụng notify():

synchronized(this) {
    if(busy)   // replaced the loop with a condition which is evaluated only once
        wait();
    busy = true;
}
...
synchronized(this) {
    busy = false;
    notify();
}

Trong trường hợp nếu bạn có số lượng luồng lớn, hoặc nếu điều kiện vòng chờ là tốn kém để đánh giá, notify()sẽ nhanh hơn đáng kể so với notifyAll(). Ví dụ: nếu bạn có 1000 luồng thì 999 luồng sẽ được đánh thức và đánh giá sau lần đầu tiên notifyAll(), sau đó là 998, sau đó là 997, v.v. Ngược lại, với notify()giải pháp, chỉ có một luồng sẽ được đánh thức.

Sử dụng notifyAll()khi bạn cần chọn chủ đề nào sẽ thực hiện công việc tiếp theo:

synchronized(this) {
    while(idx != last+1)  // wait until it's my turn
        wait();
}
...
synchronized(this) {
    last = idx;
    notifyAll();
}

Cuối cùng, điều quan trọng là phải hiểu rằng trong trường hợp notifyAll(), mã bên trong synchronizedcác khối đã được đánh thức sẽ được thực thi tuần tự, không phải tất cả cùng một lúc. Giả sử có ba luồng đang chờ trong ví dụ trên và các luồng thứ tư gọi notifyAll(). Tất cả ba luồng sẽ được đánh thức nhưng chỉ một luồng sẽ bắt đầu thực thi và kiểm tra tình trạng của whilevòng lặp. Nếu điều kiện là true, nó sẽ gọi wait()lại, và chỉ sau đó luồng thứ hai sẽ bắt đầu thực thi và sẽ kiểm tra whileđiều kiện vòng lặp của nó , v.v.


4

Đây là một lời giải thích đơn giản hơn:

Bạn đã đúng rằng dù bạn sử dụng notify () hay notifyAll (), kết quả ngay lập tức là chính xác một luồng khác sẽ thu được màn hình và bắt đầu thực thi. (Giả sử một số luồng trên thực tế bị chặn khi chờ () cho đối tượng này, các luồng không liên quan khác sẽ không chiếm hết tất cả các lõi có sẵn, v.v.) Tác động đến sau.

Giả sử luồng A, B và C đang chờ trên đối tượng này và luồng A nhận được màn hình. Sự khác biệt nằm ở những gì xảy ra khi A phát hành màn hình. Nếu bạn đã sử dụng notify (), thì B và C vẫn bị chặn trong Wait (): chúng không chờ trên màn hình, chúng đang chờ để được thông báo. Khi A nhả màn hình ra, B và C vẫn sẽ ngồi đó, chờ thông báo ().

Nếu bạn đã sử dụng notifyAll (), thì B và C đều đã vượt qua trạng thái "chờ thông báo" và cả hai đang chờ để có được màn hình. Khi A nhả màn hình ra, B hoặc C sẽ có được nó (giả sử không có luồng nào khác cạnh tranh với màn hình đó) và bắt đầu thực thi.


Giải thích rất rõ ràng. Kết quả của hành vi thông báo () này có thể dẫn đến "Tín hiệu bị mất" / "Thông báo bị thiếu" dẫn đến tình trạng bế tắc / không có tiến triển của trạng thái ứng dụng.P-Producer, C-Consumer P1, P2 và C2 đang chờ C1. Các cuộc gọi C1 thông báo () và có nghĩa là cho nhà sản xuất nhưng C2 có thể được đánh thức và vì vậy cả P1 và P2 đều bỏ lỡ thông báo và sẽ chờ thêm "thông báo" (cuộc gọi thông báo) rõ ràng hơn.
dùng104309

4

Câu trả lời này là một cách viết lại đồ họa và đơn giản hóa câu trả lời xuất sắc của xagyg , bao gồm cả nhận xét của eran .

Tại sao nên sử dụng notifyAll, ngay cả khi mỗi sản phẩm được dành cho một người tiêu dùng?

Hãy xem xét các nhà sản xuất và người tiêu dùng, đơn giản hóa như sau.

Nhà sản xuất:

while (!empty) {
   wait() // on full
}
put()
notify()

Khách hàng:

while (empty) {
   wait() // on empty
}
take()
notify()

Giả sử 2 nhà sản xuất và 2 người tiêu dùng, chia sẻ bộ đệm có kích thước 1. Hình ảnh sau đây mô tả một kịch bản dẫn đến bế tắc , điều này sẽ tránh được nếu tất cả các luồng được sử dụng notifyAll .

Mỗi thông báo được dán nhãn với chủ đề được đánh thức.

bế tắc do thông báo


3

Tôi muốn đề cập đến những gì được giải thích trong Java Concurrency in Practice:

Điểm đầu tiên, liệu Thông báo hay Thông báo Tất cả?

It will be NotifyAll, and reason is that it will save from signall hijacking.

Nếu hai luồng A và B đang chờ trên các biến vị ngữ điều kiện khác nhau của cùng một hàng đợi điều kiện và thông báo được gọi, thì đó là tùy thuộc vào JVM mà luồng JVM sẽ thông báo.

Bây giờ nếu thông báo có nghĩa là cho luồng A và JVM đã thông báo luồng B, thì luồng B sẽ thức dậy và thấy rằng thông báo này không hữu ích nên nó sẽ chờ lại. Và chủ đề A sẽ không bao giờ biết về tín hiệu bị bỏ lỡ này và ai đó đã đánh cắp thông báo của nó.

Vì vậy, gọi notify notify sẽ giải quyết vấn đề này, nhưng một lần nữa nó sẽ có tác động hiệu năng vì nó sẽ thông báo cho tất cả các luồng và tất cả các luồng sẽ cạnh tranh cho cùng một khóa và nó sẽ liên quan đến chuyển đổi ngữ cảnh và do đó tải vào CPU. Nhưng chúng ta chỉ nên quan tâm đến hiệu suất nếu nó hoạt động chính xác, nếu bản thân hành vi đó không đúng thì hiệu suất không có tác dụng.

Vấn đề này có thể được giải quyết bằng cách sử dụng đối tượng Điều kiện của Khóa khóa rõ ràng, được cung cấp trong jdk 5, vì nó cung cấp sự chờ đợi khác nhau cho từng vị từ điều kiện. Ở đây nó sẽ hoạt động chính xác và sẽ không có vấn đề về hiệu năng vì nó sẽ gọi tín hiệu và đảm bảo rằng chỉ có một luồng đang chờ điều kiện đó


3

notify()- Chọn một chủ đề ngẫu nhiên từ bộ chờ của đối tượng và đặt nó ở BLOCKEDtrạng thái. Phần còn lại của các luồng trong tập chờ của đối tượng vẫn ở WAITINGtrạng thái.

notifyAll()- Di chuyển tất cả các luồng từ bộ chờ của đối tượng sang BLOCKEDtrạng thái. Sau khi bạn sử dụng notifyAll(), không còn chủ đề nào trong bộ chờ của đối tượng được chia sẻ vì tất cả chúng đều ở BLOCKEDtrạng thái và không ở trạng tháiWAITING trạng thái.

BLOCKED- bị chặn để mua khóa. WAITING- chờ thông báo (hoặc bị chặn để hoàn thành tham gia).


3

Lấy từ blog trên Java hiệu quả:

The notifyAll method should generally be used in preference to notify. 

If notify is used, great care must be taken to ensure liveness.

Vì vậy, những gì tôi hiểu là (từ blog đã nói ở trên, nhận xét của "Yann TM" về câu trả lời được chấp nhậntài liệu Java ):

  • notify (): JVM đánh thức một trong các luồng chờ trên đối tượng này. Lựa chọn chủ đề được thực hiện tùy ý mà không công bằng. Vì vậy, cùng một chủ đề có thể được đánh thức nhiều lần. Vì vậy, trạng thái của hệ thống thay đổi nhưng không có tiến bộ thực sự được thực hiện. Do đó, tạo ra một livelock .
  • notifyAll (): JVM đánh thức tất cả các luồng và sau đó tất cả các luồng chạy theo khóa trên đối tượng này. Bây giờ, bộ lập lịch CPU chọn một luồng lấy khóa trên đối tượng này. Quá trình lựa chọn này sẽ tốt hơn nhiều so với lựa chọn của JVM. Như vậy, đảm bảo sự sống.

2

Hãy xem mã được đăng bởi @xagyg.

Giả sử hai luồng khác nhau đang chờ hai điều kiện khác nhau:
Chuỗi đầu tiên đang chờ buf.size() != MAX_SIZEluồng thứ hai đang chờbuf.size() != 0 .

Giả sử tại một số điểm buf.size() không bằng 0 . Các cuộc gọi JVM notify()thay vì notifyAll()và luồng đầu tiên được thông báo (không phải luồng thứ hai).

Chuỗi đầu tiên được đánh thức, kiểm tra buf.size()xem có thể quay lại MAX_SIZEvà quay lại chờ đợi. Chủ đề thứ hai không thức dậy, tiếp tục chờ đợi và không gọi get().


1

notify() đánh thức chủ đề đầu tiên được gọi là wait() trên cùng một đối tượng.

notifyAll()đánh thức tất cả các chủ đề được gọi wait()trên cùng một đối tượng.

Các chủ đề ưu tiên cao nhất sẽ chạy đầu tiên.


13
Trong trường hợp notify()nó không chính xác là " chủ đề đầu tiên ".
Bhesh Gurung

6
bạn không thể dự đoán cái nào sẽ được VM chọn. Chỉ chúa mới biết.
Sergii Shevchyk

Không có gì đảm bảo ai sẽ là người đầu tiên (không công bằng)
Ivan Voroshilin

Nó sẽ chỉ đánh thức luồng đầu tiên nếu HĐH đảm bảo điều đó và có khả năng là không. Nó thực sự trì hoãn HĐH (và bộ lập lịch của nó) để xác định luồng nào sẽ đánh thức.
Paul Stelian

1

thông báo sẽ chỉ thông báo cho một luồng đang ở trạng thái chờ, trong khi thông báo tất cả sẽ thông báo cho tất cả các luồng ở trạng thái chờ bây giờ tất cả các luồng được thông báo và tất cả các luồng bị chặn đều đủ điều kiện để khóa, trong đó chỉ có một luồng sẽ có khóa và tất cả những người khác (bao gồm cả những người ở trạng thái chờ trước đó) sẽ ở trạng thái bị chặn.


1

Để tóm tắt các giải thích chi tiết xuất sắc ở trên và theo cách đơn giản nhất mà tôi có thể nghĩ ra, điều này là do các hạn chế của trình giám sát tích hợp JVM, mà 1) có được trên toàn bộ đơn vị đồng bộ hóa (khối hoặc đối tượng) và 2) không phân biệt về điều kiện cụ thể đang chờ / thông báo trên / về.

Điều này có nghĩa là nếu nhiều luồng đang chờ trong các điều kiện khác nhau và thông báo () được sử dụng, thì luồng đã chọn có thể không phải là luồng sẽ tiến triển trên điều kiện mới được thực hiện - gây ra luồng đó (và các luồng khác vẫn đang chờ có thể có thể để đáp ứng điều kiện, v.v.) không thể đạt được tiến bộ, và cuối cùng là chết đói hoặc treo máy chương trình.

Ngược lại, notifyAll () cho phép tất cả các luồng chờ đợi cuối cùng lấy lại khóa và kiểm tra tình trạng tương ứng của chúng, do đó cuối cùng cho phép tiến trình được thực hiện.

Vì vậy, thông báo () chỉ có thể được sử dụng một cách an toàn nếu bất kỳ luồng chờ nào được đảm bảo cho phép thực hiện tiến trình nên được chọn, điều này nói chung được thỏa mãn khi tất cả các luồng trong cùng một màn hình chỉ kiểm tra một và cùng điều kiện - một điều khá hiếm trường hợp trong các ứng dụng thế giới thực.


0

Khi bạn gọi Wait () của "đối tượng" (hy vọng khóa đối tượng được lấy), intern này sẽ giải phóng khóa trên đối tượng đó và giúp các luồng khác có khóa trên "đối tượng" này, trong kịch bản này sẽ có có nhiều hơn 1 luồng đang chờ "tài nguyên / đối tượng" (xem xét các luồng khác cũng đưa ra sự chờ đợi trên cùng một đối tượng ở trên và theo cách đó sẽ có một luồng lấp đầy tài nguyên / đối tượng và gọi thông báo / notifyAll).

Ở đây khi bạn đưa ra thông báo của cùng một đối tượng (từ cùng / phía khác của quy trình / mã), điều này sẽ giải phóng một luồng đơn bị chặn và chờ đợi (không phải tất cả các luồng chờ - luồng được phát hành này sẽ được chọn bởi JVM Thread Trình lập lịch biểu và tất cả quá trình lấy khóa trên đối tượng giống như thông thường).

Nếu bạn chỉ có một luồng sẽ chia sẻ / làm việc trên đối tượng này, bạn chỉ có thể sử dụng phương thức notify () trong triển khai thông báo chờ của bạn.

nếu bạn đang ở trong tình huống có nhiều hơn một luồng đọc và viết trên tài nguyên / đối tượng dựa trên logic nghiệp vụ của bạn, thì bạn nên truy cập notifyAll ()

bây giờ tôi đang tìm cách chính xác jvm đang xác định và phá vỡ chuỗi chờ khi chúng tôi đưa ra thông báo () trên một đối tượng ...


0

Trong khi có một số câu trả lời chắc chắn ở trên, tôi ngạc nhiên bởi số lượng nhầm lẫn và hiểu lầm tôi đã đọc. Điều này có lẽ chứng minh ý tưởng rằng người ta nên sử dụng java.util.conc hiện càng nhiều càng tốt thay vì cố gắng viết mã đồng thời bị hỏng. Quay lại câu hỏi: để tóm tắt, cách tốt nhất hiện nay là TRÁNH thông báo () trong TẤT CẢ các tình huống do sự cố đánh thức bị mất. Bất cứ ai không hiểu điều này không nên được phép viết mã đồng thời quan trọng. Nếu bạn lo lắng về vấn đề chăn gia súc, một cách an toàn để đạt được một luồng lên một lúc là: 1. Xây dựng một hàng đợi chờ rõ ràng cho các luồng chờ; 2. Có từng luồng trong hàng đợi cho tiền thân của nó; 3. Có từng cuộc gọi thông báo notifyAll () khi hoàn tất. Hoặc bạn có thể sử dụng Java.util.conc hiện. *,


theo kinh nghiệm của tôi, việc sử dụng chờ / thông báo thường được sử dụng trong các cơ chế hàng đợi trong đó một luồng ( Runnablethực hiện) xử lý nội dung của hàng đợi. Sau wait()đó được sử dụng bất cứ khi nào hàng đợi trống. Và notify()được gọi khi thông tin được thêm vào. -> trong trường hợp như vậy, chỉ có 1 luồng từng gọi wait(), sau đó không có vẻ hơi ngớ ngẩn khi sử dụng notifyAll()nếu bạn biết chỉ có 1 luồng chờ.
bvdb

-2

Thức dậy tất cả không có nhiều ý nghĩa ở đây. chờ thông báo và thông báo, tất cả những thứ này được đặt sau khi sở hữu màn hình của đối tượng. Nếu một luồng đang trong giai đoạn chờ và thông báo được gọi, thì luồng này sẽ chiếm khóa và không có luồng nào khác tại thời điểm đó có thể chiếm khóa đó. Vì vậy, truy cập đồng thời không thể diễn ra ở tất cả. Theo như tôi biết thì bất kỳ cuộc gọi chờ thông báo và notifyall chỉ có thể được thực hiện sau khi lấy khóa trên đối tượng. Đúng nếu tôi đã sai lầm.

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.