Java Bài 44: Đồng Bộ Hoá Tập 1 – Làm Quen Với Đồng Bộ Hoá

Posted by

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.

Còn nhớ bài trước mình có hứa ở cuối bài rằng hôm nay chúng ta sẽ xem xét tất cả các phương thức hữu dụng của Thread. Nhưng mình nhận ra rằng đa số các phương thức hay ho mà Thread mang lại đều xoay quanh việc làm sao giúp đồng bộ các Thread trong một Process với nhau.

Vậy thì, để dễ dàng hơn cho các tổng hợp về các phương thức mà mình đã hứa, mình quyết định sẽ trình bày sang kiến thức về cái sự Đồng bộ hoá các Thread này. 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 hơn thôi. Hôm nay chúng ta sẽ bắt đầu làm quen với các khai niệm sơ khởi về Đồng bộ hoá trước. 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, hay Synchronization).

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ộSynchronous, còn Bất đồng bộ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 SynchronousAsynchronous).

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 husbandThreadwifeThread 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).

Kết quả chương trình khi chưa đồng bộ hoá

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.

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 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 phương thức đồng bộ đầu tiên có tên Mutual Exclusive ở bài tiếp theo.

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

Gửi phản hồi