Giới thiệu
Trong cách thức lập trình truyền thống, sau khi viết xong mã nguồn chương trình sẽ được biên dịch thành một dạng mã mà máy tính có thể hiểu được (machine code). Mã này được khối xử lý trung tâm (CPU) xử lý, chương trình được xử lý một cách tuần tự.
Thời gian thực thi các câu lệnh có thể khác nhau, nhưng nếu một câu lệnh chưa được thực hiện xong thì các câu lệnh khác sẽ không được chạy, đó gọi là xử lý đơn luồng (Single-Threaded).
Ưu điểm của lập trình đơn luồng (Single-Thread programming) là đơn giản, nếu một câu lệnh không được hoàn thành thì câu lệnh khác sẽ không được thực thi. Điều này giúp cho các bạn dễ dàng tìm kiếm và biết được các lỗi phát sinh ở đâu.
Tuy nhiên, trong thực tế người sử dụng máy tính luôn có nhu cầu hệ thống của họ có thể làm được nhiều việc cùng lúc. Họ có thể vừa làm việc với phần mềm xử lý văn bản, trong khi các phần mềm khác tải file, quản lý hàng đợi in ấn và nghe nhạc. Ngay cả một ứng dụng đơn cũng có thể làm được nhiều việc một lúc. Từ đó, dẫn đến khái niệm Lập trình xử lý đa luồng công việc (Multi-Threaded).
Với lập trình đa luồng (Mutil-Thread programming), các lập trình viên phải nhìn nhận một cách khác về phần mềm. Thay vì
thực hiện một loạt các câu lệnh một cách tuần tự, thì chúng ta có thể thực hiện nhiều câu lệnh, nhiều nhiệm vụ đồng thời. Các câu lệnh được thực hiện cùng một lúc, chứ không phải câu lệnh này thực hiện xong câu lệnh kia mới thực hiện. Một ứng dụng đa luồng có thể thực hiện được nhiều nhiệm vụ trong cùng một thời điểm, cùng một không gian bộ
nhớ, và các luồng có thể cho phép chia sẻ các biến dữ liệu để cùng xử lý.
Có nhiều cách giải quyết vấn đề này, trong bài này sẽ giới thiệu một giải pháp là sử dụng Thread, mỗi thread sẽ thực thi một công việc và thực thi song song với các công việc còn lại.
Lập trình đa luồng
Java cũng giống như nhiều ngôn ngữ lập trình cao cấp khác, hỗ trợ tốt lập trình đa luồng. Trong Java, chúng ta có thể tạo ra các luồng bằng 2 cách:
- Tạo ra các thread bằng cách sử dụng lớp java.lang.Thread
- Tạo ra các thread thông qua interface Runable
Tạo một ứng dụng Multi-Thread với lớp Thread
Lớp Java.lang.Thread cung cấp các phương thức:
- Start
- Suspend
- Resmune
- Stop
Cách đơn giản nhất là kế thừa lớp java.lang.Thread và ghi đè lên phương thức run()
Thread sẽ không được tự động kích hoạt, muốn kích hoạt một thread ta phải sử dụng phương thức start()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import java.io.*; //This class extends Thread class WriteThread extends Thread { // This method is called when the thread runs // Overrided method public void run() { PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out)); pw.println("hello!"); pw.close(); } } |
1 2 3 4 5 6 7 8 9 |
public class JThread { public static void main(String[] args) { // Create and start the thread Thread thread = new WriteThread(); thread.start(); } } |
Sau khi phương thức start() của một thread được gọi, thì thread này sẽ gửi một yêu cầu để tạo ra thread riêng biệt, sau đó phương thức run được xử lý. Nếu trong chương trình ta tạo ra nhiều thread, có thể sử dụng dòng lệnh Thread.sleep(k) để khai báo thời gian chờ để các thread được xử lý, thời gian chờ càng lớn thì việc xử lý kết quả càng lâu.
Tạo một ứng dụng Multi-Thread sử dụng Interface Runable
Sử dụng lớp java.lang.Thread là một cách đơn giản để tạo ra một ứng dụng đa luồng, nhưng đó chưa phải là cách tốt nhất. Ngôn ngữ Java chỉ hỗ trợ đơn kế thừa (ngôn ngữ C++ hỗ trợ đa kế thừa). Điều đó cũng có nghĩa: nếu một lớp đã được kế thừa từ một lớp khác thì nó không thể nào kế thừa từ lớp java.lang.Thread được nữa.
Ví dụ: Có một ứng dụng xử lý đa luồng cho quản lý Nhân viên. Khi lớp Nhân viên kế thừa từ lớp Người thì nó sẽ không được phép tiếp tục kế thừa từ lớp java.lang.Thread.
Một cách khác để tạo ra một ứng dụng đa luồng đó là implement từ Interface java.lang.Runnable. Interface Runnable cung cấp một phương thức duy nhất run(). Khi gọi phương thức start() của thread thì phương thức run() của Inteface Runnable cũng được thực thi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import java.io.*; class BasicThread2 implements Runnable { // This method is called when the thread runs // Overrided method public void run() { PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out)); pw.print("Nhap vao 1 chuoi: "); pw.flush(); try { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String s; s = br.readLine(); pw.println("Ban vua nhap chuoi " + s); pw.close(); } catch (IOException e) { pw.println("Loi io!"); pw.close(); e.printStackTrace(); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class JThread { public static void main(String[] args) { // Create the object with the run() method Runnable runnable = new BasicThread2(); System.out.println("Tao thread 1: "); // Create the thread supplying it with the runnable object Thread t1 = new Thread(runnable); System.out.println("Tao thread 2: "); // Create the thread supplying it with the runnable object Thread t2 = new Thread(runnable); System.out.println(“chay 2 thread”); // Start two threads t1.start(); t2.start(); } } |
Các hàm điều khiển Thread
1. Ngắt một Thread
Khi sử dụng phương thức Thread.Sleep() thì chương trình phải bắt các ngoại lệ để xử lý. Lý do nếu một thread bị dừng lại trong một khoảng thời gian lâu, mà trong khoảng thời gian đó nó không thể tự đánh thức nó được. Tuy nhiên, nếu thread đó cần được đánh thức sớm hơn, ta có thể sử dụng phương thức Interrupt() để ngắt nó.
2. Dừng một Thread
Đôi khi ta muốn dừng một thread trước khi nó hoàn thành, ta sẽ yêu cầu một thread khác gửi thông điệp tới thread bằng cách 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.
3. Tạm dừng và phục hồi Thread
Cách đúng đắn để ngừng một thread đang chạy là thiết lập một biến mà thread này kiểm tra thường xuyên. Khi một thread phát hiện rằng biến đó đã được thiết lập, nó sẽ trở về từ phương thức run().
Lưu ý: Thread.suspend() và Thread.stop() cung cấp các phương thức không đồng bộ để ngừng một thread. Tuy nhiên, những phương thức này đã không còn được hỗ trợ do sử dụng chúng rất không an toàn. Chúng thường gây nên deadlock và lỗi khi giải phóng tài nguyên. Phương thức Thread.resume() dùng để tiếp tục một thread đã bị suspend.
Ngoài ra, Java cũng cung cấp phương thức:
Giải phóng thời gian cho CPU (Yielding CPU Time) để nâng cao hiệu quả cho hệ thống (giải quyết tình huống 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 vùng mã lệnh). Ta có thể dùng phương pháp static yield() của thread để giải phóng thời gian CPU cho thread hiện hành và chỉ xử lý được trên thread hiện hành.
Đợi một thread kết thúc một công việc nào đó, ta dùng phương thức IsAlive() để xác định thread còn chạy hay không. Tuy nhiên, việc thường xuyên gọi phương thức IsAlive thì hiệu quả của CPU sẽ thấp, để tránh tình trạng này, ta có thể dùng phương thức joint() để đợi một thread kết công việc.
Đồng bộ hóa
Ở đây ta có một khái niệm mới là miền găng (a race condition). Miền găng nói đến sự xung đột khi đa truy cập không được quản lý hợp lý trong lúc làm việc với nhiều thread. Khi làm việc với nhiều thread, có nhiều hơn một thread muốn truy cập cùng một
tài nguyên chia sẻ (một file hoặc biến) tại cùng một thời điểm sẽ xảy ra sự mất đồng bộ.
Ví dụ, một thread có thể cố gắng đọc dữ liệu, trong khi thread khác cố gắng thay đổi dữ liệu. Trong trường hợp này, dữ liệu có thể bị sai lạc.
Việc 2 thread cùng truy cập vào một phương thức cùng một thời điểm sẽ dẫn đến tranh chấp trong việc truy cập các tài nguyên cục bộ. Trong trường hợp này, ta cần cho phép một thread hoàn thành trọn vẹn nhiệm vụ của nó (thay đổi giá trị của biến), rồi các thread kế tiếp mới được phép thực thi. Để giải quyết vấn đề này ta phải đồng bộ hóa các thread, việc đồng bộ hóa nhằm bảo đảm khi có nhiều hơn một thread truy cập tới một tài nguyên được chia sẽ, thì tài nguyên đó sẽ chỉ được sử dụng bởi một thread tại một thời điểm. Phương thức được đồng bộ hóa sẽ báo cho hệ thống đặt một khóa vòng một tài nguyên riêng biệt.
Các thread được đồng bộ hoá trong Java sử dụng thông qua một monitor (cũng được gọi là một semaphore). Một monitor là một đối tượng (object) cho phép một thread truy cập vào một tài nguyên. Cơ chế monitor thực hiện hai nguyên tắc đồng bộ chính:
- Không một luồng nào khác được phân monitor khi có một luồng đã yêu cầu và đang chiếm giữ. Những luồng có yêu cầu monitor sẽ phải chờ cho đến khi monitor được giải phóng.
- Khi có một luồng giải phóng (ra khỏi) monitor, một trong số các luồng đang chờ monitor có thể truy cập vào tài nguyên dùng chung tương ứng với monitor đó.
Để giải quyết vấn đề miền găng ta có hai giải pháp là Synchronized Methods và Synchronized Blocks (Statements)
Phương thức đồng bộ hóa (Synchronized Method)
Để tạo một phương thức được đồng bộ hóa, đơn giản chỉ thêm từ khóa synchronized vào khai báo một phương thức. Việc bổ sung từ khóa synchronized nhằm đảm bảo chỉ có một thread được phép ở bên trong phương thức tại một thời điểm.
1 2 |
public synchronized void doStuff(int value) { } |
Đồng bộ khối (Synchronized Statement Block)
Tạo ra các phương thức đồng bộ với từ khóa synchronized trong phạm vi các lớp là một con đường dễ dàng và có hiệu quả của việc thực hiện sự đồng bộ. Tuy nhiên, điều này không có hiệu quả trong tất cả các trường hợp. Đôi khi ta chỉ muốn đồng bộ việc truy cập vào một đối tượng (object) của một lớp.
Đồng bộ khối được sử dụng khi không cần phải đồng bộ toàn bộ phương thức hoặc khi muốn nhận lock trên một đối tượng khác. Để đồng bộ truy cập một đối tượng của lớp này, ta gọi các phương thức mà lớp này định nghĩa, được đặt trong một khối đồng bộ. Tất cả các được đặt trong một câu lệnh đồng bộ như sau:
1 2 3 |
synchronized(object) { // block code } |
Ở đây “object” là một tham chiếu đến một đối tượng được đồng bộ.
Phối hợp hoạt động
Ngoài việc kiểm soát nhiều thread cùng truy cập vào tài nguyên dùng chung tại một thời điểm, còn có một vấn đề nữa là ta phải phối hợp hoạt động giữa các thread với nhau.
Xét hai thread P1 và P2. Trong đó, P1 cần thực hiện toán tử O1, P2 cần thực hiện O2 và điều kiện là O2 chỉ được thực hiện sau khi toán tử O1 đã hoàn thành. Sử dụng semaphore là phù hợp trong trường hợp này. Chúng ta chỉ cần thêm các dòng lệnh đồng bộ hóa vào chương trình.
Trong lớp semaphore, chúng ta có thể sử dụng tương ứng hàm Semaphore.acquire() như Wait() và Semaphore.release() như là Notify().
Leave a Reply