Trường hợp Deadlock khi lập trình multithreading trong Java

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

1. Thread Deadlock trong Java là gì?

Trong Java, chúng ta có thể dùng từ khóa synchronized để đảm bảo rằng tại một thời điểm nhất định thì chỉ có 1 Thread được sử dụng một tài nguyên (file, object, method,…). Giả sử, tài nguyên ở đây là method abc() thì có nghĩa là khi một Thread t1 đang sử dụng method abc() thì những Thread khác phải chờ đến khi Thread t1 sử dụng xong method abc() thì chúng mới được sử dụng method abc().

Khi lập trình multithreading, có những trường hợp mà 2 hoặc nhiều Thread rơi vào trạng thái chờ đợi lẫn nhau vì mỗi Thread giữ một tài nguyên và chờ đợi tài nguyên từ Thread khác.

Ví dự thread deadlock trong Java

Ví dụ, ThreadX và ThreadY đều cần tài nguyên A và B để thực hiện công việc của mình. Nhưng ThreadX giữ tài nguyên A và chời đợi tài nguyên B đang bị ThreadY nắm giữ. Trong lúc đó, ThreadY lại chờ đợi ThreadX trả tài nguyên A để sử dụng dẫn đến ThreadX và ThreadY chờ đợi lẫn nhau mãi mãi.

Ví dụ một trường hợp Deadlock trong Java

import java.util.logging.Level;
import java.util.logging.Logger;

class Shared{
    //hàm synchronized thứ 1
    synchronized void hamSynchronized1(Shared sh, String threadName){
        System.out.println(threadName + "-Synchronized1 begin...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            Logger.getLogger(Shared.class.getName()).log(Level.SEVERE, null, ex);
        }
 
        //đối tượng sh gọi hàm hamSynchronized2()
        sh.hamSynchronized2(threadName);
        System.out.println(threadName + "-Synchronized1 end!");
    }
 
    //hàm synchronized thứ 2
    synchronized void hamSynchronized2(String threadName){
        System.out.println(threadName + "-Synchronized2 begin...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            Logger.getLogger(Shared.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println(threadName + "-Synchronized2 end!");
    }
}

class Thread1 extends Thread{
    private Shared s1;
    private Shared s2;
 
    //hàm khởi tạo của Thread1 sử dụng Shared s1 và s2
    public Thread1(Shared s1, Shared s2){
        this.s1 = s1;
        this.s2 = s2;
    }
 
    //hàm run() thực thi công việc của Thread1
    @Override
    public void run(){
        s1.hamSynchronized1(s2, this.getName());
    }
}
 
class Thread2 extends Thread{
    private Shared s1;
    private Shared s2;
 
    //hàm khởi tạo của Thread2 sử dụng Shared s1 và s2
    public Thread2(Shared s1, Shared s2){
        this.s1 = s1;
        this.s2 = s2;
    }
 
