Lập lịch (scheduler) và đồng bộ hóa (synchronization) Thread trong Java

Đây là bài 61/62 bài của series môn học Ngôn ngữ lập trình Java

1. Thứ tự ưu tiên (priority) giữa các thread

Mỗi Thread trong Java có một thứ tự ưu tiên (priority). Thứ tự ưu tiên này được đánh số từ 1 đến 10. Thread nào có thứ tự ưu tiên lớn hơn thì sẽ có cơ hội được ưu tiên thực thi trước. Trong hầu hết các trường hợp, thứ tự ưu tiên của các Thread sẽ phụ thuộc vào JVM. Tuy nhiên, lập trình viên cũng có thể gán thứ tự ưu tiên của một Thread trong Java.

Lớp java.lang.Thread hỗ trợ phương thức setPriority(int newPriority) để thay đổi thứ tự ưu tiên của Thread. Các hằng số thứ tự ưu tiên được định nghĩa sẵn trong lớp Thread là:

– MIN_PRIORITY = 1

– NORM_PRIORITY=5

– MAX_PRIORITY=10

Một Thread khi được tạo ra sẽ có thứ tự ưu tiên mặc định là NORM_PRIORITY và sẽ được thực thi theo quy tắc FCFS (First Come First Serve).

Để lấy thứ tự ưu tiên của Thread, chúng ta sử dụng hàm public final int getPriority().

class MyThread extends Thread {
    @Override
    public void run(){
        System.out.println(this.getName() + " with priority is "
                + this.getPriority() + " processing...");
    }
}
  
class Main {  
    public static void main(String[] args){
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();
        MyThread mt3 = new MyThread();
        //setName Threads
        mt1.setName("MyThread-1");
        mt2.setName("MyThread-2");
        mt3.setName("MyThread-3");
        //setPriority Threads
        mt1.setPriority(Thread.MIN_PRIORITY);
        mt2.setPriority(mt1.getPriority() + 1);
        mt3.setPriority(Thread.MAX_PRIORITY);
        //start Threads
        mt1.start();
        mt2.start();
        mt3.start();
    }
}
Kết quả
MyThread-3 with priority is 10 processing...
MyThread-2 with priority is 2 processing...
MyThread-1 with priority is 1 processing...

Lưu ý: Thứ tự ưu tiên của Thread không thể đảm bảo Thread có thứ tự ưu tiên cao hơn sẽ luôn luôn được thực thi trước so với các Thread có thứ tự ưu tiên thấp hơn. Việc lựa chọn các Thread để thực thi phụ thuộc vào bộ lập lịch (Scheduler) của hệ điều hành hoặc của JVM.

2. Lập lịch (scheduler) cho các thread

Bộ lập lịch (scheduler) giúp xác định Thread nào sẽ được đưa vào CPU để thực thi. Scheduler là một phần của hệ điều hành hoặc Java Virtual Machine (JVM). Với ngôn ngữ lập trình Java thì Scheduler sẽ là một phần của JVM và JVM sẽ giao tiếp với hệ điều hành để lựa chọn Thread nào được thực thi.

Những Thread ở trạng thái là runnable mới được Scheduler lập trình. Và các tiêu chí ảnh hưởng đến việc quyết định Thread nào sẽ được thực thi là:

  • Thứ tự ưu tiên (Priority) của các Thread
  • Thời gian đến của Thread (Time of Arrival): Có những trường hợp các Thread có cùng độ ưu tiên thì những Thread nào đến trước (có thể hiểu là được start() trước) hoặc có thời gian chờ lớn hơn sẽ có cơ hội thực thi trước.

2.1. Các chiến lược lập lịch

First Come First Serve (FCFS) scheduling

Thread nào đến trước thì được đưa vào CPU thực thi trước. Ví dụ:

ThreadsTime of Arrival
t10
t21
t32
t43

Dựa vào Time of Arrival thì Thread t1 sẽ được thực thi đầu tiên. Kế đó là Thread t2, t3 và cuối cùng là Thread t4.

Time-slicing scheduling

