Java Thread: notify() and wait()

Để tránh các vấn đề thường gặp với multithreading:

  • Hai hay nhiều thread tranh nhau sử dụng chung tài nguyên - race condition.
  • Thread này chiếm giữ tài nguyên của thread khác yêu cầu đến -> dẫn đến bị tắc nghẽn - deadlock.
  • Các thread khác chiếm hết tài nguyên -> có thread không được cung cấp đủ tài nguyên -> đói tài nguyên - resource starvation.

Chúng ta có thể sử dụng một trong những cơ chế dưới đây để đồng bộ Thread, tránh xảy ra các vấn đề nêu trên.

  • Sử dụng cơ chế Monitor.
  • Sử dụng cơ chế Semaphore.
  • Sử dụng cơ chế synchronized.
  • Sử dụng Object Lock.

Trong bài này mình muốn giới thiệu về phương pháp Monitor - sử dụng notify() và wait() để đồng bộ thread.

1. Một số định nghĩa

Từ khoá synchronized được sử dụng cho việc truy cập độc quyền . Để thực hiện một phương pháp đồng bộ , chỉ cần thêm từ khóa synchronized để khai báo. Sau đó, sẽ không có hai lời gọi của method đồng bộ trên cùng một đối tượng có thể xen vào với nhau.

Synchronized phải xác định đối tượng cung cấp các khóa bên trong. Khi đồng bộ đối tượng - synchronized object được sử dụng, bạn phải tránh sử dụng đồng bộ hóa method trên một đối tượng khác cùng tham gia vào method đồng bộ hiện tại.

wait() cho phép thread được loại ra khỏi danh sách thread đang hoạt động cho đến khi một thread gọi hàm notify().

notify() thông báo và kích hoạt trở lại thread đầu tiên gọi wait() trên cùng một đối tượng.

2. Ví dụ 1

public class ThreadA {
    public static void main(String[] args){
        ThreadB b = new ThreadB();
        b.start();

        synchronized(b){
            try{
                System.out.println("Waiting for b to complete...");
                b.wait();
            }catch(InterruptedException e){
                e.printStackTrace();
            }

            System.out.println("Total is: " + b.total);
        }
    }
}

class ThreadB extends Thread{
    int total;
    @Override
    public void run(){
        synchronized(this){
            for(int i=0; i<100 ; i++){
                total += i;
            }
            notify();
        }
    }
}

Trong ví dụ ở trên, thread b được khai báo sử dụng đồng bộ - synchronized. b sẽ hoàn tất tính toán trước khi thread chính hiển thị tổng giá trị tính toán.

Output:

Waiting for b to complete...
Total is: 4950
If b is not synchonized like the code below:
public class ThreadA {
	public static void main(String[] args) {
		ThreadB b = new ThreadB();
		b.start();

		System.out.println("Total is: " + b.total);

	}
}

class ThreadB extends Thread {
	int total;

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			total += i;
		}
	}
}

Trong ví dụ tiếp theo, kết quả hiện thị có thể là 0, 10, hoặc một kết quả nào khác nữa. Điều này phụ thuộc vào tình trạng quản lý thread của monitor đang chạy. Bởi vì kết quả tổng tính toán được có thể hiển thị lên giữa lúc hàm run của ThreadB đang chạy.

3. Ví dụ 2

Ví dụ thứ hai đưa ra phức tạp hơn ví dụ 1 một chút.

import java.util.Vector;

class Producer extends Thread {

    static final int MAXQUEUE = 5;
    private Vector messages = new Vector();

    @Override
    public void run() {
        try {
            while (true) {
                putMessage();
                //sleep(5000);
            }
        } catch (InterruptedException e) {
        }
    }

    private synchronized void putMessage() throws InterruptedException {
        while (messages.size() == MAXQUEUE) {
            wait();
        }
        messages.addElement(new java.util.Date().toString());
        System.out.println("put message");
        notify();
        //Sau khi event put message được xảy ra, hàm notify() đươc gọi để đánh thức - kích hoạt lại thred getMessage tiếp tục hoạt động.
    }

    // Được gọi bởi Consumer
    public synchronized String getMessage() throws InterruptedException {
        notify();
        while (messages.size() == 0) {
            wait();
            //Gọi hàm wait() để đồng bộ hoá đoạn code sau, Thread hiện tại sẽ tạm dừng, rơi vào trạng thái nằm chờ đến khi method notify được gọi.
        }
        String message = (String) messages.firstElement();
        messages.removeElement(message);
        return message;
    }
}

class Consumer extends Thread {

    Producer producer;

    Consumer(Producer p) {
        producer = p;
    }

    @Override
    public void run() {
        try {
            while (true) {
                String message = producer.getMessage();
                System.out.println("Got message: " + message);
                //sleep(200);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String args[]) {
        Producer producer = new Producer();
        producer.start();
        new Consumer(producer).start();
    }
}

Kết quả hiện thị:

Got message: Fri Dec 02 21:37:21 EST 2011
put message
put message
put message
put message
put message
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011
put message
put message
put message
put message
put message
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011
Got message: Fri Dec 02 21:37:21 EST 2011

Bài viết ở trên, mình dịch lại từ trên trang: http://www.programcreek.com/2009/02/notify-and-wait-example/

Tuy nhiên để sử dụng được wait() và notify() đúng cách chúng ta vẫn cần phải quan tâm đến điều kiện xác đinh để thread dừng lại và chạy tiếp khi nào. Nếu chỉ sử dụng wait và notify như trên chúng ta vẫn có thể rơi tiếp và tình huống như sau: Thread A và B dừng lại khi điều kiện Condition C xảy ra và Thread C call notify() method, lúc này vẫn có thể xảy ra các vấn đề về multithread trên A và B. Tất nhiên lúc này bạn vẫn có thể áp dụng kĩ thuật wait và notify cho A và B khi A, B sử dụng chung resource. Nhưng lúc này phương pháp này sẽ trở lên không được rõ ràng nữa. Tôi sẽ tiếp tục giới thiệu về semaphore với cơ chế gần tương tự wait() và notify() trong bài tiếp theo.

Về cơ bản để giải quyết các vấn đề về multithreading thì việc hiểu rõ về các chức năng cần giải quyết và phân chia các thread cũng như quản lý resource trong thread, xác định điều kiện để dừng và chạy tiếp thread quan trọng hơn rất nhiều so với việc chọn các kĩ thuật ở trên. Nếu chúng ta có thể define chính xác các yếu tố trên thì dùng phương pháp nào cũng sẽ có thể kiểm soát được chính xác logic chạy của chương trình. Và các vấn đề multithreading sẽ trở lên đơn giản hơn rất nhiều.