Java Bài 46: Đồng Bộ Hóa Tập 3 – Đồng Bộ Cooperation – Các Từ Khóa wait/notify/notifyall

Posted by

Chào mừng các bạn đã đến với bài học Java số 46, bài học về Đồng bộ hoá. Đây là bài học trong chuỗi bài về lập trình ngôn ngữ Java của Yellow Code Books.

Thật là một thời gian khá lâu cho bài học về Java phần tiếp theo này. Đây có thể được xem là bài viết Java khởi đầu cho năm mới, tuy nhiên lại là một chủ đề “còn nợ” lại từ năm cũ. Có thể vì thời gian đợi khá lâu sẽ làm bạn quên đôi chút. Mình xin nhắc lại là chúng ta đang nói về các cách thức Đồng bộ hóa thread trong lập trình Java. Chúng ta tìm cách làm sao để các Thread tuy được “tự do tự tại” trong việc thực thi các tác vụ song song, lại có thể biết tuân thủ theo các nguyên tắc trật tự nào đó khi chúng có sử dụng chung đến các đối tượng, hay chúng ta gọi là các tài nguyên. Bài hôm trước là một cách, hôm nay chúng ta đến với cách thứ hai. Mời các bạn đến với bài học.

Đồng Bộ Cooperation Là Gì?

Đồng bộ Cooperation, hay có một số tài liệu gọi là Inter-Thread Communication, mục đích của phương pháp đồng bộ này không ngoài mong muốn tránh các xung đột từ các Thread khi chúng đồng thời cùng sử dụng đến cùng một đối tượng, hay tài nguyên của hệ thống. Vậy tại sao lại gọi là Cooperation? Khác với bài trước có nêu lên phương pháp đồng bộ Mutual Exclusive, phương pháp này của bài hôm trước tạo ra một cơ chế loại trừ, giúp các Thread nào dùng đến tài nguyên trước sẽ được ưu tiên, Thread dùng sau sẽ bị loại trừ và phải đợi. Phương pháp hôm nay thì ngược lại hoàn toàn. Không hề có sự “đến trước sẽ ưu tiên trước” nữa. Mà chúng có sự “cộng tác” với nhau (Cooperation), cộng tác theo một tinh thần mà một Thread có thể hoàn toàn “nhường” cho Thread khác sử dụng đến tài nguyên mà nó đã “giành” trước, để rồi khi Thread nào đó khác sử dụng xong tài nguyên đó, Thread đó phải “đánh thức” nó dậy để nó tiếp tục công việc trên tài nguyên đó.

Đồng Bộ Cooperation Như Thế Nào?

Như bạn cũng biết. Cốt lõi của phương pháp đồng bộ của bài hôm nay đó là các Thread sẽ phải tự nó điều chỉnh và nhường nhịn lẫn nhau trong việc sử dụng tài nguyên. Chính vì vậy chúng ta phải làm quen với các phương thức liên quan đến sự điều chỉnh và nhường nhịn này, thay vì chỉ với một từ khóa như bài hôm trước. Chúng ta đang nói đến các phương thức wait(), notify()notifyAll().

Trước khi cùng tìm hiểu ý nghĩa và cách sử dụng của 3 phương thức này, thì mình muốn nhắc lại một chút về cơ chế Monitor & Lock mà bài hôm trước có nhắc đến. Vì sự đồng bộ của bài hôm nay không nằm ngoài việc sử dụng đến MonitorLock này.

Như bài trước có nói rằng, mỗi một đối tượng trong hệ thống sẽ có một Monitor quản lý. Mỗi một Monitor như vậy chỉ có một Lock. Khi Thread nào muốn sử dụng đến đối tượng đó, nó phải đăng ký qua Monitor của đối tượng, Monitor sẽ trao Lock về cho Thread đó để được quyền sử dụng đến đối tượng. Từ khóa synchronized trên đối tượng mà bạn đã làm quen giúp cho Monitor biết rằng nếu trao Lock về cho Thread nào đó, thì Thread khác sẽ phải ở Monitor đợi cho đến khi Lock được trả về.

