Bài giảng Ngôn ngữ Java - Chương 6: Đa luồng - Phạm Duy Trung

Nội dung bài học

• Thread

• Vòng đời của thread

• Multithreading

• Xếp lịch chạy cho thread

• Thread safe

• Deadlock

• Lock và synchronized

• SwingWorker

Thread là gì

• Thread /θred/

• Thread là một tiến trình hạng nhẹ (lightweight process), là

luồng logic tuần tự các lệnh chương trình, với một điểm bắt đầu

và một điểm kết thúc

• Trong vòng đời của mình, thread chỉ được thực thi một lần duy

nhất

• Bản thân thread không phải là một chương trình, nó không chạy

độc lập mà nằm trong một chương trình hoàn chỉnh

pdf 65 trang kimcuc 6200
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Ngôn ngữ Java - Chương 6: Đa luồng - Phạm Duy Trung", để tải tài liệu gốc về máy hãy click vào nút Download ở trên

Tóm tắt nội dung tài liệu: Bài giảng Ngôn ngữ Java - Chương 6: Đa luồng - Phạm Duy Trung

Bài giảng Ngôn ngữ Java - Chương 6: Đa luồng - Phạm Duy Trung
ĐA LUỒNG
Multithreading
duytrung.tcu@gmail.com
Nội dung bài học
• Thread
• Vòng đời của thread
• Multithreading
• Xếp lịch chạy cho thread
• Thread safe
• Deadlock
• Lock và synchronized
• SwingWorker
duytrung.tcu@gmail.com
Thread là gì
• Thread /θred/
• Thread là một tiến trình hạng nhẹ (lightweight process), là
luồng logic tuần tự các lệnh chương trình, với một điểm bắt đầu
và một điểm kết thúc
• Trong vòng đời của mình, thread chỉ được thực thi một lần duy
nhất
• Bản thân thread không phải là một chương trình, nó không chạy
độc lập mà nằm trong một chương trình hoàn chỉnh
duytrung.tcu@gmail.com
Thread là gì
• Một chương trình có thể là đơn luồng (single-thread) hoặc đa
luồng (multi-thread)
• Đơn luồng: 1 điểm vào và 1 điểm ra
• Đa luồng: 1 điểm bắt đầu ở main(), sau đó là nhiều điểm vào
và nhiều điểm ra chạy song hành với main()
duytrung.tcu@gmail.com
Đa nhiệm (Multitasking / Multi-processing)
• Đa số các HĐH hiện nay là đa nhiệm
• Thực hiện đồng thời nhiều công việc dựa trên chia sẻ tài
nguyên: CPU, bộ nhớ, các kênh vào ra
• Với CPU đơn nhân: chỉ một tác vụ được thực hiện tại một thời
điểm, xếp lịch trên các khe thời gian (time slice)
duytrung.tcu@gmail.com
Đa nhiệm (Multitasking)
• Với CPU đa nhân: nhiều tác vụ có thể chạy song song trên các
CPU khác nhau
• Về cơ bản, có 2 dạng đa nhiệm:
1. Đa nhiệm hợp tác: mỗi tác vụ sử dụng tài nguyên hệ thống
cho đến khi thực hiện xong thì nhường cho tác vụ khác
2. Đa nhiệm ưu tiên: mỗi tác vụ được cấp một khe thời gian, 
nhường điều khiển cho tác vụ khác khi dùng hết tài nguyên
của mình
duytrung.tcu@gmail.com
Đa luồng (Multithreading)
• Xét trong một tiến trình (process) hay chương trình (program)
• Một tiến trình có bộ nhớ lệnh và các khối điều khiển riêng
• Tiến trình có thể chạy nhiều luồng để nâng cao hiệu quả
• Các luồng chạy trong ngữ cảnh của tiến trình, cùng chia sẻ các
tài nguyên được cấp cho tiến trình
• Các luồng chỉ chạy trong ngữ cảnh cụ thể của tiến trình, khi
chạy các luồng sử dụng stack, thanh ghi và bộ đếm chương
trình của riêng mình
duytrung.tcu@gmail.com
Tại sao sử dụng thread
• Tận dụng tối ưu hơn tài nguyên của hệ thống
 Khi một luồng đang bị đình chỉ, do đang chờ đọc dữ liệu từ một cổng
vào/ra, luồng khác có thể sử dụng CPU để tính toán, mang lại kết quả
thực hiện tốt hơn
• Giúp nâng cao hiệu quả tương tác của chương trình với người
sử dụng
 Khi soạn thảo văn bản, một thread sẽ đảm nhiệm khi ta ấn lưu file, 
