Giới thiệu
Phần cứng ngày càng phát triển, từ đơn nhân thành đa nhân, từ máy tính chỉ chạy được 1 chương trình tại 1 thời điểm (đơn nhiệm) đã có thể chạy nhiều chương trình 1 lúc (đa nhiệm), cùng lướt web, nghe nhạc, chơi game - multi-process.
Trong phạm vi nhỏ hơn của 1 chương trình cũng có những thay đổi tương tự để chương trình vừa có thể vẽ, phát âm thanh và tải dữ liệu đồng thời, đó chính là multi-thread.
Process và Thread
Process
Process (tiểu trình) được hiểu là 1 chương trình đang chạy, ví dụ đọc bài viết này cần trình duyệt web - đây là 1 process và process này sẽ có ID (Process IDentifier) để phân biệt với các process khác .
Tất cả các process đều được quản lý bởi hệ điều hành và mỗi process có 1 vùng nhớ làm việc riêng mà các process khác không được can thiệp vào.
Các process này có thể chạy song song với nhau. Về bản chất thì khái niệm song song được hiểu bởi con người, đối với máy tính tại 1 thời điểm CPU chỉ đáp ứng được 1 process. Những process được hệ điều hành lập lịch Scheduling (phân phối phần cứng cho mỗi process) sao cho các process sử dụng CPU hiệu quả. Thời gian này quá nhanh đối với con người nên người dùng cảm thấy các process được chạy đồng thời.
Tìm hiểu các giải thuật định thời với các giải thuật như First Come First Served (FCFS) Scheduling, Shortest-Job-First (SJF) Scheduling, Priority Scheduling, Round Robin(RR) Scheduling.
Xem các process đang chạy trên Windows
Mở chương trình cmd và gõ tasklist
để hiển thị danh sách các process đang chạy.
tasklist
Và dưới đây là hình ảnh các process đang chạy.

Hoặc có thể mở Task Manager để xem các process đang chạy

Thread
Thread thường được nhắc tới với các tên là tiểu trình, luồng, tuyến.
Trong 1 process thường có nhiều thread chạy song song với nhau, các thread sử dụng chung vùng nhớ của Process. Khi 1 chương trình được start hay là process start thì luôn luôn có 1 thread được tạo và thread này được gọi là Main Thread. Từ Main Thread có thể tạo ra các thread khác để xử lý những công việc riêng.
Hình ảnh mô tả Process và Thread