Vậy vẫn với cơ chế Monitor & Lock này thì bài hôm nay sẽ như thế nào. Chúng ta cùng đến với các phương thức mà mình vừa nhắc đến trên đây. Lưu ý là các phương thức này đều được xây dựng sẵn ở lớp cha Object, điều đó có nghĩa là tất cả các đối tượng trong Java đều có các phương thức này cả nhé.

wait()

Phương thức này khi được gọi, nó sẽ làm Thread đang nắm giữ Lock trên đối tượng phải trả Lock này lại cho Monitor của đối tượng đó. Đồng thời Thread đó rơi vào trạng thái ngủ, đợi cho một Thread nào đó khác “đánh thức” dậy bằng một trong hai phương thức dưới đây.

notify()

Như đã nói ở trên, phương thức này giúp “đánh thức” Thread đã vào trạng ngủ bởi phương thức wait().

notifyAll()

Phương thức này mở rộng hơn cho notify(). Nó giúp “đánh thức” tất cả các Thread nào đã gọi đến wait() bên trong đối tượng này.

Bạn có thể thấy rằng, ý tưởng cốt lõi trong việc sử dụng các phương pháp đồng bộ của bài hôm nay, đó là việc “nhường”. Một Thread đã vào trong Monitor của một đối tượng trước tiên, lấy được Lock của đối tượng đó rồi, nhưng vì một tình huống thực tế nào đó, mà Thread đó vẫn chưa sử dụng đến đối tượng này ngay. Nó tiến hành wait() để nhường cho Thread nào đó vào sử dụng đối tượng này trước, rồi mới đánh thức nó, để nó tiếp tục công việc hiện tại còn đang dang dở.

Để hiểu rõ hơn về cách sử dụng các phương thức trong cách đồng bộ của bài hôm nay, chúng ta cùng đến với bài thực hành sau.

Thực Hành Giải Quyết Bài Toán Rút Tiền Từ Ngân Hàng

Chúng ta cùng tiếp tục xây dựng các tác vụ liên quan đến nghiệp vụ ngân hàng. Bạn có thể xem lại bài thực hành hôm trước để ôn lại cách thức đồng bộ cũ, và so sánh với kịch bản của bài thực hành hôm nay.

Hôm trước bạn đã xây dựng nên một giải thuật “hoàn hảo”, khi mà cả ông chồng lẫn cô vợ cùng thực hiện việc rút tiền trên cùng một tài khoản. Bạn đã làm cho ứng dụng phân biệt được ai rút trước, ai rút sau, để đảm bảo kiểm tra số dư một cách tuần tự, giúp ngân hàng không bị “lỗ” khi có tình huống rút cùng lúc như thế này. Và mình nghĩ cách thức giải quyết tránh xung đột của bài hôm trước hoàn toàn phù hợp cho kịch bản như thế.

Chính vì vậy mà hôm nay chúng ta không dùng đến kịch bản này nữa. Chúng ta thay đổi một chút. Giả sử ngân hàng mà bạn đang làm việc có một dịch vụ mới, đó là hỗ trợ người dùng đặt lệnh rút tiền ngay khi số dư tài khoản đủ cho khách hàng đó rút. Điều này có nghĩa rằng là, nếu trong lúc khách hàng đặt lệnh rút, mà tài khoản ngân hàng đủ cho tác vụ này, thì khách hàng sẽ nhận được tiền ngay, còn như nếu không đủ, nó sẽ chờ cho đến khi tài khoản vừa đủ tiền thì thực hiện lệnh rút. Yêu cầu của vế thứ 2 này hơi phức tạp. Nếu bạn không xem qua bài học hôm nay, bạn có thể xây dựng cho ứng dụng một chức năng kiểm tra thường xuyên tài khoản khách hàng, cứ mỗi phút kiểm tra một lần chẳng hạn, ngay khi đủ tiền rút, sẽ thực hiện lệnh rút ngay. Ý tưởng kiểm tra định kỳ coi bộ đúng, nhưng chưa hay, nó sẽ làm chậm hệ thống nếu bạn có quá nhiều khách hàng.

