Trong hầu hết cỏc chương trỡnh đa luồng, cỏc thread cú thể cần phải truy cập đến cựng một đối tượng. Điều gỡ xảy ra nếu cả hai thread cựng truy cập đến đối tượng và làm thay đổi thuộc tớnh đối tượng. Kết quả cú thể là cỏc thread bị ảnh hưởng lẫn nhau và khụng đạt được kết quả mong muốn. Tỡnh trạng này gọi là “đua tranh”.
V.1 Tỡnh trạng “đua tranh”
Trong phần tiếp theo, chỳng ta mụ phỏng một ngõn hàng cú rất nhiều tài khoản khỏch hàng. Chỳng ta thử thiết kế một chương trỡnh để chuyển tiền ngẫu nhiờn giữa cỏc tài khoản. Mỗi tài khoản cú một thread riờng. Mỗi giao dịch thực hiện việc chuyển tiền từ account sang một account ngẫu nhiờn khỏc.
Ta cú một class Bank với phương thức transfer để chuyển tiền. Nếu account chuyển khụng đủ tiền, giao dịch kết thỳc ngay.
public void transfer(int from, int to, double amount) {
System.out.print(Thread.currentThread()); accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount;
System.out.printf(" Số dư: %10.2f%n", getTotalBalance()); }
Sau đõy là đoạn code cho class transferRunnable: class TransferRunnable implements Runnable {
. . .
public void run() {
try {
int toAccount = (int) (bank.size() * Math.random()); double amount = maxAmount * Math.random(); bank.transfer(fromAccount, toAccount, amount); Thread.sleep((int) (DELAY * Math.random())); }
catch(InterruptedException e) {} }
}
Khi chương trỡnh chạy, ta khụng biết được trong mỗi tài khoản cũn bao nhiờu tiền nhưng tổng số tiền của tất cả cỏc tài khoản là khụng đổi. Chương trỡnh trờn chạy và khụng bao giờ dừng lại, ta phải ấn Ctrl + C để dừng nú.
Tuy nhiờn, trong một số tỡnh huống chương trỡnh trờn cú thể gõy lỗi và in ra tổng số tiền khỏc nhau, đú là khi cú nhiều hơn một tài khoản cựng chuyển tiền đến một tài khoản khỏc, tức là cựng thực hiện cõu lệnh:
Giả sử đõy là một hành vi đơn vị (atomic- hành vi chỉ chiếm một chu kỳ thực hiện lệnh của CPU), khi đú mỗi thread sẽ thực hiện phộp tớnh này một cỏch tuần tự mà khụng gõy ảnh hưởng vỡ trong một thời điểm, CPU cũng chỉ phục vụ được cho một thread.
Tuy nhiờn, phộp tớnh này lại khụng phải là atomic, cụng việc cú thể phải làm là: 1- Load giỏ trị account[to] vào thanh ghi
2- Cộng với account
3- Load kết quả ngược trả lại cho account[to]
Giả sử cú 2 thread là thread_1 và thread_2 cựng thực hiện đến dũng lệnh này. Thread_1 thực hiện xong bước 1 và 2 thỡ bị ngắt, sau đú thread_2 sẽ thực hiện cõu lệnh trờn với đầy đủ 3 bước tức là account[to] đó bị thay đổi giỏ trị. Tiếp theo, thread_1 thức dậy và làm tiếp bước 3, account[to] lỳc này đó ghi đố giỏ trị do thread_2 cập nhật. Điều này dẫn tới việc tổng số tiền bị thay đổi, phần tiền do thread_2 chuyển cho account[to] đó bị mất.
V.2 Khúa đối tượng
Trong vớ dụ trờn, nếu ta đảm bảo rằng phương thức transfer() được thực hiện thành cụng trước khi thread bị ngắt thỡ sẽ khụng cú vấn đề gỡ xảy ra. Cú nghĩa là, trong một thời điểm sẽ chỉ cú một thread được truy cập vào một đoạn mó lệnh nào đú.
Từ phiờn bản Java 5.0 cú hỗ trợ hai cơ chế để bảo vệ một khối lệnh khỏi sự truy cập đồng thời. Cỏc phiờn bản trước của Java sử dụng từ khúa synchronized, Java 5.0 giới thiệu thờm lớp ReentrantLock cho mục đớch này.
Cấu trỳc đoạn lệnh căn bản để sử dụng cơ chế này là: myLock.lock(); // đối tượng ReentrantLock
try { //Đoạn lệnh cần bảo vệ } finally {
myLock.unlock(); // đảm bảo khúa được mở kể cả khi exception xảy ra }
Đoạn lệnh này đảm bảo rằng chỉ cú một thread tại một thời điểm cú thể đi vào vựng được bảo vệ. Khi một thread đầu tiờn đó gọi lock thỡ khụng một thread nào sau đú cú thể vượt qua lệnh lock().
Bõy giờ chỳng ta sẽ khúa phương thức transfer() trong lớp Bank: public class Bank
{
public void transfer(int from, int to, int amount) {
bankLock.lock(); try
{
if (accounts[from] < amount) return; System.out.print(Thread.currentThread()); accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to); accounts[to] += amount;
} finally { bankLock.unlock(); } } . . .
private Lock bankLock = new ReentrantLock(); // ReentrantLock là một lớp cài đặt giao diện lock.
}
V.3 Đối tượng điều kiện
Trong trường hợp thread đi vào vựng bảo vệ, nú cần hoàn thành cụng việc để cú thể mở khúa cho cỏc thread khỏc đi vào. Tuy nhiờn, nếu trong đú cụng việc chớnh cần thực hiện bị giới hạn bởi một điều kiện nào đú, chẳng hạn ở đõy, việc gọi transfer() để chuyển tiền chỉ được thực hiện khi số tiền của account gửi lớn hơn số tiền được gửi:
if (bank.getBalance(from) >= amount) bank.transfer(from, to, amount);
Khi cú điều kiện này, một thread cú thể khụng thỏa món và nú khụng thực hiện việc chuyển tiền, tức là cụng việc của nú là khụng hữu ớch. Đến đõy, ta cú thể nghĩ ra một giải phỏp là để thread đợi cho đến khi nú cú đủ tiền để chuyển:
public void transfer(int from, int to, int amount) {
bankLock.lock(); try
{
while (accounts[from] < amount) // Đợi cho đến khi tài khoản của nú cú đủ tiền { // đợi . . . } // Chuyển tiền . . . } finally { bankLock.unlock(); } }
Việc đợi ở đõy sẽ kết thỳc khi một thread khỏc chuyển tiền vào cho nú. Tuy nhiờn ta thấy rằng vựng chứa lệnh chuyển tiền lại đang bị khúa bởi chớnh thread này nờn cỏc thread khỏc khụng thể chuyển tiền được. Kết quả là thread này khụng bao giờ thoỏt khỏi tỡnh trạng đợi, chương trỡnh rơi vào deadlock.
Đõy là tỡnh huống mà ta phải sử dụng đối tượng điều kiện: class Bank
{
{ . . .
sufficientFunds = bankLock.newCondition(); }
. . .
private Condition sufficientFunds; }
Nếu phương thức transfer() kiểm tra và thấy rằng nú chưa đủ tiền để chuyển thỡ nú sẽ gọi: sufficientFunds.await();Lỳc này nú sẽ nằm chờ thread khỏc chuyển tiền.
Cú một sự khỏc nhau căn bản giữa một thread đang đợi để khúa (gọi phương thức lock() để đi vào vựng bảo vệ) và thread gọi phương thức await(). Nú sẽ khụng thể mở khúa khi đang nằm trong vựng khúa cho đến khi một thread khỏc gọi signalAll(). Một thread thực hiện xong việc chuyển tiền, nú nờn gọi: sufficientFunds.signalAll() để mở khúa cho cỏc thread đang nằm đợi thỏa món điều kiện đủ tiền. Một khi thread đó thoỏt khỏi tỡnh trạng đợi do await() gõy ra, nú sẽ được chạy tiếp và cố gắng đi vào vựng khúa và bắt đầu từ vị trớ mà nú chờ đợi trước đú - vị trớ gọi phương thức await() và kiểm tra lại điều kiện.
Một phương thức await() nờn được gọi trong một vũng lặp đợi: while (!(đủ tiền))
condition.await();
Nếu khụng cú thread nào gửi signalAll() thỡ thread đang đợi khụng cú cỏch nào chạy tiếp được. Nếu tất cả cỏc thread khỏc đều đang bị chặn, thread gọi await() khụng giả phúng tỡnh trạng này cho một thread nào đú thỡ bản thõn nú cũng bị chặn, chương trỡnh sẽ bị treo.
Vấn đề là khi nào thỡ gọi signalAll()? Nguyờn tắc chung là khi một thread hoàn thành việc thay đổi trạng thỏi đối tượng thỡ nú nờn gọi signalAll(). Ở đõy, sau khi cộng tiền vào cho tài khoản nhận và thỡ signalAll() sẽ được gọi. Việc gọi này sẽ giỳp cỏc thread cũng đang trong tỡnh trạng chờ đợi này khụng bị chặn nữa và tiếp tục cụng việc với một giỏ trị tài khoản cú thể đó bị thay đổi.
public void transfer(int from, int to, int amount) {
bankLock.lock(); try
{
while (accounts[from] < amount) sufficientFunds.await(); // Chuyển tiền . . . sufficientFunds.signalAll(); } finally { bankLock.unlock(); } }
Chỳ ý là việc gọi signalAll() khụng phải là kớch hoạt thread bị chặn ngay lập tức. Nú chỉ giải phúng tỡnh trạng bị chặn của cỏc thread khỏc cho đến khi nú kết thỳc đoạn chương trỡnh được bảo vệ. Một phương thức khỏc là sign() chỉ trao khúa cho một thread xỏc định thay vỡ tất cả. Một thread chỉ cú thể gọi cỏc phương thức signalAll(), sign() và await() trong vựng được bảo vệ.
Chỳng ta cú thể kết thỳc phần này bằng một cõu chuyện minh họa: Cú 10 người cựng đến ngõn hàng để chuyển tiền cho nhau, mỗi người chuyển cho một người khỏc trong số 9 người cũn lại. Mỗi thời điểm ngõn hàng chỉ chuyển tiền cho một người, người được gửi tiền sẽ được vào trong ngõn hàng và giữ khúa cổng, 9 người khỏc phải chờ ngoài cổng. Nếu chẳng may người đang thực hiện chuyển số tiền lớn hơn anh ta cú, anh ta sẽ đợi ở trong ngõn hàng (gọi await()) và nộm khúa ra ngoài cổng cho tất cả 9 người kia. Một trong số 9 người ngoài cổng sẽ được đi vào trong ngõn hàng, thực hiện việc chuyển tiền. Sau khi chuyển xong, anh ta thụng bỏo cho tất cả cỏc anh khỏc đang phải đợi trong ngõn hàng là mỡnh vừa chuyển tiền (gọi signalAll()) rồi đi ra ngoài, sau đú những người đang đợi cũng quay ra cổng để đợi đến lượt vào.
Bài tập
1. Viết một chương trỡnh tạo một thread thực hiện việc giải một phương trỡnh bậc 2 với cỏc hệ số cho trước.
2. Viết chương trỡnh song song với 10 thread để cộng 10000 số tự nhiờn liờn tiếp từ 1 đến 10000.