III.1 Độ ưu tiờn của thread
Mỗi Thread cú một độ ưu tiờn. Mặc định thỡ thread thừa kế độ ưu tiờn từ thread cha của nú- là thread đó gọi nú. Ta cú thể tăng hoặc giảm độ ưu tiờn của bất kỳ thread nào với phương thức setPriority() bằng một giỏ trị nằm giữa MIN_PRIORITY(1) và MAX_PRIORITY (10).
Cỏc thread cú độ ưu tiờn càng cao thỡ khả năng thực hiện càng cao.
III.2 Nhúm thread
Cỏc thread cú thể được đưa vào cựng một nhúm để cú thể đồng thời làm việc với cả nhúm thread này.
Khai bỏo một nhúm Thread: String groupName = . . .;
ThreadGroup g = new ThreadGroup(groupName)
Sau đú, với mỗi thread được tạo ra ta dựng constructor cú khai bỏo nhúm thread: Thread t = new Thread(g, threadName);
Để kiểm tra xem cú thread nào trong nhúm g cũn hoạt động khụng, ta dựng phương thức: g. activeCount();
Để ngắt tất cả cỏc thread trong nhúm g ta gọi: g.interrupt();
III.3 Quản lý cỏc ngoại lệ của thread
Phương thức run() khụng thể nộm ra một ngoại lệ được kiểm soỏt mà bị kết thỳc bởi một ngoại lệ khụng kiểm soỏt được mỗi khi cú lỗi. Khụng cú mệnh đề catch nào được thực hiện mà thay vào đú, trước khi thread bị dead, ngoại lệ này sẽ được gửi cho một bộ xử lý ngoại lệ khụng được kiểm soỏt.
Bộ xử lý này phải thuộc một lớp cú cài đặt giao diện Thread.UncaughtExceptionHandler chỉ với 1 phương thức:
void uncaughtException(Thread t, Throwable e);
Từ phiờn bản Java 1.5, ta cú thể cài bộ xử lý ngoại lệ cho bất cứ thread nào bằng phương thức: setUncaughtExceptionHandler(). Mgoaif ra ta cú thể cài bộ xử lý ngoại lệ mặc định cho tất cả cỏc thread bằng cỏch gọi phương thức setDefaultUncaughtExceptionHandler của lớp Thread.
Nếu khụng cài bộ xử lý ngoại lệ cho một thread thỡ mặc định là null. Tuy nhiờn, cỏc thread riờng lại phụ thuộc vào bộ xử lý của nhúm thread chứa nú. Khi này, phương thức uncaughtException sẽ làm cỏc việc sau:
1- Nếu nhúm thread cú cài đặt uncaughtException thỡ phương thức đú được gọi.
2- Ngược lại, nếu phương thức Thread.getDefaultExceptionHandler trả về một bộ xử lý khụng phải là null thỡ bộ xử lý này được gọi.
3- Nếu là null, nếu Throwable là một đối tượng của TheadDead, khụng cú gỡ xảy ra. 4- Nếu khụng, tờn của thread và stack trace được gửi tới System.err và đưa ra màn hỡnh.
IV. Điều khiển cỏc thread
Sau khi đó khởi động được một thread rồi, vấn đề tiếp theo sẽ là điều khiển thread.
IV.1 Interrupt một thread
Khi sử dụng phương thức Thread.sleep(int) thỡ chương trỡnh thường phải bắt cỏc ngoại lệ để xử lý. Lý do là thread bị dừng lại trong một khoảng thời gian lõu và trong khoảng thời gian đú nú khụng thể tự đỏnh thức nú được. Tuy nhiờn, nếu một thread cần phải được đỏnh thức sớm hơn, ta cú thể ngắt nú dựng phương thức interrupt().
public class SleepyHead extends Thread {
// Phương thức được chạy khi thread khởi động lần đầu public void run()
{
System.out.println ("Thread đang ngủ. Hóy đỏnh thức nú"); try
{
// Ngủ trong 8 tiếng
Thread.sleep( 1000 * 60 * 60 * 8 );
System.out.println ("Đú là một giấc ngủ trưa"); }
catch (InterruptedException ie) {
System.err.println ("Chỉ mới được hơn 5 phỳt thụi...."); }
}
// Phương thức main tạo và bắt đầu thread public static void main(String args[]) throws java.io.IOException
{
// Tạo một thread
// bắt đầu thread sleepy.start();
// Nhắc người dựng để dừng thread
System.out.println ("Nhấn Enter để ngưng thread"); System.in.read();
// Ngắt thread sleepy.interrupt(); }}
Cỏch thực hiện ở đõy là gửi một thụng điệp từ một thread khỏc để đỏnh thức một thread đang ngủ. Ở đõy, thread chớnh- hàm main- sẽ đợi cho người dựng ấn enter, sau đú gửi một thụng điệp ngắt tới thread đang ngủ.
IV.2 Dừng một thread
Đụi khi ta muốn một thread dừng lại trước khi nhiệm vụ của nú hoàn thành. Một thread (gọi là thread điều khiển) cú thể gửi một thụng điệp dừng tới một thread khỏc bằng việc gọi phương thức Thread.stop(). Điều này yờu cầu thread điều khiển phải giữ một tham chiếu tới thread muốn dừng.
public class StopMe extends Thread {
// Phương thức được thực hiện khi thread khởi động lần đầu public void run()
{
int count = 1;
System.out.println ("Thread đếm!"); for (;;)
{
// In ra biến count và tăng giỏ trị cho nú System.out.print (count++ + " "); // Ngủ nửa giõy
try { Thread.sleep(500); } catch (InterruptedException ie) {} }}
// phương thức main
public static void main(String args[]) throws java.io.IOException {
// Tạo và bắt đầu thread
Thread counter = new StopMe(); counter.start(); // Nhắc người dựng System.out.println ("Nhấn Enter để dừng đếm"); System.in.read(); // Dừng thread counter.stop(); }}
IV.3 Tạm dừng và phục hồi một thread
Cỏc phương thức suspend() dựng để tạm dừng một thread trong khi resume() dựng để tiếp tục một thread đó bị suspend. Tuy nhiờn sử dụng cỏc thread này rất hay gõy ra tỡnh trạng deadlock do thread bị suspend đang chiếm giữ một tài nguyờn và khụng giải phúng trong khi nhiều thread khỏc cú thể đang đợi sử dụng tài nguyờn đú.
IV.4 Giải phúng thời gian cho CPU
Khi một thread rơi vào trạng thỏi đợi một sự kiện xảy ra hoặc đi vào một vựng mó lệnh mà nếu giải phúng thời gian cho CPU sẽ nõng cao hiệu quả của hệ thống. Trong trường hợp này, thread nờn giải phúng thời gian hệ thống hơn là dựng sleep() để nghỉ trong thời gian dài. Để làm điều này ta dựng phương thức static yield() của Thread để giải phúng thời gian CPU cho thread hiện thời. Ta khụng thể giải phúng thời gian CPU của bất kỳ thread nào mà chỉ đối với thread hiện thời.
IV.5 Đợi một thread kết thỳc cụng việc
Đụi khi ta cần đợi cho một thread kết thỳc một cụng việc nào đú như gọi một phương thức hay đọc một thuộc tớnh thành phần,…Ta dựng phương thức isAlive() để xỏc định thread cũn chạy khụng. Tuy nhiờn, nếu thường xuyờn dựng phương thức này để kiểm tra sau đú dựng sleep() hoặc yield() thỡ hiệu quả sử dụng CPU sẽ rất thấp. Một cỏch hiệu quả là gọi phương thức joint() để đợi một thread kết thỳc cụng việc.
public class WaitForDeath extends Thread {
// Phương thức run() public void run() {
System.out.println ("thread chuẩn bị nghỉ...."); // Nghỉ trong 5 giõy
try {
Thread.sleep(5000); }
catch (InterruptedException ie) {} }
// phương thức main
public static void main(String args[]) throws java.lang.InterruptedException
{
// Tạo thread
Thread dying = new WaitForDeath(); dying.start();
// Nhắc user
System.out.println ("Đợi cho thread kết thỳc"); dying.join();
V. Đồng bộ thread
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.
Phụ lục A. Cỏc từ khúa của Java
Từ khúa í nghĩa
abstract Một lớp hoặc phương thức trừu tượng
assert Được sử dụng để định vị lỗi nội bộ chương trỡnh boolean Kiểu boolean
break Thoỏt khỏi lệnh switch hoặc cỏc vũng lặp byte Số nguyờn 1 byte
case Một lựa chon của switch
catch Một mệnh đề của khối Try để bắt một ngoại lệ char Kiểu ký tự Unicode
class Định nghĩa một class const Khụng được sử dụng
continue Tiếp tục tại vị trớ cuối vũng lặp hiện thời default Mệnh đề mặc định của switch
do Phần trờn của vũng lặp do/while double Kiểu số thực floating-number 8 byte else Mệnh đề else của một lệnh if
extends Xỏc định lớp cha của một lớp
final Một hằng số, hoặc một lớp hay phương thức khụng thể bị khai bỏo chống. finally Một phần của khối try mà luụn được thực hiện
float Số thực 4 byte for Một kiểu vũng lặp
goto Khụng sử dụng
if Một lệnh điều khiển rẽ nhỏnh
implements Chỉ ra cỏc interfaces mà lớp sẽ cài đặt.