Và tình huống thực tế là, có một anh chồng, tài khoản của anh còn 5 triệu VND. Cô vợ muốn rút 10 triệu VND nhưng không đủ, cô sử dụng dịch vụ rút tiền ngay khi tài khoản đủ như mình có nói trên đây. Một ngày nọ, anh chồng nạp vào tài khoản 5 triệu VND. Thỏa điều kiện. Ứng dụng thực hiện việc rút tiền với cô vợ ngay lập tức.

Để làm được điều này, chúng ta sẽ xây dựng lớp BankAccount như sau. Như bạn biết, BankAccount chính là lớp quản lý thông tin số dư. Chính các Thread của ông chồng hay cô vợ đều dùng đến lớp này để thay đổi thông tin số sư của tài khoản. Lớp BankAcount đã được xây dựng từ bài trước với các phương thức checkAccountBalance()withdraw() cho tình huống kiểm tra số dư và rút tiền song song. Bài này chúng ta sẽ xây dựng thêm hai phương thức là withdrawWhenBalanceEnough()deposit() cho yêu cầu của dịch vụ mới mà mình mới trình bày ở trên. Lớp BankAccount:

public class BankAccount extends Object {

	long amount = 5000000; // Số tiền có trong tài khoản

	public synchronized boolean checkAccountBalance(long withDrawAmount) {
		// Giống code bài hôm trước, bạn tự copy/paste vào, bài này mình không hiển thị lại
	}
	
	public synchronized void withdraw(String threadName, long withdrawAmount) {
        // Giống code bài hôm trước, bạn tự copy/paste vào, bài này mình không hiển thị lại
    }

	public synchronized void withdrawWhenBalanceEnough(String threadName, long withdrawAmount) {
        // In thông tin người rút
        System.out.println(threadName + " check: " + withdrawAmount);
         
        while (!checkAccountBalance(withdrawAmount)) {
        	// Nếu không đủ tiền, thì đợi cho đến khi có đủ tiền thì rút
        	System.out.println(threadName + " wait for balance enough");
        	try {
				wait();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
        }
        
        // Đủ tiền, hoặc không còn đợi nữa, thì được phép rút
        // Giả lập thời gian rút tiền và 
        // cập nhật số tiền còn lại vào cơ sở dữ liệu
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    	
        amount -= withdrawAmount;
        System.out.println(threadName + " withdraw successful: " + withdrawAmount);
    }
	
	public synchronized void deposit(String threadName, long depositAmount) {
		// In thông tin người nạp tiền
		System.out.println(threadName + " deposit: " + depositAmount);
		
		// Giả lập thời gian nạp tiền và 
        // cập nhật số tiền mới vào cơ sở dữ liệu
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
		
		amount += depositAmount;
		
		// Đánh thức đối tượng đang ngủ và chờ có tiền thì rút
		notify();
	}
}

Bạn hãy chú ý các phương thức wait()notify() được dùng trong các phương thức của BankAccount. Đầu tiên bạn nên biết rằng các phương thức đồng bộ của bài hôm nay phải được để trong khối synchronized để đảm bảo tránh xung đột trước. Do đó các phương thức withdrawWhenBalanceEnough()deposit() đều là các phương thức synchronized cả. Bạn hãy xem lại bài hôm trước nếu chưa hiểu từ khóa synchrozied được dùng làm gì nhé.

Quay lại BankAccount. Phương thức withdrawWhenBalanceEnough() vừa vào đã kiểm tra số dư. Nếu như số dư không đủ, nó gặp ngay lệnh wait(). Như đã nói, lệnh này làm cho Thread đang nắm giữ Lock hiện tại phải trả Lock lại và ngủ, đợi chờ Thread nào đó khác đánh thức dậy. Vòng lặp while trong việc kiểm tra số dư ở đoạn này giúp cho Thread khi sống dậy vẫn phải kiểm tra số dư lại nữa. Nếu số dư khi thức dậy đã đủ, thì sẽ thực hiện lệnh rút tiền ở các dòng code bên dưới nó. Còn số dư chưa đủ, lại wait() và ngủ tiếp. Bạn hiểu chưa nào.

Còn phương thức deposit() ở BankAccount sẽ là phương thức nạp tiền vào tài khoản. Sau khi nạp tiền xong, phương thức này cứ việc gọi đến notify() để đánh thức Thread nào đó đang ngủ và chờ được rút nếu có. Và thực ra notify()deposit() không hề biết có Thread nào đang ngủ và chờ đánh thức đâu, nên nếu chắc chắn, bạn cứ gọi đến notifyAll() để đánh thức tất cả các Thread đã gọi đến wait() bên trong BankAccount này cũng được nhé.

Sau đó, để đỡ rối, chúng ta xây dựng 2 Thread, một Thread để rút tiền nếu đủ, và một Thread để nạp tiền.

Thread rút tiền như sau.

public class WithdrawThread extends Thread {