trong quá trình lưu thread khác tiếp tục thực hiện cho người dùng nhập
văn bản -> giao diện người dùng đáp ứng tốt – Responsive UI
duytrung.tcu@gmail.com
Minh họa: Unresponsive UI
• Giao diện người dùng đáp ứng kém
• Chương trình đếm: 
- Đếm từ 1 đến 100.000.000, hiển thị số đếm lên JTextField
- chạy đếm khi ấn “Start”, dừng đếm khi ấn “Stop”. Các handler 
cho hai nút xử lý thông qua 1 cờ tên là stopFlag kiểu Boolean: 
khởi tạo là false, đặt true khi ấn “Stop”, false khi ấn “Start”
duytrung.tcu@gmail.com
Minh họa: Unresponsive UI
duytrung.tcu@gmail.com
btnStart.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
stopFlag = false;
for(int i=0; i<10000000;++i)
{
if(stopFlag) break;
tfCount.setText(count + "");
++count;
}
}
});
btnStop.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
stop = true; // set the stop flag
}
});
không thay đổi giá trị??
Tạo thread
• Trong Java, thread là đối tượng
• Tạo thread mới có thể sử dụng 2 cơ chế sau:
duytrung.tcu@gmail.com
class
interface
extends
implements
1 2
Runnable (java.lang.Runnable)
• Là interface mà các lớp có đặc tính thực thi được bởi các luồng
cần kế nhiệm (implements)
• Tạo ra một khuôn mẫu chung cho các đối tượng chạy đồng thời
• Các lớp kế nhiệm từ Runnable:
AsyncBoxView.ChildState, ForkJoinWorkerThread, FutureTask,
RenderableImageProducer, SwingWorker, Thread, TimerTask
• Phương thức kế nhiệm duy nhất: public void run();
logic thực hiện của đối tượng kế nhiệm sẽ đặt trong phương thức
này
duytrung.tcu@gmail.com
Lớp Thread (java.lang.Thread)
• Kế nhiệm từ Runnable
• Các hàm khởi tạo chính:
public Thread();
public Thread(String threadName);
public Thread(Runnable target);
public Thread(Runnable target, String threadName);
Hai hàm đầu được sử dụng khi tạo thread theo cơ chế kế thừa lớp Thread
Hai hàm sau được sử dụng khi tạo thread bằng một đối tượng kế nhiệm Runnable
duytrung.tcu@gmail.com
Tạo thead: với Thread
1. Tạo một lớp kế thừa Thread và override phương thức run()
2. Tạo một đối tượng từ lớp vừa tạo
3. Chạy thread khi cần bằng phương thức start()
duytrung.tcu@gmail.com
class MyThread extends Thread{
public void run(){
// logic thực thi thread tại đây
...
}
}
MyThread thr1 = new MyThread()
thr1.start();
Tạo thread: với Runnable
• Runnable là một interface có phương thức duy nhất là run()
1. Tạo một lớp kế nhiệm Runnable và cung cấp thân hàm cho
phương thức run()
duytrung.tcu@gmail.com
public interface Runnable
{
void run();
}
public class MyRunnable implements Runnable
{
public void run()
{
// logic thực thi thread tại đây
. . .
}
}
Tạo thread: với Runnable
2. Tạo ra một đối tượng từ lớp vừa tạo
3. Khởi tạo một đối tượng Thread và truyền vào đối tượng kiểu
Runnable ở trên
4. Gọi đến phương thức start để bắt đầu thực hiện thread
duytrung.tcu@gmail.com
Runnable r = new MyRunnable();
t.start();
Thread t = new Thread(r);
Demo 1
duytrung.tcu@gmail.com
class MyThread extends Thread
{
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run()
{
for(int i = 1; i <= 5; i++)
{
System.out.println(name+ ":" + i);
yield();
}
}
}
Demo 1
duytrung.tcu@gmail.com
public class Demo1 {
public static void main(String[] args) {
Thread[] threads = {
new MyThread("Thread 1"),
new MyThread("Thread 2"),
new MyThread("Thread 3")
};
for(Thread t : threads)
{
t.start();
}
}
}
run:
Thread 1:1
Thread 2:1
Thread 3:1
Thread 2:2
Thread 1:2
Thread 2:3
Thread 3:2
Thread 2:4
Thread 1:3
Thread 2:5
Thread 3:3
Thread 1:4
Thread 3:4
Thread 1:5
Thread 3:5
BUILD SUCCESSFUL (total time: 0 seconds)
* Không đoán trước được kết quả hiển thị, ta không kiểm soát
được hoàn toàn trình tự thực hiện các thread
Lớp Thread: Các phương thức
Tên phương thức Giải thích
public void start()
Bắt đầu chạy thread, JRE sẽ gọi đến phương thức run() của lớp, 
các thread đang chạy vẫn tiếp tục
public void run()
Chuỗi các lệnh thực thi khi chạy thread, khi run() hoàn thành, 
thread chấm dứt
public static sleep(long milisecs)
Đình chỉ thread hiện tại và nhường điều khiển cho thread khác
trong khoảng thời gian tính bằng mili-giây (cộng nano-giây). Cho 
phép thread hoạt động trở lại sớm hơn khoảng thời gian “ngủ” dự
kiến thông qua phương thức interrupt.
public static sleep(long millisecs, 
int nanosecs)
public void interrupt()
public static void yield()
Gợi ý cho trình xếp lịch (scheduler) rằng thread hiện tại có thể
nhường quyền điều khiển cho thread khác trong lịch chạy, tuy vậy
trình xếp lịch có thể bỏ qua gợi ý này
public boolean isAlive() Trả về false nếu thread chưa chạy, hoặc đã “chết” và ngược lại
public void setPriority(int p) Đặt mức ưu tiên cho thread
duytrung.tcu@gmail.com
Vòng đời của một thread
• Khi khởi tạo, thread ở trạng thái “NEW”
• Ở trạng thái này , thread mới chỉ là một đối tượng trong bộ nhớ heap, 
chưa được cấp tài nguyên gì
duytrung.tcu@gmail.com
Vòng đời của một thread
• Phương thức start() được gọi, thread chuyển sang trạng thái
“RUNNABLE”
• Thread được cấp tài nguyên cần thiết, được xếp lịch chạy, và gọi
phương thức run()
duytrung.tcu@gmail.com
Vòng đời của một thread
Thread rơi vào trạng thái “not RUNNABLE” trong các tình huống sau:
• Gọi sleep()
• Gọi wait() để chờ một số điều kiện được đáp ứng rồi mới thực hiện tiếp
• Bị “blocked” để chờ một thao tác vào /ra thực hiện xong
duytrung.tcu@gmail.com
Vòng đời của một thread
Từ “not RUNNABLE”, thread trở lại “RUNNABLE” trong các trường hợp:
• Thread “ngủ” được báo thức khi hết thời gian hoặc gọi interrupt()
• Thread đang chờ do gọi wait() thì notify() hoặc notifyAll() được gọi thông
báo các điều kiện cần thiết đã xong và có thể chạy tiếp
• Thao tác vào/ra được thực hiện xong, thread được “unblocked”
duytrung.tcu@gmail.com
Vòng đời của một thread
• Thread kết thúc hay ở trạng thái “TERMINATED” khi phương thức run() 
chạy xong và kết thúc
• isAlive() kiểm tra xem thread còn sống hay không
duytrung.tcu@gmail.com
isAlive() = true
isAlive() = false
Vòng đời của một thread
Phương thức getState() trả về trạng thái hiện tại của thread, 
có kiểu enum trong Thread.State, nhận một trong các giá trị
sau
 NEW
 RUNNABLE
 WAITING
 BLOCKED
 TIMED_WAITING
 TERMINATED