Như ví dụ về chiến lược lập trình FCFS ở trên, nếu Thread t1 chiếm thời gian thực thi quá lớn thì các Thread còn lại phải đợi mãi mà chưa được thực thi. Time-slicing scheduling có thể là một giải pháp cho trường hợp này. Mỗi Thread chỉ được thực thi trong 1 khoảng thời gian nhất định rồi phải tạm dừng và nhường CPU cho các Thread khác thực thi.

Time-slicing scheduling trong Java

Trong hình trên, mỗi Thread được thực thi trong 2 giây. Vì vậy, Thread t1 được thực thi trong 2 giây rồi nhường CPU để thực thi Thread t2. Tương tự đến các Thread t3, t4. Rồi nếu Thread t1 trong 2 giây mà chưa thực thi xong thì Thread t1 tiếp tục sẽ được CPU thực thi. Quá trình tương tự lặp lại cho các Thread khác.

Preemptive-Priority scheduling

Chiến lược này dựa trên độ ưu tiên để chọn Thread được thực thi trước. Thread nào có độ ưu tiên cao hơn sẽ được đưa vào CPU để thực thi trước.

Preemptive-priority scheduling trong Java

Trong hình trên, Thread Thread1 và Thread3 được ưu tiên thực thi trước vì có thứ tự ưu tiên cao hơn.

2.2. Bộ lập lịch (schuduler) trong Java hoạt động như thế nào?

Thread scheduler trong Java

Scheduler trong Java sẽ vận dụng cả 3 chiến lược lập lịch để giúp các Thread được thực thi hiệu quả nhất có thể. Thông thường, Thread nào có thứ tự ưu tiên cao hơn sẽ được chọn thực thi trước. Các Thread thực thi trong 1 khoảng thời gian nhất định với chiến lược Time-slicing scheduling. Rồi cứ luân phiên được đưa vào CPU để thực thi cho đến khi mỗi Thread hoàn thành công việc của mình.

Khi có các Thread có cùng thứ tự ưu tiên thì Thread nào đến trước thì được chọn thực thi trước với chiến lược FCFS.

Lưu ý về scheduler

Những phần trình bày ở trên là nguyên lý chung của bộ lập lịch (scheduler) trong Java. Còn khi lập trình và chạy code với Java thì các bạn có thể thấy kết quả hiển thị ra khi thực thi các Thread không phải lúc nào cũng như nguyên lý đã trình bày. Bởi khi thực thi còn một số yếu tố khác ảnh hưởng như CPU (số nhân, số luồng của CPU), cách lập lịch của hệ điều hành, các Thread đang thực thi của những chương trình khác mà hệ điều hành đang quản lý, thời gian hoàn thành của các Thread,…

Lấy một ví dụ, Thread t1, t2 có thứ tự ưu tiên lần lượt là 8 và 2. Thread t1 được thực thi trước nhưng thời gian thực thi các công việc của Thread t1 lâu hơn nhiều so với Thread t2. Tất nhiên, Thread t1 không thể chiếm CPU mãi mà phải trả lại CPU để thực thi Thread t2. Khi Thread t2 được thực thi thì công việc được hoàn thành trước. Khi đó, kết quả xuất ra màn hình có thể sẽ báo là Thread t2 thực thi xong trước Thread t1. Do đó, các bạn đừng nhầm lẫn giữa Thread thực thi xong trước và được xuất kết quả ra trước với Thread được chọn để thực thi trước nhé.

3. Đồng bộ hóa (synchronization) giữa các thread

Khi có nhiều Thread chạy cùng lúc (multithreading), có những trường hợp mà nhiều Thread cùng cố gắng truy cập vào cùng một tài nguyên (file, object, method,…) cùng một lúc. Khi đó, chúng có thể tạo ra những kết quả không mong muốn và không lường trước được.

Do đó, cần có một cơ chế đảm bảo chỉ có 1 Thread truy cập vào tài nguyên vào một thời điểm nhất định. Cơ chế đó gọi là đồng bộ hóa (synchronization). Trong Java, có nhiều cách để thực thi cơ chế synchronization. Nhưng cách phổ biến là chúng ta sử dụng từ khóa synchronized khi định nghĩa phương thức (method) để đánh dấu là chỉ có 1 Thread có thể sử dụng method này tại một thời điểm nhất định.

