Được chỉnh sửa ngày 24/06/2021.
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() và 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 Monitor và Lock 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, hoặc tự dậy khi hết thời gian ngủ nếu gọi đến wait(long timeoutMilis).
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(). Nếu có nhiều Thread cùng gọi đến wait(), tức là cùng bị ngủ khi gọi đến đối tượng này, phương thức notify() sẽ đánh thức bất kỳ Thread nào trong các Thread đang ngủ.
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 đợi, cho đến khi được đánh thức (hoặc tự hết giờ đợi), để 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 hành của chúng ta 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() và 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() và 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 { long amount = 5000000; // Số tiền có trong tài khoản public 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() và 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() và 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é.
Kết Luận
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 ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết 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.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.
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.
Nhờ anh làm bài viết về phần bất đồng bộ với hehe ^^.
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 ^^
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!
“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.”
Chỗ này có phải đa hình lúc runtime override ko a ?
Ý tưởng của e là dùng Anonymous Inner Class để override lại hàm run, hoặc tạo 2 class: deposit, withdraw extends rồi override phương thức run.
Mình nghĩ là ổn, đó là một cách tổ chức code theo một kiến trúc chặt chẽ hơn.
ad ơi tại sao luồng của người chồng không thực hiện trước ạ
Mình chưa hiểu câu hỏi của bạn. Không biết là bạn đang thắc mắc rằng với code của bài viết, tạo sao luồng của người chồng không thực hiện trước. Hay bạn muốn hỏi tại sao mình không code để cho luồng của người chồng thực hiện trước.
Nếu vì code trên, thì mình gọi start husbandThread sau wifeThread mà bạn. Vả lại husbandThread trước khi nạp tiền còn ngủ 2 giây nữa, nên khả năng chắc chắn là chạy sau wifeThread rồi nhé.
Anh ơi cho em hỏi, ở dòng 17 của BankAccount, tại sao phải dùng while ạ?
Dùng if có được không tại em dùng if vẫn được
Uhm mình đang nghĩ là bạn nói đúng. Vòng while lúc này cũng không giúp chương trình chạy đúng hơn if, vì sau khi kiểm tra thấy không đủ tiền rút, cũng không có vòng lặp tiếp theo được chạy. Để mình xem kỹ lại, có gì sẽ chỉnh sửa lại code của bài viết bạn nhé.
Cảm ơn ad về những bài viết java 😀