Search…

Lập Trình Multithreading trong Ngôn Ngữ Lập Trình Java

02/10/20209 min read
Để đáp ứng được yêu cầu thực hiện được nhiều tác vụ cùng một lúc, Java cung cấp multithreading, mỗi tác vụ riêng được hiểu như 1 thread và thread này thực hiện một công việc được chỉ định.

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.

tasklist trên Windows.

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

Task Manager trên Windows.

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

Process và Thread
Process

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:

  1. Tạo class kế thừa từ class Thread.
  2. Implements interface Runnable.

Override lại phương thức run() trong class Threadinterface 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 XY đượ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.

Main Thread và Worker Thread

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 thread2thread3 truy cập. Sau khi thread1 thực hiện xong sẽ đánh thức thread2thread3, 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.

IO Stream

IO Stream Co., Ltd

30 Trinh Dinh Thao, Hoa Thanh ward, Tan Phu district, Ho Chi Minh city, Vietnam
+84 28 22 00 11 12
developer@iostream.co

383/1 Quang Trung, ward 10, Go Vap district, Ho Chi Minh city
Business license number: 0311563559 issued by the Department of Planning and Investment of Ho Chi Minh City on February 23, 2012

©IO Stream, 2013 - 2024