Ví dụ không sử dụng synchronized thì kết quả có thể không như mong muốn

class Table{
    //phương thức không synchronized
    void printTable(int n, String threadName){
        System.out.println("\n" + threadName + " processing...");
        for(int i=1;i<=5;i++){
            System.out.print(n*i + " ");
            try{  
                Thread.sleep(500);  
            }catch(Exception e){
                System.out.println(e);
            }  
        }
    }
}

class MyThread extends Thread {
    Table t;
    int n;
    MyThread(Table t, int n){  
        this.t = t;
        this.n = n;
    }  
    
    @Override
    public void run(){
        t.printTable(n, this.getName());  
    }
}
  
class Main {  
    public static void main(String[] args){
        //một object obj
        Table obj = new Table(); 
        MyThread mt1 = new MyThread(obj, 5);
        mt1.setName("MyThread-1");
        MyThread mt2 = new MyThread(obj, 10);
        mt2.setName("MyThread-2");
        mt1.start();  
        mt2.start();  
    }
}
Kết quả

MyThread-2 processing...

MyThread-1 processing...
10 5 20 10 15 30 40 20 25 50

Chúng ta có một đối tượng obj của lớp Table được tạo ra. Cả 2 Thread mt1mt2 thực thi cùng lúc, đều truy cập đến đối tượng obj và gọi hàm printTable() để thực thi công việc.

Kết quả cho thấy Thread mt1mt2 truy cập đến đối tượng obj và gọi hàm printTable() cùng lúc nên có thể gây ra kết quả không mong muốn.

Ví dụ sử dụng synchronized thì có kết quả như mong muốn

class Table{
    //phương thức synchronized
    synchronized void printTable(int n, String threadName){
        System.out.println("\n" + threadName + " processing...");
        for(int i=1;i<=5;i++){
            System.out.print(n*i + " ");
            try{  
                Thread.sleep(500);  
            }catch(Exception e){
                System.out.println(e);
            }  
        }
    }
}

class MyThread extends Thread {
    Table t;
    int n;
    MyThread(Table t, int n){  
        this.t = t;
        this.n = n;
    }  
    
    @Override
    public void run(){
        t.printTable(n, this.getName());  
    }
}
  
class Main {  
    public static void main(String[] args){
        //một object obj
        Table obj = new Table(); 
        MyThread mt1 = new MyThread(obj, 5);
        mt1.setName("MyThread-1");
        MyThread mt2 = new MyThread(obj, 10);
        mt2.setName("MyThread-2");
        mt1.start();  
        mt2.start();  
    }
}
Kết quả

MyThread-1 processing...
5 10 15 20 25 
MyThread-2 processing...
10 20 30 40 50

Chúng ta có một đối tượng obj của lớp Table được tạo ra. Cả 2 Thread mt1mt2 thực thi cùng lúc, đều truy cập đến đối tượng obj và gọi hàm printTable() để thực thi công việc. Nhưng hàm printTable() được khai báo với từ khóa synchronized để đánh dấu là hàm này chỉ được một Thread gọi tại một thời điểm nhất định.

Trong chương trình trên, Thread mt1mt2 được start() và đều có thứ tự ưu tiên (Priority) mặc định là NORM_PRIORITY=5. Nhưng Thread mt1 đến trước (được start() trước) thì sẽ được ưu tiên thực thi trước. Khi Thread mt1 đang gọi hàm synchronized void printTable() thì Thread mt2 không gọi được hàm này và phải đợi sau khi Thread mt1 gọi xong thì mới gọi được.

Kết quả cho thấy Thread mt1 gọi hàm printTable() và thực thi xong thì Thread mt2 mới gọi hàm printTable() và thực thi công việc của nó.

5/5 - (1 bình chọn)
Bài trước và bài sau trong môn học<< Các loại Thread trong Java: Daemon Thread và User ThreadTrường hợp Deadlock khi lập trình multithreading trong Java >>
Chia sẻ trên mạng xã hội:

Trả lời

Lưu ý:

1) Vui lòng bình luận bằng tiếng Việt có dấu.

2) Khuyến khích sử dụng tên thật và địa chỉ email chính xác.

3) Mọi bình luận trái quy định sẽ bị xóa bỏ.