    //hàm run() thực thi công việc của Thread2
    @Override
    public void run(){
        s2.hamSynchronized1(s1, this.getName());
    }
}

class Main{  
    public static void main(String[] args){
        //tạo đối tượng Shared s1
        Shared s1 = new Shared();
        //tạo đối tượng Shared s2
        Shared s2 = new Shared();
        //tạo và start Thread1 t1 sử dụng đối tượng s1 và s2
        Thread1 t1 = new Thread1(s1, s2);
        t1.setName("Thread1-t1");
        t1.start();
        
        //tạo và start Thread2 t2 sử dụng đối tượng s1 và s2
        Thread2 t2 = new Thread2(s1, s2);
        t2.setName("Thread2-t2");
        t2.start();
 
        try {
            //thiết lập main Thread ngủ trong 2000ms
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
Kết quả
Thread1-t1-Synchronized1 begin...
Thread2-t2-Synchronized1 begin...

Các bạn có thể thấy kết quả là các Thread cứ chờ đợi mãi mà không được thực hiện tiếp. Chương trình trên sẽ không thể nào hoàn tất và kết thúc được.

Trong chương trình trên, lớp Shared có 2 hàm synchronized hamSynchronized1()hamSynchronized2(). Trong đó, hàm hamSynchronized1() còn cần một đối tượng Shared sh để thực thi công việc của mình. Trong lớp Thread1Thread2 đều cần 2 tài nguyên là 2 đối tượng Shared s1 và s2. Trong hàm main(), các Thread hoạt động như sau:

1. Thread1 t1 được start() và chiếm giữ Shared s1.

2. Sau đó, Thread1 t1 ngủ (sleep) trong 1000ms.

3. Khi đó, Thread2 t2 được start() và chiếm giữ Shared s2.

4. Khi Thread1 t1Thread2 t2 hoạt động lại thì Thread1 t1 đang giữ Shared s1 và chờ Thread2 t2 trả lại Shared s2 để sử dụng.

5. Thread2 t2 đang giữ Shared s2 và chờ Thread1 t1 trả lại Shared s1 để sử dụng.

6. Hai Thread là t1t2 cứ chờ lẫn nhau để được sử dụng tài nguyên mà Thread kia đang chiếm giữ dẫn đến Deadlock.

Phát hiện Deadlock

Nếu các bạn đang chạy chương trình Java trên Windows và sử dụng Java 8, chúng ta có thể phát hiện Thread Deadlock bằng cách sử dụng lệnh jcmd trên Command Prompt.

Bước 1 – Mở Command Prompt trên Windows rồi gõ lệnh jcmd thì sẽ thấy được PID (Process ID) của Process có các Thread gặp Deadlock.

C:\Users\LHVINH>jcmd
3012 sun.tools.jcmd.JCmd
3060
16668 anotherPackage.Main

Process có PID là 16668 với lớp Main đang chạy nằm trong package anotherPackage có các Thread gặp Deadlock.

Bước 2 – Gõ tiếp lệnh jcmd <PID> Thread.print để phát hiện Deadlock.

C:\Users\LHVINH>jcmd 16668 Thread.print
16668:
2022-01-18 21:34:44
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode):
…
Found one Java-level deadlock:
=============================
"Thread2-t2":
  waiting to lock monitor 0x00000000033db058 (object 0x0000000780669c90, a anotherPackage.Shared),
  which is held by "Thread1-t1"
"Thread1-t1":
  waiting to lock monitor 0x00000000033daef8 (object 0x0000000780669ca0, a anotherPackage.Shared),
  which is held by "Thread2-t2"

Java stack information for the threads listed above:
===================================================
"Thread2-t2":
        at anotherPackage.Shared.hamSynchronized2(Main.java:23)
        - waiting to lock <0x0000000780669c90> (a anotherPackage.Shared)
        at anotherPackage.Shared.hamSynchronized1(Main.java:17)
        - locked <0x0000000780669ca0> (a anotherPackage.Shared)
        at anotherPackage.Thread2.run(Main.java:63)
"Thread1-t1":
        at anotherPackage.Shared.hamSynchronized2(Main.java:23)
        - waiting to lock <0x0000000780669ca0> (a anotherPackage.Shared)
        at anotherPackage.Shared.hamSynchronized1(Main.java:17)
        - locked <0x0000000780669c90> (a anotherPackage.Shared)
        at anotherPackage.Thread1.run(Main.java:46)

Found 1 deadlock.

2. Một số giải pháp tránh Thread Deadlock trong Java

Để tránh Deadlock, chúng ta phải biết chính xác các khả năng có thể xảy ra Deadlock. Đó là một quá trình phức tạp và không dễ thực hiện. Chúng ta cũng có một số giải pháp giúp tránh Thread Deadlock. Tuy nhiên, chúng chỉ giúp hạn chế khả năng Thread Deadlock xảy ra chứ không đảm bảo 100% là không gặp Deadlock.

  • Tránh giao một tài nguyên (file, object, method,…) cho 1 Thread chiếm giữ rồi còn cố giao cho một Thread khác. Đây là trường hợp phổ biến nhất dẫn đến DeadLock.
  • Tránh việc sử dụng synchronized cho các tài nguyên khi không cần thiết. Cần xem xét kỹ một tài nguyên có được sử dụng cho nhiều Thread hay không? Và tài nguyên có ảnh hưởng đến kết quả tính toán khi có nhiều Thread sử dụng hay không.
  • Sử dụng hàm join() của lớp java.lang.Thread: Có thể cho một Thread kết thúc rồi mới cho Thread khác bắt đầu thì sẽ tránh trường hợp một Thread đợi Thread khác.

Ví dụ tránh Deadlock với hàm join()

Lấy ví dụ trường hợp Deadlock ở trên, chúng sử dụng hàm join() để đảm bảo Thread1 t1 thực hiện xong rồi mới đến Thread2 t2.

import java.util.logging.Level;
import java.util.logging.Logger;

class Shared{
    //hàm synchronized thứ 1
    synchronized void hamSynchronized1(Shared sh, String threadName){
        System.out.println(threadName + "-Synchronized1 begin...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            Logger.getLogger(Shared.class.getName()).log(Level.SEVERE, null, ex);
        }
 
        //đối tượng sh gọi hàm hamSynchronized2()
        sh.hamSynchronized2(threadName);
        System.out.println(threadName + "-Synchronized1 end!");
    }
 
    //hàm synchronized thứ 2
    synchronized void hamSynchronized2(String threadName){
        System.out.println(threadName + "-Synchronized2 begin...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            Logger.getLogger(Shared.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println(threadName + "-Synchronized2 end!");
    }
}

class Thread1 extends Thread{
    private Shared s1;
    private Shared s2;
 
    //hàm khởi tạo của Thread1 sử dụng Shared s1 và s2
    public Thread1(Shared s1, Shared s2){
        this.s1 = s1;
        this.s2 = s2;
    }
 
    //hàm run() thực thi công việc của Thread1
    @Override
    public void run(){
        s1.hamSynchronized1(s2, this.getName());
    }
}
 
class Thread2 extends Thread{
    private Shared s1;
    private Shared s2;
 
    //hàm khởi tạo của Thread2 sử dụng Shared s1 và s2
    public Thread2(Shared s1, Shared s2){
        this.s1 = s1;
        this.s2 = s2;
    }
 
    //hàm run() thực thi công việc của Thread2
    @Override
    public void run(){
        s2.hamSynchronized1(s1, this.getName());
    }
}

class Main{  
    public static void main(String[] args){
        //tạo đối tượng Shared s1
        Shared s1 = new Shared();
        //tạo đối tượng Shared s2
        Shared s2 = new Shared();
        //tạo và start Thread1 t1 sử dụng đối tượng s1 và s2
        Thread1 t1 = new Thread1(s1, s2);
        t1.setName("Thread1-t1");
        t1.start();
        try {
            //chờ cho đến khi Thread1 t1 thực hiện xong
            //rồi mới cho start các Thread khác
            t1.join();
        } catch (InterruptedException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
        
        //tạo và start Thread2 t2 sử dụng đối tượng s1 và s2
        Thread2 t2 = new Thread2(s1, s2);
        t2.setName("Thread2-t2");
        t2.start();
 
        try {
            //thiết lập main Thread ngủ trong 2000ms
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
Kết quả
Thread1-t1-Synchronized1 begin...
Thread1-t1-Synchronized2 begin...
Thread1-t1-Synchronized2 end!
Thread1-t1-Synchronized1 end!
Thread2-t2-Synchronized1 begin...
Thread2-t2-Synchronized2 begin...
Thread2-t2-Synchronized2 end!
Thread2-t2-Synchronized1 end!

Kết quả cho thấy Thread1 t1 thực hiện xong rồi mới đến Thread2 t2 thực hiện. Thread1 t1Thread2 t2 không còn chờ đợi lẫn nhau.

5/5 - (1 bình chọn)
Bài trước và bài sau trong môn học<< Lập lịch (scheduler) và đồng bộ hóa (synchronization) Thread 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ỏ.