Tại sao phải cần đến Thread?
Trong ứng dụng có những công việc tốn khá nhiều thời gian. Ví dụ:
- Giao tiếp network: nếu download file hay reques server chờ trả về kết quả thì tốn khá nhiều thời gian.
- Đọc ghi file: chi phí đọc ghi file là khá lớn.
Nếu thực hiện những công việc này trên Main Thread thì những công việc khác phải chờ, sau khi hoàn thành công việc này mới tiếp tục thực việc công việc khác. Việc chờ như vậy đôi khi sẽ khiến ứng dụng bị "đơ" 1 thời gian, chẳng hạn như UI bị đóng băng.
Giải pháp cho vấn đề trên là tạo ra Thread khác để thực hiện những công việc khác nhau, chạy song song với Main Thread, để ứng dụng của chúng ta đạt hiệu quả cao về xử lý lẫn trải nghiệm người dùng.
Multilthreading trong Java
Để tạo Thread trong Java có hai cách:
- Tạo class kế thừa từ
class Thread
. - Implements
interface Runnable
.
Override lại phương thức run()
trong class Thread
và interface Runnable
, các công việc cần chạy trong thread sẽ viết trong phương thức run()
này.
Cách 1: kế thừa lớp Thread
Đầu tiên tạo 1 class kế thừa lớp Thread
và override lại phương thức run()
package com.nguyennghia.demothreading; public class MyThread extends Thread { @Override public void run() { for(int i = 0; i < 50; i++){ System.out.print("X"); } } }
Khi cần chạy thread này, tiến hành tạo đối tượng MyThread
và gọi phương thức start()
.
package com.nguyennghia.demothreading; public class Program { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); // call start() method to run thread for(int i = 0; i < 50; i++){ System.out.print("Y"); } } }
Chạy chương trình sẽ thấy X
và Y
được in ra không theo trật tự nào vì myThread
chạy song song với Main Thread, mỗi lần chạy sẽ thấy các kết quả khác nhau:
YYYYYYYYYYYYYYXXXXXYYYXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
Cách 2: Implements interface Runnable
Đầu tiên tạo 1 class implements interface Runable
package com.nguyennghia.demothreading; public class MyRunnale implements Runnable { @Override public void run() { for(int i = 0; i < 50; i++){ System.out.print("X"); } } }
Khi cần chạy thread này, tiến hành tạo đối tượng MyRunnable
và truyền vào constructor của Thread
, sau đó gọi phương thức start()
của Thread.
package com.nguyennghia.demothreading; public class Program { public static void main(String[] args) { Thread myThread = new Thread(new MyRunnale()); myThread.start(); for(int i = 0; i < 50; i++){ System.out.print("Y"); } } }
Kết quả sẽ xuất ra tương tự như cách 1.
Ngoài ra có thể implement trực tiếp Runnable tại thời điểm truyền vào constructor của Thread như bên dưới nếu code xử lý không quá phức tạp.
package com.nguyennghia.demothreading; public class Program { public static void main(String[] args) { Thread myThread = new Thread(new Runnable() { @Override public void run() { for(int i = 0; i < 50; i++){ System.out.print("X"); } } }); myThread.start(); for(int i = 0; i < 50; i++){ System.out.print("Y"); } } }
Hình ảnh mô phỏng Main Thread và Worker Thread (thread con) trong cả hai cách trên.

