Được chỉnh sửa ngày 15/06/2021.
Chào mừng các bạn đã đến với bài học Java số 44, 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.
Với việc kết thúc Bài 43 vừa rồi thì chúng ta đã sơ bộ làm quen với Thread rồi. Nếu bạn nào còn muốn tìm hiểu nhiều hơn về các phương thức khác của Thread thì có thể xem thêm ở bài viết mở rộng này của mình.
Hôm nay chúng ta sẽ sang kiến thức mới mẻ hơn, cũng liên quan đến Thread, nhưng nói về cái sự Đồng bộ hoá các Thread. Trong quá trình tiếp cận khái niệm về Đồng bộ hoá, bạn sẽ hiểu rõ hơn các phương thức hữu dụng bên trong một Thread mà mình đã đề cập ở bài học trước hay bài viết mở rộng. Mời các bạn cùng đến với bài học.
Đồng Bộ Hoá Là Gì?
Như các bạn cũng biết sơ qua ở các dòng giới thiệu trên đây của mình. Bài hôm nay nói về Đồng bộ hoá, nhưng không phải cái sự đồng bộ dữ liệu giữa các thiết bị offline với dữ liệu trên mây đâu nha. Đồng bộ ở đây là đồng bộ về cách thức hoạt động giữa các Thread với nhau. Vậy rốt cục thì nó là cái gì? Mình giải thích cho nó rõ ra là, qua các phần về Thread, nhất là Thread tập 3 vừa rồi, chắc chắn bạn đã hiểu rõ Thread, và bạn cũng biết luôn rằng ở Thread đang tồn tại một vấn đề khá đau đầu, đó là cho dù Thread là một cách thức rất hay để chúng ta tổ chức các tác vụ bên trong ứng dụng được nhanh hơn, mượt mà hơn nhờ vào đặc tính xử lý song song của nó, thì, điều này lại dẫn đến một nguy cơ, đó là cùng một lúc, có thể có nhiều hơn một Thread muốn can thiệp vào một tài nguyên dùng chung nào đó. Chúng ta cần phải có một cơ chế giúp điều tiết sao cho trong cùng một thời điểm, chỉ có một Thread có quyền được sử dụng tài nguyên dùng chung này, các Thread khác phải chờ đợi đến lượt mình. Cơ chế điều tiết này được gọi với cái tên Đồng bộ hoá (Tiếng Anh gọi là Synchronized).
Tuy nhiên, mình cũng xin nói thêm một vấn đề nữa của Đồng bộ hoá. Đó là, nếu Đồng bộ hoá chỉ được nhắc đến một cách độc lập, thì bạn có thể hiểu chức năng của nó như mình nói trên đây. Còn nếu Đồng bộ được so sánh với Bất đồng bộ (khi này Đồng bộ là Synchronous, còn Bất đồng bộ là Asynchronous) thì vấn đề lại khác đi. Synchronous giúp tổ chức các Thread theo một trật tự nhất định, Thread này xong thì Thread kia mới được thực thi tiếp, tuần tự và nhịp nhàng. Còn Asynchronous là sự tổ chức một cách… vô tổ chức, tức là chúng ta sẽ không quan tâm đến Thread nào xong trước Thread nào xong sau. Và loạt bài về Đồng bộ hoá này chúng ta chỉ nói đến khái niệm Synchronized thôi nhé (tức là không có sự so sánh giữa Synchronous và Asynchronous).
Khi Nào Sẽ Dùng Đến Đồng Bộ Hoá?
Như mình có nói ở trên, đồng bộ hoá giúp điều chỉnh sao cho cùng một thời điểm chỉ có một Thread là được sử dụng đến tài nguyên dùng chung nào đó. Vậy thì khi nào mới phải có sự điều chỉnh này? Thực ra thì không phải nhất thiết lúc nào các tài nguyên bên trong ứng dụng (các tài nguyên này là các file hoặc các đối tượng nào đó) đều cần phải có sự đồng bộ của hệ thống. Chỉ những tài nguyên nào có sự tranh chấp, có sự dùng chung giữa các Thread với nhau, dẫn đến nguy cơ có Thread này đang chỉnh sửa giá trị của đối tượng, đồng thời Thread khác cũng thực hiện việc chỉnh sửa lên đối tượng này, dẫn đến các “hiểu lầm” không cần thiết ở các Thread, có thể sẽ gây ra các tai nạn ở runtime,… thì mới dùng đến Đồng bộ hoá mà thôi.
Một ví dụ thực tế là ở ứng dụng quản lý tài khoản của ngân hàng chẳng hạn. Giả sử bạn là người xây dựng ứng dụng quản lý tài khoản này, trong ứng dụng này của bạn có một đối tượng chuyên đọc ghi cơ sở dữ liệu về số dư tài khoản của khách hàng. Một ngày nọ, khách hàng ra cây ATM để rút tiền trong tài khoản ra, giả sử tài khoản của anh này còn 20 triệu VND, anh này cần rút 15 triệu VND. Bạn cũng biết rằng để rút được tiền, ATM (tức ứng dụng của bạn) phải trải qua thao tác kiểm tra số dư trong tài khoản đó, sau đó ATM này nhận lệnh rút tiền của khách và chuẩn bị thực hiện việc rút tiền (lúc này đối tượng trong ứng dụng của bạn chỉ mới đọc dữ liệu số dư, chưa có sự sửa chữa số dư). Nhưng đồng thời cùng thời điểm đó, ở nhà, cô vợ của khách hàng đó cũng vào cùng một tài khoản với chồng mình, tiến hành chuyển hết 20 triệu VND trong tài khoản của anh chồng qua tài khoản của cô ấy, trải qua thao tác kiểm tra số dư trong tài khoản, cô vợ thấy vẫn còn đủ tiền (vì cây ATM vẫn chưa làm thao tác trừ tiền), cô vợ thực hiện lệnh chuyển tiền. Và rồi chuyện gì xảy ra? Vì đối tượng trong ứng dụng của bạn thấy còn tiền (ở cả cây ATM của anh chồng và trang web chuyển khoản của cô vợ), nó thực hiện thao tác trừ 15 triệu VND trong cơ sở dữ liệu đối với anh chồng, và trừ 20 triệu VND trong cơ sở dữ liệu nữa đối với cô vợ. Anh chồng sẽ nhận được khoản tiền cần rút, và, tài khoản của cô vợ cũng nhận được khoản tiền vừa chuyển qua. Tóm lại, ngân hàng sẽ mất tiền (mất 15 triệu VND), bạn bị đuổi việc.
Nào, nào, mình chỉ giả sử thôi, chắc chắn sau khi đọc qua loạt bài về Đồng bộ hoá này, bạn hoàn toàn có thể tránh được lỗi lầm có thể xảy đến trong tương lai như ví dụ thôi. Để dễ hiểu hơn, chúng ta cùng làm lại tình huống này thành một project nho nhỏ như sau.
Ví Dụ Rút Tiền Ngân Hàng Khi Chưa Đồng Bộ Hoá
Ở ví dụ này mình mời các bạn cùng xây dựng một ứng dụng mô phỏng việc rút tiền từ ngân hàng như trên đây.
Đầu tiên chúng ta xây dựng một đối tượng nắm giữ thông tin số dư tài khoản của khách hàng. Đối tượng này được bạn xây dựng rất cẩn thận, nó có thể kiểm tra số dư tài khoản trước khi cho phép rút (số dư được thiết lập ban đầu là 20 triệu). Sau khi kiểm tra số dư và thấy được phép rút tiền, nó sẽ trừ số dư trong tài khoản đi. Giả sử các thao tác kiểm tra số dư tài khoản và cập nhật lại số dư mới vào cơ sở dữ liệu mất 2 giây cho mỗi thao tác. Tất cả đều được giả lập thông qua code của lớp sau. Lớp này mình đặt tên là BankAccount.
public class BankAccount { long amount = 20000000; // Số tiền có trong tài khoản public boolean checkAccountBalance(long withDrawAmount) { // Giả lập thời gian đọc cơ sở dữ liệu và kiểm tra tiền try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } if (withDrawAmount <= amount) { // Cho phép rút tiền return true; } // Không cho phép rút tiền return false; } public void withdraw(String threadName, long withdrawAmount) { // In thông tin người rút System.out.println(threadName + " withdraw: " + withdrawAmount); if (checkAccountBalance(withdrawAmount)) { // 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; } // In ra số dư tài khoản System.out.println(threadName + " see balance: " + amount); } }
Code trên đây dễ hiểu đúng không bạn. Bạn cũng đã biết rằng, BankAccount này chính là một tài nguyên dùng chung. Và đã là tài nguyên dùng chung, thì bạn nên xây dựng một Thread rút tiền như sau. Thread này sẽ cho phép truyền vào tài nguyên dùng chung này, và truyền luôn số tiền cần rút, rồi sau đó nó sẽ gọi đến phương thức rút tiền của tài nguyên đó. Mình đặt tên cho Thread rút tiền này là WithdrawThread.
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.withdraw(threadName, withdrawAmount); } }
Cuối cùng, code cho phương thức main() khá đơn giản, chúng ta chỉ cần khai báo 2 Thread rút tiền rồi cho chúng sử dụng chung cái tài nguyên bankAccount thôi.
public static void main(String[] args) { BankAccount bankAccount = new BankAccount(); // Người chồng rút 15 triệu WithdrawThread husbandThread = new WithdrawThread("Husband", bankAccount, 15000000); husbandThread.start(); // Người vợ rút hết tiền (20 triệu) WithdrawThread wifeThread = new WithdrawThread("Wife", bankAccount, 20000000); wifeThread.start(); }
Và khi thực thi chương trình này lên, bạn sẽ thấy kết quả như hình dưới. Kết quả này chính là quá trình mà hai Thread husbandThread và wifeThread cùng vào kiểm tra tài khoản và thấy có khả năng rút được, sau đó chúng cùng thực hiện lệnh rút tiền, và kết quả cả hai đều thấy tiến trình thực hiện thành công. Nhưng… giá trị số dư lại là số âm (có nghĩa là ngân hàng mất tiền).
Chúng ta chỉ dừng lại ở ví dụ trên đây thôi, đến bài học sau, khi chúng ta bắt đầu các kiến thức cụ thể về các cách thức Đồng bộ hoá, chúng ta sẽ biết làm thế nào để ngân hàng trên đây không bị mất tiền, và có thể giữ lại cái mạng, à không, cái job cho bạn.
Các Cách Thức Đồng Bộ Hoá
Ở loạt bài về đồng bộ hoá này, mình sẽ chia chúng làm 2 cách, cũng là 2 phần lớn để chúng ta dễ dàng tiếp cận và ghi nhớ.
Cách thứ nhất được gọi là Mutual Exclusive. Có thể hiểu là Loại trừ lẫn nhau. Cách này hệ thống sẽ giúp ưu tiên một Thread và giúp ngăn chặn các Thread khác, khỏi nguy cơ xung đột với nhau. Do đặc tính này của cơ chế làm chúng ta liên tưởng tới một sự can thiệp mạnh tay, quyết liệt của hệ thống, nên mới có cái tên “loại trừ”. Cách này sẽ được mình gói gọn trong bài học kế tiếp.
Cách thứ hai được gọi là Cooperation. Có thể hiểu là Cộng tác với nhau. Cách này bản thân các Thread sẽ bắt tay với nhau, cùng nhau điều tiết thứ tự ưu tiên để có thể tự bản thân chúng tránh sự xung đột. Cách này sẽ được mình trình bày ở bài kế tiếp theo bài về cách loại trừ trên đây.
Thực ra ở bài học trước, bạn cũng đã được làm quen sơ qua với hai cách thức đồng bộ này rồi. Bạn nhớ lại đi. Link này là cách Mutual Exclusive, còn link này là cách Cooperation.
Kết Luận
Xong rồi, bài học nhẹ nhàng thôi đúng không nào. Kiến thức của bài viết chỉ tập trung vào việc giới thiệu khái niệm Đồng bộ hoá và hai cách thức lớn của Đồng bộ hoá mà chúng ta sẽ xem xét chúng ở các phần sau 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 phương thức đồng bộ đầu tiên có tên Mutual Exclusive ở bài tiếp theo.
Em có một thắc mắc là, rõ ràng thread husband chạy trước thì đáng lẽ phải in dòng “Husband withdraw 15000000” trước chứ nhỉ? Kết quả thì lại in dòng “Wife withdraw 20000000”. Mong anh giải thích cho em với
Thực ra thread mà các bạn đang xem là các task bất đồng bộ. Có nghĩa là chúng ta không biết nó được hệ thống thực thi trước sau khi nào, và kết thúc khi nào. Một thread khi bạn start() nó, nó phải qua một vài trạng thái, và phải đợi hệ thống cấp phát tài nguyên đâu đó sẵn sàng thì nó mới được thực thi. Cho nên việc bạn start() một thread trước không có nghĩa là nó sẽ được run trước đâu nhé. Nắm được ý này bạn sẽ hiểu rõ hơn về bất đồng bộ đấy.
ANh ơi làm sao để để hủy thời gian chờ của 1 thread ạ , Mong anh giúp em , em đang xây dựng 1 phần mền có liên quan đến cái này ạ .
Bạn có thể gọi đến phương thức interrupt() của một Thread, nhưng vẫn phải try catch InterruptedException bên trong Thread đó nữa, chi tiết bạn có thể xem link này nhé: https://yellowcodebooks.com/2021/06/12/tong-hop-cac-phuong-thuc-cua-thread/#interruptisInterruptedThreadinterrupted
Viết bài tâm huyết và rất chi tiết