	String threadName = "";
	long withdrawAmount = 0;
	BankAccount bankAccount;

	public WithdrawThread(String threadName, BankAccount bankAccount, long withdrawAmount) {
		this.threadName = threadName;
		this.bankAccount = bankAccount;
		this.withdrawAmount = withdrawAmount;
	}

	@Override
	public void run() {
		bankAccount.withdrawWhenBalanceEnough(threadName, withdrawAmount);
	}
}

Thread rút tiền dễ hiểu đúng không bạn. Không có gì mới lạ cả. Còn đây là Thread nạp tiền.

public class DepositThread extends Thread {

	String threadName = "";
	long depositAmount = 0;
	BankAccount bankAccount;

	public DepositThread(String threadName, BankAccount bankAccount, long depositAmount) {
		this.threadName = threadName;
		this.bankAccount = bankAccount;
		this.depositAmount = depositAmount;
	}

	@Override
	public void run() {
		bankAccount.deposit(threadName, depositAmount);
	}
}

Các Thread rút tiền và nạp tiền không khác nhau là bao, chúng ta chỉ tách ra 2 Thread để lát nữa vào phương thức main() dễ nhìn thôi. Bạn có thể gộp cả 2 Thread này lại thành 1 vẫn được nhé. Bạn thử đi.

Phương thức main() chúng ta sẽ gọi đến 2 Thread như sau.

public class MainClass {
	
	public static void main(String[] args) {
		BankAccount bankAccount = new BankAccount();
		
		// Cô vợ muốn rút 10 triệu VND (bạn chú ý khi này tiền không đủ để rút)
		WithdrawThread wifeThread = new WithdrawThread("Wife", bankAccount, 10000000);
		wifeThread.start();
		
		// Anh chồng nạp vào 5 triệu VND
		DepositThread husbandThread = new DepositThread("Husband", bankAccount, 5000000);
		husbandThread.start();
	}
}

Đến đây bạn có thể thực thi chương trình để xem kết quả in ra console thế nào rồi nhé.

Đồng bộ Thread - - Kết quả bài thực hành

Chúng ta vừa hoàn thành xong kiến thức thứ 3 trong chuỗi kiến thức về Đồng bộ hóa Thread. Nhưng lại là kiến thức thứ 6 trong chuỗi kiến thức về Thread rồi đó. Bạn có thể thấy tầm quan trọng của Thread chưa nào. Chúng ta kết thúc chuỗi bài về Đồng bộ hóa Thread ở đây. Nhưng vẫn chưa kết thúc chuỗi kiến thức về Thread đâu. Và bài sau chúng ta sẽ đến với một kiến thức mới mẻ về Thread nữa nhé.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:

Đánh giá 5 sao bên dưới mỗi bài nếu thấy thích.
Comment bên dưới mỗi bài nếu có thắc mắc.
Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.

Bài Kế Tiếp

Chúng ta sẽ nói đến một khía cạnh mới của Thread có tên là Deadlock.

Advertisements
Rating: 5.0/5. From 4 votes.
Please wait...

3 comments

  1. anh có thể viết về một số mô hình như MVC, MVP trong android được không ạ! Em cảm ơn anh ^^

  2. Các bài viết của anh đều ngắn gọn và dễ hiểu.
    Cảm ơn anh rất nhiều!
    Mong chời thêm loạt bài nữa về thread!

Leave a Reply