Các thông tin của Thread
ThreadID
Khi 1 Thread được tạo ra sẽ có 1 ID do máy ảo Java cung cấp, không thể thay đổi giá trị ID này.
Phương thức getID()
cho phép lấy ID của thread
myThread.getId(); //get id of myThread
ThreadName
Thread cũng sẽ có 1 tên đại diện cho thread đó, có thể gán tên cho thread sử dụng phương thức setName()
.
myThread.setName("ThreadName");
Và phương thức getName()
để lấy tên của thread
myThread.getName();
ThreadPriority
Các thread khi tạo ra có độ ưu tiên, độ ưu tiên này sẽ được CPU sử dụng để lập lịch cho Thread. Priority có giá trị từ 0 đến 10. Các giá trị phổ biến được định nghĩa sẵn trong lớp Thread là:
public final static int MAX_PRIORITY = 10; // The maximum priority value allowed for a thread.
public final static int MIN_PRIORITY = 1; // The minimum priority value allowed for a thread.
public final static int NORM_PRIORITY = 5; // The normal (default) priority value assigned to threads.
Sử dụng phương thức setPriority()
để cấu hình priority cho thread và getPriority()
lấy giá trị priority của thread.
myThread.setPriority(Thread.MAX_PRIORITY); myThread.getPriority();
Mặc định khi thread được tạo sẽ có giá trị priority là NORM_PRIORITY
.
ThreadState
1 thread có các state
, trạng thái dưới đây:
NEW
: thread đã được khởi tạo nhưng chưa chạy.RUNNABLE
: thread đang chạy.BLOCKED
: thread bị chặn, trạng thái này xảy ra khi thread tiến hành truy cập vào vùng dữ liệu dùng chung nhưng tại thời điểm đó có một thread khác đang trong vùng này.WAITING
: thread trong trạng thái chờ tín hiệu từ thread khác, xảy ra khi gọi Object.wati() hoặc Thread.join(). Trạng thái này kết thúc khi một thread khác gọi phương thức Object.notify().TIMED_WAITING
: tương tự như trạng thái WAITING, nhưng trong một khoảng thời gian xác định.TERMINATED
: thread đã kết thúc.
Đụng độ giữa các Thread và cách giải quyết
Ccác thread tạo ra sẽ dùng chung 1 vùng nhớ, nếu các process này cùng truy cập vào 1 vùng nhớ tại cùng 1 thời điểm thì dẫn đến sai, mất dữ liệu.
Để khắc phục điều này, Java cung cấp synchronized để đồng bộ các thread khi sử dụng chung vùng nhớ chia sẻ (shared memory).
Mội khối synchronized đánh dấu 1 phương thức hay 1 khối mã được đồng bộ tránh xung đột giữa các thread.
Khi 1 thread can thiệt vào phương thức hay khối mã được đánh dấu là synchronized thì thread này sẽ khóa (lock) không cho các thread khác can thiệp vào cho đến khi thread này thực hiện xong thì mới đánh thức các thread khác. Và như vậy tại 1 thời điểm chỉ có 1 thread truy cập vào vùng nhớ được chia sẻ.
Tạo class ShareMemory
với phương thức là printData()
đại diện cho dữ liệu dùng chung cho nhiều thread.
package com.nguyennghia.demothreading; public class ShareMemory { public void printData(String threadName) { for(int i = 0; i < 50; i++) { System.out.println(threadName + ": " + i); } } }
Tiến hành tạo 3 thread để cùng truy cập vào phương thức printData
của đối tượng ShareMemory
.
package com.nguyennghia.demothreading; public class MyThread extends Thread { private ShareMemory mShareMemory; private String mThreadName; public MyThread(ShareMemory sm, String threadName) { this.mShareMemory = sm; this.mThreadName = threadName; } @Override public void run() { mShareMemory.printData(mThreadName); } }
Hàm Main:
public class Program { public static void main(String[] args) { ShareMemory sm = new ShareMemory(); MyThread thread1 = new MyThread(sm, "Thread1"); MyThread thread2 = new MyThread(sm, "Thread2"); MyThread thread3 = new MyThread(sm, "Thread3"); thread1.start(); thread2.start(); thread3.start(); } }
Chạy và xem kết quả
Thread1: 0 Thread3: 0 Thread2: 0 Thread3: 1 Thread1: 1 Thread3: 2 Thread3: 3 Thread2: 1 Thread3: 4 Thread1: 2 Thread3: 5 Thread2: 2 Thread3: 6 Thread1: 3 Thread3: 7 Thread2: 3 Thread3: 8 Thread1: 4 Thread3: 9 Thread2: 4 Thread3: 10 . . .
Cả 3 thread đều truy cập vào 1 tài nguyên trong khi thread này vẫn nắm giữ.
Để đồng bộ, thêm từ khóa synchronized vào trước phương thức printData()
package com.nguyennghia.demothreading; public class ShareMemory { public synchronized void printData(String threadName){ for(int i = 0; i < 50; i++){ System.out.println(threadName + ": " + i); } } }
Xem lại kết quả
Thread1: 0 Thread1: 1 Thread1: 2 Thread1: 3 Thread1: 4 Thread1: 5 Thread1: 6 Thread1: 7 Thread1: 8 Thread1: 9 Thread1: 10 . . . Thread3: 0 Thread3: 1 Thread3: 2 Thread3: 3 Thread3: 4 Thread3: 5 Thread3: 6 Thread3: 7 Thread3: 8 Thread3: 9 Thread3: 10 . . . Thread2: 0 Thread2: 1 Thread2: 2 Thread2: 3 Thread2: 4 Thread2: 5 Thread2: 6 Thread2: 7 Thread2: 8 Thread2: 9 Thread2: 10
thread1
sẽ được giữ tài nguyên và khóa không cho thread2
và thread3
truy cập. Sau khi thread1
thực hiện xong sẽ đánh thức thread2
và thread3
, lúc này thread3
sẽ được giữ tài nguyên và khóa không cho thread2
vào. Sau khi thực hiện xong sẽ đánh thức thread3
thực hiện. Như vậy tại 1 thời điểm chỉ có 1 thread được can thiệp vào vùng nhớ chia sẻ.
Với từ khóa synchronized
mà ngôn ngữ Java cung cấp có thể đồng bộ hóa giữa các thread.