duytrung.tcu@gmail.com
Mức ưu tiên và xếp lịch cho thread
• JVM xếp lịch chạy cho các thread dựa trên mức ưu tiên
• Mỗi thread được gán một mức ưu tiên là một số nguyên trong
khoảng từ Thread.MIN_PRIORITY đến Thread.MAX_PRIORITY, 
Thread.NORM_PRIORITY là mức ưu tiên mặc định bằng 5
• Khi thread được tạo, nó kế thừa mức ưu tiên từ thread cha
• Đặt mức ưu tiên cho thread bằng phương thức
public void setPriority(int priority);
trong đó, int priority phụ thuộc vào JVM, có thể trong khoảng từ 1 đến 10
duytrung.tcu@gmail.com
Mức ưu tiên và xếp lịch cho thread
• Mức ưu tiên càng cao, càng được JVM ưu tiên chạy trước
• Với các thread cùng mức ưu tiên cao nhất, JVM xếp lịch cho
chúng theo thuật toán Round Robin
• Xét ưu tiên, nếu một thread có mức ưu tiên cao hơn trở nên
RUNNABLE, thread đang chạy có mức ưu tiên thấp hơn ngay
lập tức phải nhường điều khiển cho nó
• Nếu nhiều thread đang RUNNABLE và có cùng mức ưu tiên, một
thread có thể chiếm điều khiển cho đến khi chạy xong (tham ăn
- starvation). Có thể sử dụng sleep() hoặc yield() để phân
bổ điều khiển cho các thread khác nữa
duytrung.tcu@gmail.com
Mức ưu tiên và xếp lịch cho thread
Tóm lại, một thread sẽ duy trì chạy cho tới khi:
 Một thread mức ưu tiên cao hơn trở nên RUNNABLE
 Chủ động nhường điều khiển cho thread khác thông qua các phương thức
