Câu trả lời:
Các phương thức wait()
và notify()
được thiết kế để cung cấp một cơ chế cho phép chặn một luồng cho đến khi một điều kiện cụ thể được đáp ứng. Đối với điều này, tôi giả sử bạn muốn viết một triển khai hàng đợi chặn, nơi bạn có một số phần tử dự phòng có kích thước cố định.
Điều đầu tiên bạn phải làm là xác định các điều kiện mà bạn muốn các phương thức chờ đợi. Trong trường hợp này, bạn sẽ muốn put()
phương thức chặn cho đến khi có không gian trống trong cửa hàng và bạn sẽ muốn take()
phương thức chặn cho đến khi có một phần tử nào đó quay trở lại.
public class BlockingQueue<T> {
private Queue<T> queue = new LinkedList<T>();
private int capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
public synchronized void put(T element) throws InterruptedException {
while(queue.size() == capacity) {
wait();
}
queue.add(element);
notify(); // notifyAll() for multiple producer/consumer threads
}
public synchronized T take() throws InterruptedException {
while(queue.isEmpty()) {
wait();
}
T item = queue.remove();
notify(); // notifyAll() for multiple producer/consumer threads
return item;
}
}
Có một vài điều cần lưu ý về cách mà bạn phải sử dụng các cơ chế chờ và thông báo.
Trước tiên, bạn cần đảm bảo rằng mọi cuộc gọi đến wait()
hoặc notify()
nằm trong một vùng mã được đồng bộ hóa (với các cuộc gọi wait()
và notify()
được đồng bộ hóa trên cùng một đối tượng). Lý do cho điều này (ngoài các mối quan tâm an toàn luồng tiêu chuẩn) là do một cái gì đó được gọi là tín hiệu bị bỏ lỡ.
Một ví dụ về điều này, là một luồng có thể gọi put()
khi hàng đợi xảy ra đầy, sau đó nó kiểm tra điều kiện, thấy rằng hàng đợi đã đầy, tuy nhiên trước khi nó có thể chặn một luồng khác được lên lịch. Chuỗi thứ hai này sau đó take()
là một phần tử từ hàng đợi và thông báo cho các luồng chờ rằng hàng đợi không còn đầy đủ. Vì luồng đầu tiên đã kiểm tra điều kiện, tuy nhiên, nó sẽ chỉ gọi wait()
sau khi được lên lịch lại, mặc dù nó có thể đạt được tiến bộ.
Bằng cách đồng bộ hóa trên một đối tượng được chia sẻ, bạn có thể đảm bảo rằng sự cố này không xảy ra, vì take()
cuộc gọi của luồng thứ hai sẽ không thể thực hiện tiến trình cho đến khi luồng đầu tiên thực sự bị chặn.
Thứ hai, bạn cần đặt điều kiện bạn đang kiểm tra trong một vòng lặp while, thay vì một câu lệnh if, do một vấn đề được gọi là đánh thức giả. Đây là nơi một luồng chờ đôi khi có thể được kích hoạt lại mà không notify()
được gọi. Đặt kiểm tra này trong một vòng lặp while sẽ đảm bảo rằng nếu xảy ra đánh thức giả, điều kiện sẽ được kiểm tra lại và luồng sẽ gọi wait()
lại.
Như một số câu trả lời khác đã đề cập, Java 1.5 đã giới thiệu một thư viện đồng thời mới (trong java.util.concurrent
gói) được thiết kế để cung cấp một mức độ trừu tượng cao hơn so với cơ chế chờ / thông báo. Sử dụng các tính năng mới này, bạn có thể viết lại ví dụ ban đầu như vậy:
public class BlockingQueue<T> {
private Queue<T> queue = new LinkedList<T>();
private int capacity;
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
public void put(T element) throws InterruptedException {
lock.lock();
try {
while(queue.size() == capacity) {
notFull.await();
}
queue.add(element);
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while(queue.isEmpty()) {
notEmpty.await();
}
T item = queue.remove();
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
Tất nhiên, nếu bạn thực sự cần một hàng đợi chặn, thì bạn nên sử dụng một triển khai của giao diện BlockingQueue .
Ngoài ra, đối với những thứ như thế này tôi rất khuyến nghị Java đồng thời trong thực tiễn , vì nó bao gồm mọi thứ bạn có thể muốn biết về các vấn đề và giải pháp liên quan đến tương tranh.
Không phải là một ví dụ xếp hàng, nhưng cực kỳ đơn giản :)
class MyHouse {
private boolean pizzaArrived = false;
public void eatPizza(){
synchronized(this){
while(!pizzaArrived){
wait();
}
}
System.out.println("yumyum..");
}
public void pizzaGuy(){
synchronized(this){
this.pizzaArrived = true;
notifyAll();
}
}
}
Một số điểm quan trọng:
1) KHÔNG BAO GIỜ làm
if(!pizzaArrived){
wait();
}
Luôn luôn sử dụng while (điều kiện), bởi vì
while(!pizzaExists){ wait(); }
.2) Bạn phải giữ khóa (được đồng bộ hóa) trước khi gọi chờ / nofity. Chủ đề cũng phải có được khóa trước khi thức dậy.
3) Cố gắng tránh mua bất kỳ khóa nào trong khối được đồng bộ hóa của bạn và cố gắng không gọi các phương thức của người ngoài hành tinh (các phương pháp bạn không biết chắc chắn họ đang làm gì). Nếu bạn phải, hãy chắc chắn thực hiện các biện pháp để tránh bế tắc.
4) Cẩn thận với thông báo (). Gắn bó với notifyAll () cho đến khi bạn biết bạn đang làm gì.
5) Cuối cùng, nhưng không kém phần quan trọng, hãy đọc Đồng thời Java trong Thực tiễn !
pizzaArrived
cờ? nếu cờ được thay đổi mà không có lệnh gọi notify
thì nó sẽ không có hiệu lực. Cũng chỉ với wait
và notify
gọi các ví dụ hoạt động.
synchronized
từ khóa, việc khai báo biến đó là không cần thiết volatile
và nên tránh nó để tránh nhầm lẫn @mrida
Mặc dù bạn đã yêu cầu wait()
và notify()
đặc biệt, tôi cảm thấy rằng trích dẫn này vẫn đủ quan trọng:
Josh Bloch, Phiên bản Java hiệu quả thứ 2 , Mục 69: Thích các tiện ích đồng thời hơn wait
và notify
(nhấn mạnh vào nó):
Do khó sử dụng
wait
vànotify
chính xác, bạn nên sử dụng các tiện ích đồng thời cấp cao hơn thay vì [...] sử dụngwait
vànotify
trực tiếp giống như lập trình trong "ngôn ngữ lắp ráp đồng thời", so với ngôn ngữ cấp cao hơn được cung cấp bởijava.util.concurrent
. Nếu hiếm khi, lý do để sử dụngwait
vànotify
trong mã mới .
notify()
và wait()
một lần nữa
Bạn đã xem Hướng dẫn Java này chưa?
Hơn nữa, tôi khuyên bạn nên tránh xa việc chơi với loại công cụ này trong phần mềm thực. Chơi với nó thật tốt để bạn biết nó là gì, nhưng đồng thời có những cạm bẫy ở khắp mọi nơi. Tốt hơn là sử dụng trừu tượng hóa cấp cao hơn và các bộ sưu tập được đồng bộ hóa hoặc hàng đợi JMS nếu bạn đang xây dựng phần mềm cho người khác.
Đó là ít nhất những gì tôi làm. Tôi không phải là một chuyên gia đồng thời vì vậy tôi tránh xa việc xử lý các luồng bằng tay bất cứ khi nào có thể.
Thí dụ
public class myThread extends Thread{
@override
public void run(){
while(true){
threadCondWait();// Circle waiting...
//bla bla bla bla
}
}
public synchronized void threadCondWait(){
while(myCondition){
wait();//Comminucate with notify()
}
}
}
public class myAnotherThread extends Thread{
@override
public void run(){
//Bla Bla bla
notify();//Trigger wait() Next Step
}
}
Ví dụ cho Wait () và notifyall () trong Threading.
Một danh sách mảng tĩnh được đồng bộ hóa được sử dụng làm tài nguyên và phương thức Wait () được gọi nếu danh sách mảng trống. Phương thức notify () được gọi sau khi một phần tử được thêm vào danh sách mảng.
public class PrinterResource extends Thread{
//resource
public static List<String> arrayList = new ArrayList<String>();
public void addElement(String a){
//System.out.println("Add element method "+this.getName());
synchronized (arrayList) {
arrayList.add(a);
arrayList.notifyAll();
}
}
public void removeElement(){
//System.out.println("Remove element method "+this.getName());
synchronized (arrayList) {
if(arrayList.size() == 0){
try {
arrayList.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else{
arrayList.remove(0);
}
}
}
public void run(){
System.out.println("Thread name -- "+this.getName());
if(!this.getName().equalsIgnoreCase("p4")){
this.removeElement();
}
this.addElement("threads");
}
public static void main(String[] args) {
PrinterResource p1 = new PrinterResource();
p1.setName("p1");
p1.start();
PrinterResource p2 = new PrinterResource();
p2.setName("p2");
p2.start();
PrinterResource p3 = new PrinterResource();
p3.setName("p3");
p3.start();
PrinterResource p4 = new PrinterResource();
p4.setName("p4");
p4.start();
try{
p1.join();
p2.join();
p3.join();
p4.join();
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("Final size of arraylist "+arrayList.size());
}
}
if(arrayList.size() == 0)
, tôi nghĩ nó có thể là một lỗi ở đây.
notify
chỉ đánh thức một chủ đề. Nếu hai luồng tiêu dùng đang cạnh tranh để loại bỏ một yếu tố, một thông báo có thể đánh thức luồng tiêu dùng khác, không thể làm gì về nó và sẽ quay trở lại trạng thái ngủ (thay vì nhà sản xuất, mà chúng tôi hy vọng sẽ chèn một yếu tố mới.) Bởi vì chủ đề của nhà sản xuất không được đánh thức, không có gì được chèn vào và bây giờ cả ba chủ đề sẽ ngủ vô thời hạn. Tôi đã xóa nhận xét trước đây của mình như đã nói (sai) rằng đánh thức giả là nguyên nhân của vấn đề (Không phải vậy.)