Bài giảng Kỹ thuật phần mềm - Chương 6: Đa luồng

pdf 65 trang Hùng Dũng 04/01/2024 220
Bạn đang xem 20 trang mẫu của tài liệu "Bài giảng Kỹ thuật phần mềm - Chương 6: Đa luồng", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfbai_giang_ky_thuat_phan_mem_chuong_6_da_luong.pdf

Nội dung text: Bài giảng Kỹ thuật phần mềm - Chương 6: Đa luồng

  1. ĐA LUỒNG Multithreading duytrung.tcu@gmail.com
  2. 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
  3. 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
  4. 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
  5. Đ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
  6. Đ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
  7. Đ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
  8. 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
  9. 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
  10. Minh họa: Unresponsive UI btnStart.addActionListener(new ActionListener() { btnStop.addActionListener(new ActionListener() { @Override @Override public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent evt) { stopFlag = false; stop = true; // set the stop flag for(int i=0; i<10000000;++i) } { }); if(stopFlag) break; tfCount.setText(count + ""); ++count; } không thay đổi giá trị?? } }); duytrung.tcu@gmail.com
  11. Tạo thread • Trong Java, thread là đối tượng • Tạo thread mới có thể sử dụng 2 cơ chế sau: 1 2 class interface extends implements duytrung.tcu@gmail.com
  12. 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
  13. 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
  14. Tạo thead: với Thread 1. Tạo một lớp kế thừa Thread và override phương thức run() class MyThread extends Thread{ public void run(){ // logic thực thi thread tại đây } } 2. Tạo một đối tượng từ lớp vừa tạo MyThread thr1 = new MyThread() 3. Chạy thread khi cần bằng phương thức start() thr1.start(); duytrung.tcu@gmail.com
  15. Tạo thread: với Runnable • Runnable là một interface có phương thức duy nhất là run() public interface Runnable { void 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() public class MyRunnable implements Runnable { public void run() { // logic thực thi thread tại đây . . . } } duytrung.tcu@gmail.com
  16. Tạo thread: với Runnable 2. Tạo ra một đối tượng từ lớp vừa tạo Runnable r = new MyRunnable(); 3. Khởi tạo một đối tượng Thread và truyền vào đối tượng kiểu Runnable ở trên Thread t = new Thread(r); 4. Gọi đến phương thức start để bắt đầu thực hiện thread t.start(); duytrung.tcu@gmail.com
  17. Demo 1 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(); } } } duytrung.tcu@gmail.com
  18. Demo 1 public class Demo1 { run: public static void main(String[] args) { Thread 1:1 Thread[] threads = { Thread 2:1 Thread 3:1 new MyThread("Thread 1"), Thread 2:2 new MyThread("Thread 2"), Thread 1:2 new MyThread("Thread 3") Thread 2:3 }; Thread 3:2 for(Thread t : threads) Thread 2:4 { Thread 1:3 t.start(); Thread 2:5 } Thread 3:3 } Thread 1:4 Thread 3:4 } Thread 1:5 Thread 3:5 * 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 BUILD SUCCESSFUL (total time: 0 seconds) duytrung.tcu@gmail.com
  19. Lớp Thread: Các phương thức Tên phương thức Giải thích Bắt đầu chạy thread, JRE sẽ gọi đến phương thức run() của lớp, public void start() các thread đang chạy vẫn tiếp tục Chuỗi các lệnh thực thi khi chạy thread, khi run() hoàn thành, public void run() 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 public static sleep(long millisecs, trong khoảng thời gian tính bằng mili-giây (cộng nano-giây). Cho int nanosecs) 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 void interrupt() Gợi ý cho trình xếp lịch (scheduler) rằng thread hiện tại có thể public static void yield() 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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 isAlive() = true isAlive() = false duytrung.tcu@gmail.com
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. Đồ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
  32. 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. 100.000 duytrung.tcu@gmail.com
  33. 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
  34. 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
  35. Minh họa khi không đồng bộ thread accounts[to] += amount 1. Tải accounts[to] vào thành ghi +500$ 2. Cộng với amount 3. Trả kết quả về với accounts[to] +900$ đúng phải là 6400, mất 900$! duytrung.tcu@gmail.com
  36. 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
  37. Đồng bộ thread (Synchronization) • 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
  38. Đồ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
  39. Đồ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
  40. Lớp ReentrantLock (java.util.concurrent.locks) • Dàn cảnh bảo vệ đoạn lệnh sử dụng ReentrantLock như sau: 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 } • 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
  41. 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: if(bank.getBalance(from) >= amount) bank.transfer(from, to, amount); • 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: 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); • Đế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
  42. 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: 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 Không khả thi do thread hiện tại đã } chiếm quyền điều finally{ khiển cho đến khi đối bankLock.unlock(); tượng bankLock được } unlock! } duytrung.tcu@gmail.com
  43. 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
  44. Biến điều kiện – đối tượng Condition Tên phương thức Mô tả Đưa thread hiện tại về tình trạng chờ, cho đến khi được thread void await() khác báo hiệu (signal) hoặc bị ngắt (interrupt) Tương tự như await(), thêm khoảng thời gian tối đa cho thread ở boolean await(long time, TimeUnit unit) tình trạng chờ Tương tự như await(), thêm khoảng thời gian tối đa cho thread ở long awaitNanos(long nanosTimeout) tình trạng chờ tính bằng nano-giây Đưa thread về tình trạng chờ cho đến khi được thread khác báo void awaitUninterruptibly() 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
  45. 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ệ” private Condition sufficientFunds; • Khi khởi tạo đối tượng Bank, khởi tạo đối tượng Condition: sufficientFunds = bankLock.newCondition(); • 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() while(accounts[from] < amount){ sufficientFunds.await(); } duytrung.tcu@gmail.com
  46. 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 sufficientFunds.signalAll(); • 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
  47. 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 while(accounts[from] < amount){ sufficientFunds.await(); } duytrung.tcu@gmail.com
  48. 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
  49. 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
  50. 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() 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(); } } duytrung.tcu@gmail.com
  51. 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
  52. 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
  53. Đồ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
  54. 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
  55. 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 public void method() { implicitLock.lock(); public synchronized void method() try{ { // method body // method body } } finally{ implicitLock.unlock(); } } wait() ↔ implicitCondition.await() notify() ↔ implicitCondition.signal() notifyAll() ↔ implicitCondition.signalAll() duytrung.tcu@gmail.com
  56. 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ó 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; } } duytrung.tcu@gmail.com
  57. 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: private boolean done; private volatile boolean done; public synchronized boolean isDone() public boolean isDone() { { return done; return done; } } duytrung.tcu@gmail.com
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. invokeLater() và invokeAndWait() / 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 } }); } • 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
  64. 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
  65. SwingWorker 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 Chờ doInBackground() chạy xong và lấy kết quả public final T get() throws InterruptedException, Gọi get() trong EDT sẽ dừng mọi sự kiện lại, kể cả vẽ lại, ExecutionException 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 Cố gắng hủy bỏ tác vụ đang được SwingWorker thực hiện mayInterruptIfRunning) 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 duytrung.tcu@gmail.com