wait(), yield() và sleep()
 Thread kết thúc, cụ thể là run() kết thúc
 Trên các hệ điều hành có chia khe thời gian (time slicing), thread sẽ dừng khi
dùng hết định mức CPU dành cho nó
• Lưu ý quan trọng: xếp lịch ưu tiên cho thread phụ vào JVM, JVM
thông qua hệ điều hành để thực hiện đa luồng, không phải lúc nào
JVM cũng đảm bảo thread có mức ưu tiên cao nhất được chạy
trước, chẳng hạn thread có ưu tiên thấp hơn có thể được chạy để
ngăn starvation. Do đó, đừng quá tin tưởng vào mức ưu tiên khi thiết
kế thuật toán
duytrung.tcu@gmail.com
Nhóm thread – ThreadGroup
• Một số trường hợp đòi hỏi gom các thread có chức năng tương
tự nhau thành nhóm để tiện sử dụng
 Lấy ví dụ với một trình duyệt web, nếu nhiều thread đều đang thực hiện lấy một
bức ảnh trên internet về thì người dùng bấm Stop để ngừng quá trình tải trang
web lại; lúc này sẽ rất tiện lợi và an toàn nếu như có thể dừng tất cả các thread 
đang tải ảnh về cùng một lúc
• Xây dựng một ThreadGroup cho phép làm việc với một nhóm
các thread
String groupName = “Loading Image Group”;
// Tên nhóm phải là duy nhất
ThreadGroup g = new ThreadGroup(groupName);
duytrung.tcu@gmail.com
Nhóm thread – ThreadGroup
• Đưa thread vào threadGroup thông qua hàm khởi tạo:
Thread t = new Thread(g, threadName);
• Phương thức activeCount() trả về số thread còn có thể chạy
được
if(g.activeCount == 0){
// Tất cả các thread đều đã kết thúc}
• Để ngắt tất cả các thread trong một nhóm, gọi đến interrupt() 
cho cả nhóm
g.interrupt();
duytrung.tcu@gmail.com
Đồng bộ thread (Synchronization)
• Trong các ứng dụng đa luồng, một tình huống phổ biến là hai
thread trở lên cần tương tác với cùng đối tượng
• Điều gì xảy ra nếu các thread này đều thay đổi đối tượng theo
những chiều hướng khác nhau?
• Đồng bộ thread rất quan trọng, ngăn chặn tình trạng các thread 
“giẫm chân lên nhau”
duytrung.tcu@gmail.com
Minh họa khi không đồng bộ thread
Giả thiết ta xây dựng một ngân hàng, quản lý 100 tài khoản, mỗi tài
khoản ban đầu có 1000$. Các tài khoản chuyển một số tiền ngẫu
nhiên đến một tài khoản ngẫu nhiên trong nội bộ ngân hàng.
duytrung.tcu@gmail.com
100.000
Minh họa khi không đồng bộ thread
• Xây dựng lớp Bank:
 double[] accounts: mảng chứa số tiền của các tài khoản
• Hàm khởi tạo và các phương thức:
 public Bank(int n, double initialBalance) : n tài khoản, và số tiền ban 
đầu trong mỗi tài khoản
 public void transfer(int from, int to, double amount): phương
thức chuyển số tiền amount từ tài khoản from, đến tài khoản to 
 public double getTotalBalance(): phương thức trả về số tiền đang có trong
toàn ngân hàng, bằng tổng số tiền của tất cả các tài khoản
duytrung.tcu@gmail.com
Minh họa khi không đồng bộ thread
• Xây dựng mỗi tài khoản là một thread, mỗi thread liên tục chuyển tiền từ
tài khoản hiện tại sang tài khoản khác bằng phương thức transfer()
• In ra thông tin mỗi giao dịch và tính tổng số tiền trong toàn ngân hàng
bằng phương thức getTotalBalance(). Kết quả in ra có dạng như sau:
duytrung.tcu@gmail.com
Minh họa khi không đồng bộ thread
duytrung.tcu@gmail.com
accounts[to] += amount
1. Tải accounts[to] vào thành ghi
2. Cộng với amount
3. Trả kết quả về với accounts[to]
+500$
+900$
đúng phải là 6400, mất 900$!
Thread safe
• Một đoạn chương trình được gọi là thread safe – an toàn với đa
luồng – nếu nó hoạt động đúng khi được thực thi đồng thời bởi
nhiều thread
• Đáp ứng được nhiều thread sử dụng một dữ liệu chia sẻ: mảng
account trong ví dụ ngân hàng chẳng hạn
• “Đồng thời” xét trên khía cạnh hiệu quả thu được, còn mỗi thời
điểm chỉ có một thread sử dụng dữ liệu chia sẻ
• Rõ ràng phương thức transfer() không thread safe!
duytrung.tcu@gmail.com
• Vấn đề: phương thức transfer() có
thể bị cắt ngang bởi một thread
khác, hoặc thread hiện tại chưa
thực hiện xong thì đã hết tài nguyên
(khe thời gian chẳng hạn)
• Giải quyết: phải đảm bảo hoàn
thành phương thức rồi mới nhường
điều khiển cho thread khác
duytrung.tcu@gmail.com
Đồng bộ thread (Synchronization)
Đồng bộ thread (Synchronization)
2 cơ chế chính để bảo vệ một khu vực lệnh khỏi các truy cập
đồng thời:
1. Sử dụng Lock, cụ thể là lớp ReentrantLock, xuất hiện từ
JDK 5.0
2. Sử dụng từ khóa synchronized trước JDK 5.0
duytrung.tcu@gmail.com
Đồng bộ thread (Synchronization)
2 cơ chế chính để bảo vệ một khu vực lệnh khỏi các truy cập
đồng thời:
1. Sử dụng Lock, cụ thể là lớp ReentrantLock, xuất hiện từ
JDK 5.0
2. Sử dụng từ khóa synchronized trước JDK 5.0
duytrung.tcu@gmail.com
Lớp ReentrantLock (java.util.concurrent.locks)
• Dàn cảnh bảo vệ đoạn lệnh sử dụng ReentrantLock như sau:
• Khi có một thread “bấm khóa” đối tượng myLock, không thread nào
khác có thể xâm nhập vào đoạn lệnh chính
• Khi thread khác gọi đến phương thức lock(), thread này sẽ rơi vào
trạng thái BLOCKED, cho đến khi thread kia “mở khóa” myLock
duytrung.tcu@gmail.com
myLock.lock(); // a ReentrantLock object
try{
bank.transfer(from, to, amount);
}
finally
{
myLock.unlock(); // make sure the lock is unlocked even if 
// an exception is thrown
}
Vấn đề với kiểm tra điều kiện
• Trong ví dụ ngân hàng, ta mong muốn số dư của tài khoản nguồn phải
không nhỏ hơn lượng tiền cần chuyển:
• Kịch bản sau hoàn toàn có thể xảy ra: thread mới kiểm tra điều kiện xong, 
kết quả là hợp lệ, chưa chuyển tiền thì mất quyền điều khiển:
• Đến khi thread chạy lại, số dư tài khoản này lại nhỏ hơn số tiền muốn
chuyển, do đó ta cần đảm bảo rằng thread phải không bị gián đoạn trong
quá trình trong quá trình kiểm tra điều kiện và chuyển tiền
duytrung.tcu@gmail.com
if(bank.getBalance(from) >= amount)
bank.transfer(from, to, amount);
if(bank.getBalance(from) >= amount)
// Mới kiểm tra điều kiện xong thì đến phiên thread khác
bank.transfer(from, to, amount);
Vấn đề với kiểm tra điều kiện
• Cách giải quyết: bảo vệ toàn bộ đoạn lệnh trong transfer() như sau:
duytrung.tcu@gmail.com
public void transfer(int from, int to, double amount){
bankLock.lock();
try{
while(accounts[from] < amount){
// chờ tài khoản khác chuyển tiền vào cho đến khi
// số dư là hợp lệ
...
}
// chuyển tiền
...
}
finally{
bankLock.unlock();
}
}
Không khả thi do
thread hiện tại đã
chiếm quyền điều
khiển cho đến khi đối
tượng bankLock được
unlock!
Biến điều kiện – đối tượng Condition
• Condition (java.util.concurrent.locks) là một interface, đóng vai
trò như một phương tiện để dừng thread lại, cho đến khi được
một thread khác thông báo điều kiện đã đảm bảo để chạy tiếp
• Một đối tượng Condition luôn sinh từ một “khóa” cụ thể. Để
sinh một Condition, gọi phương thức newCondition() từ đối
tượng “khóa”
• Một “khóa” do đó có thể gắn với một hoặc nhiều đối tượng
Condition
• Đặt tên Condition sao cho mô tả được điều kiện mà nó đại diện
duytrung.tcu@gmail.com
Biến điều kiện – đối tượng Condition
Tên phương thức Mô tả
void await()
Đưa thread hiện tại về tình trạng chờ, cho đến khi được thread
khác báo hiệu (signal) hoặc bị ngắt (interrupt)
boolean await(long time, TimeUnit unit)
Tương tự như await(), thêm khoảng thời gian tối đa cho thread ở
tình trạng chờ
long awaitNanos(long nanosTimeout)
Tương tự như await(), thêm khoảng thời gian tối đa cho thread ở
tình trạng chờ tính bằng nano-giây
void awaitUninterruptibly()
Đưa thread về tình trạng chờ cho đến khi được thread khác báo
hiệu
boolean awaitUntil(Date deadline) Đưa thread về tình trạng chờ đến hết thời hạn deadline
void signal() Đánh thức một thread đang chờ
void signalAll() Đánh thức tất cả các thread đang chờ
duytrung.tcu@gmail.com
Biến điều kiện – đối tượng Condition
• Trở lại ví dụ ngân hàng, ta sinh ra một đối tượng Condition để
đặc trưng cho điều kiện “số dư tài khoản hợp lệ”
• Khi khởi tạo đối tượng Bank, khởi tạo đối tượng Condition:
• Khi kiểm tra thấy số dư tài khoản không hợp lệ, ta dừng thread 
để chờ đến khi đủ số dư bằng phương thức await()
duytrung.tcu@gmail.com
private Condition sufficientFunds; 
sufficientFunds = bankLock.newCondition();
while(accounts[from] < amount){
sufficientFunds.await(); 
...
}
Biến điều kiện – đối tượng Condition
• Thread hiện tại sẽ rơi vào trạng thái BLOCKED, đưa vào trong danh
sách đợi (waitset), và nhả “khóa” cho thread khác
• Trạng thái BLOCKED này khác với ngữ cảnh khi chờ khóa: thread
tiếp tục BLOCKED kể cả khi “khóa” sẵn sàng trở lại, nó chỉ
UNBLOCKED khi một thread khác gọi đến signalAll() của cùng
đối tượng Condition
• Lúc này thread trở lại RUNNABLE, được xếp lịch chạy, và sẵn sàng
nhận khóa khi có thể
duytrung.tcu@gmail.com
sufficientFunds.signalAll();
Biến điều kiện – đối tượng Condition
• Khi nhận được “khóa”, thread tiếp tục tại vị trí cuối cùng khi nó
rời đi: trở lại từ await()
• Tại thời điểm này, rõ ràng thread nên kiểm tra lại điều kiện, bởi
dễ hiểu điều kiện được đánh giá là đúng chỉ tại thời điểm gọi
signalAll(), còn hiện tại thì chưa biết
• Do vậy, một kinh nghiệm là nên gọi await() từ đối tượng điều
kiện trong một vòng lặp
duytrung.tcu@gmail.com
while(accounts[from] < amount){
sufficientFunds.await(); 
...
}
Deadlock
• Deadlock là hiện tượng hai hoặc nhiều thread chờ nhau nhả
khóa và do đó bị “tắc” mãi mãi
duytrung.tcu@gmail.com
Deadlock
• Khi thread gọi await(), “số phận” của nó phụ thuộc vào các
thread còn lại: nếu không thread nào gọi signalAll(), nó sẽ
vĩnh viễn không chạy trở lại nữa→ deadlock
• Khi các thread đều đang block, còn lại thread cuối cùng gọi
await() mà không unblock bất kỳ thread nào, do đó tất cả đều
BLOCKED, chương trình “treo”!!→ deadlock
• Kinh nghiệm: gọi signalAll() mỗi khi trạng thái của đối
tượng thay đổi theo chiều hướng có lợi cho các thread đang
chờ
duytrung.tcu@gmail.com
Deadlock
• Ở ví dụ ngân hàng, mỗi khi số dư tài khoản được cập nhật, các
thread đang chờ cần được trao cơ hội để kiểm tra xem số dư tài
khoản của mình đã hợp lệ chưa, bằng cách gọi signalAll()
duytrung.tcu@gmail.com
public void transfer(int from, int to, double amount){
bankLock.lock();
try{
while(accounts[from] < amount){
sufficientFunds.await();
// chuyển tiền
sufficientFunds.signalAll();
}
finally{
bankLock.unlock();
}
}
Deadlock
• Phương thức signal() chỉ đánh thức một thread duy nhất, 
được chọn ngẫu nhiên, trong danh sách đợi
• signal() rõ ràng có hiệu năng tốt hơn signalAll(), tuy vậy
tiềm ẩn một rủi ro: nếu như thread được đánh thức chạy tiếp
song vẫn chưa đạt điều kiện, nó sẽ về trạng thái BLOCKED để
chờ tiếp. Nếu như không có thread nào khác gọi đến signal()
nữa, chương trình “treo”→ deadlock
duytrung.tcu@gmail.com
Tổng kết về Lock và Condition
• Một khóa bảo vệ một khu vực code, chỉ cho phép một thread
được thực thi code tại mỗi thời điểm
• Một khóa chịu trách nhiệm điều hành các thread đang muốn
thực thi khu vực code được bảo vệ
• Một khóa có thể liên kết với một hoặc nhiều biến điều kiện
Condition
• Mỗi biến điều kiện chịu trách nhiệm điều hành các thread đã
xâm nhập vào khu vực code bảo vệ song không thể chạy tiếp
do điều kiện chưa đáp ứng
duytrung.tcu@gmail.com
Đồng bộ thread (Synchronization)
2 cơ chế chính để bảo vệ một khu vực lệnh khỏi các truy cập
đồng thời:
1. Sử dụng Lock, cụ thể là lớp ReentrantLock, xuất hiện từ
JDK 5.0
2. Sử dụng từ khóa synchronized trước JDK 5.0
duytrung.tcu@gmail.com
Từ khóa synchronized
• Trước Lock và Condition, Java sử dụng một cơ chế tương tranh
khác
• Mỗi đối tượng Java đều có một “khóa ẩn” (implicit lock)
• Nếu một phương thức được khai bao với từ khóa
synchronized, khóa của đối tượng sẽ bảo vệ cho toàn bộ
phương thức
• Do vậy, để gọi được phương thức, thread phải giành được khóa
của đối tượng chứa phương thức
duytrung.tcu@gmail.com
Cơ chế Lock và cơ chế synchronized
• “Khóa ẩn” của đối tượng chỉ có một “điều kiện ẩn” gắn kèm
duytrung.tcu@gmail.com
public synchronized void method()
{
// method body
}
public void method()
{
implicitLock.lock();
try{
// method body
}
finally{
implicitLock.unlock();
}
}
wait() ↔ implicitCondition.await()
notify() ↔ implicitCondition.signal()
notifyAll() ↔ implicitCondition.signalAll()
Sử dụng synchronized cho một khối lệnh
• Khai báo một đối tượng Object chỉ để tận dụng “khóa ẩn” của
nó
duytrung.tcu@gmail.com
class Bank{
private Object lock = new Object();
private double accounts[];
public void transfer(int from, int to, double amount){
synchronized(lock){
accounts[from] -= amount;
accounts[to] += amount;
}
...
}
...
Từ khóa volatile
• Đôi khi, việc đồng bộ thread sử dụng lock hay synchronized sẽ
lãng phí khi ta chỉ đọc ghi đến một, hai thuộc tính của đối tượng
• Từ khóa volatile cung cấp một cơ chế không cần khóa để
đồng bộ việc truy cập đến một thuộc tính của đối tượng
Ví dụ một đối tượng có một cờ boolean tên done có thể được được set bằng thread này,
get bằng thread khác:
duytrung.tcu@gmail.com
private boolean done;
public synchronized boolean isDone()
{
return done;
}
private volatile boolean done;
public boolean isDone()
{
return done;
}
Tổng kết về truy cập đồng thời
Truy cập đồng thời đến một thuộc tính là an toàn trong các
trường hợp sau:
• Thuộc tính là volatile
• Thuộc tính là final, và truy cập diễn ra sau khi nó đã được
khởi tạo
• Truy cập đến thuộc tính được bảo vệ bởi một khóa
duytrung.tcu@gmail.com
Thread trên Swing
Một ứng dụng Swing chạy trên nhiều thread, chia làm 3 loại:
1. Main thread, là thread chạy phương thức main(), bắt đầu và
kết thúc GUI
2. Event-dispatching thread (EDT)
3. Một số thread nền
duytrung.tcu@gmail.com
Thread trên Swing
• Mọi thao tác xử lý sự kiện, vẽ và hiển thị đều thực hiện ở EDT,
nhằm đảm bảo:
 Các sự kiện được xử lý lần lượt, kết thúc cái này mới đến cái khác
 Quá trình vẽ không bị ngắt bởi các sự kiện
• Nếu EDT bị chiếm bởi bởi một tác vụ đòi hỏi khối lượng tính
toàn lớn, thì giao diện sẽ “đóng băng”!
• Kinh nghiệm cho thấy những tác vụ nào yêu cầu thời gian thực
hiện từ 30 mili-giây trở lên thì không nên chạy trên EDT
• Code tương tác với các component nên chạy trên EDT, vì nhiều
component không thread safe!
duytrung.tcu@gmail.com
Thread trên Swing
Tóm lại:
• Các tác vụ tốn thời gian hoặc tác vụ IO không nên chạy trên
EDT, nếu không sẽ ảnh hưởng đến tính đáp ứng của giao diện
• Các component của Swing chỉ nên được sử dụng trong EDT để
đạt được thread safe
duytrung.tcu@gmail.com
invokeLater() và invokeAndWait()
• javax.swing.SwingUtilities
• invokeLater(Runnable) và invokeAndWait(Runnable)
đặt một tác vụ Runnable vào chạy trong EDT
• Để ngăn chặn các vấn đề xảy ra giữa main thread và EDT, sử
dụng invokeLater(Runnable) để tạo các component trong
EDT, thay vì main thread
• Gọi invokeLater() trong bất cứ thread nào để yêu EDT chạy
đoạn code trong phương thức run() của đối tượng Runnable
• Giống với java.awt.EventQueue.invokeLater()
duytrung.tcu@gmail.com
invokeLater() và invokeAndWait()
• invokeAndWait() chờ EDT chạy xong đoạn code cụ thể mới
trả về, hay được sử dụng trong init() của JApplet
duytrung.tcu@gmail.com
/** The entry main() method */
public static void main(String args[]) {
// Run the GUI codes on the event-dispatching thread for thread-safety
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("My Swing Application");
frame.setContentPane(new MyMainPanel());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null); // center on screen
frame.setVisible(true); // show it
}
});
}
SwingWorker
• Là lớp trừu tượng, tạo kết nối giữa EDT với một số thread chạy
nền
• Sử dụng để xếp các tác vụ tính toán lớn vào thread chạy nền và
trả kết quả trung gian hoặc kết quả cuối cùng về cho EDT
• Khai báo lớp của SwingWorker như sau:
public abstract class SwingWorker implements RunnableFuture
trong đó: T chỉ kiểu kết quả trả về của phương thức doInBackground() và get()
V chỉ kiểu kết quả trả về của các phương thức publish() và process()
RunnableFuture là kết hợp của 2 interface: Runnable và Future.
Runnable có run(), Future có get(), cancel(), isDone() và isCancelled()
duytrung.tcu@gmail.com
SwingWorker
duytrung.tcu@gmail.com
Tên phương thức Mô tả
protected T doInBackground() throws Exception Thực thi tác vụ nào đó trong thread chạy nền
protected void done() Làm gì trên EDT sau khi doInBackground() chạy xong
public final T get() throws InterruptedException, 
ExecutionException
Chờ doInBackground() chạy xong và lấy kết quả
Gọi get() trong EDT sẽ dừng mọi sự kiện lại, kể cả vẽ lại,
cho đến khi SwingWorker chạy xong
public final void execute() Bắt đầu thực hiện tác vụ trong SwingWorker
public final boolean cancel(boolean
mayInterruptIfRunning)
Cố gắng hủy bỏ tác vụ đang được SwingWorker thực hiện
public final boolean isDone() Trả về true nếu tác vụ đã được hoàn thành
public final boolean isCancelled() Trả về true nếu tác vụ đã bị hủy trước khi hoàn thành

File đính kèm:

  • pdfbai_giang_ngon_ngu_java_chuong_6_da_luong_pham_duy_trung.pdf