Java Bài 47: Deadlock – Yellow Code Books

Rate this item:

Rating: 5.0/5. From 23 votes.

Please wait…

Được chỉnh sửa ngày 25/06/2021.

Chào mừng các bạn đã đến với bài học Java số 47, bài học về Deadlock. Đây là bài học trong chuỗi bài về lập trình ngôn ngữ Java của Yellow Code Books.

Với việc kết thúc bài học hôm trước thì chúng ta cũng đã xong kiến thức về Đồng bộ hóa Thread. Bạn đã thấy vai trò rất đắc lực của từ khóa synchronized trong việc đảm bảo không xảy ra sự tranh chấp đối với tài nguyên dùng chung rồi đúng không nào. Quả thật synchronized rất tốt, nhưng nếu lạm dụng nó không đúng chỗ, bạn sẽ gặp một tình huống mà bài học hôm nay nhắm đến. Tình huống đó có tên là Deadlock.

Deadlock Là Gì?

Thoạt nghe qua cái tên làm chúng ta liên tưởng đến “cái chết” (Dead) nào đó!?! Có thể nói, bài học hôm nay không nằm ngoài cái sự chết chóc. Cụ thể hơn, mình đang nói về cái chết của ứng dụng của chúng ta, cái chết này gây ra bởi các Thread trong chương trình của bạn, chúng “chờ đợi” nhau cho đến chết!

Nếu như sự “chết chóc” nghe thấy sợ quá, thì mình mời bạn cùng đến với 2 tình huống sau, tình huống đầu mình sẽ lấy ví dụ vui từ thực tế, tình huống sau sẽ đi cụ thể vào trong lập trình xem Deadlock là gì nhé.

Hiểu Deadlock Qua Ví Dụ Thực Tế

Tuy đây là tình huống không dẫn đến cái sự “chờ nhau đến chết” như trong lập trình, nhưng nó cũng khiến cho các đương sự không biết phải sử xự ra sao.

Hình dưới là cảnh một cảnh sát đang nắm giữ một tên cướp. Anh cảnh sát muốn tên cướp còn lại phải trao trả con tin trước thì ảnh mới thả tên cướp đang nắm giữ. Trong khi đó, tên cướp kia thì nhất định không trả con tin, buộc anh cảnh sát phải thả đồng bọn của hắn ra trước.

Mô phỏng khả năng xảy ra deadlock trong thực tếMô phỏng khả năng xảy ra deadlock trong thực tếMô phỏng khả năng xảy ra deadlock trong thực tế

Vậy là, mỗi phe trong tình huống này đều nắm giữ riêng con tin của họ, và không phe nào chịu trao trả con tin về cho phe kia cả. Tình huống này rõ ràng là sẽ khó có một thỏa hiệp đạt được trong một thời gian ngắn. Deadlock khi này đã xảy ra.

Deadlock Trong Lập Trình

Trong lập trình thì tình huống Deadlock cũng tương tự như ví dụ thực tế vui trên đây. Nếu xem như Cảnh sátCướp là mỗi Thread. Thì Đồng bọn của cướpCon tin chính là các tài nguyên. Thread Cảnh sát đang nắm giữ tài nguyên Đồng bọn của cướp thông qua từ khóa synchronized, nhưng Thread Cảnh sát lại rất muốn giữ luôn cả Con tin. Mà Con tin lại đang bị nắm giữ bởi Thread Cướp cũng bằng từ khóa synchrozied, trong khi đó Cướp cũng lại muốn nhận về Đồng bọn của cướp. Trong lập trình thì khi này Deadlock cũng sẽ xảy ra.

Thực Hành Xây Dựng Ứng Dụng Gây Ra Deadlock

Nếu như trên đây là các tình huống thực tế và lý thuyết để bạn dễ hiểu hơn về Deadlock. Thì bây giờ mình mời các bạn cùng xây dựng một ứng dụng thật, có khả năng gây ra Deadlock nhé.

Chúng ta vẫn sẽ đến với kịch bản xây dựng ứng dụng ngân hàng như các bài học về đồng bộ Thread mà các bạn đã rất quen thuộc. Giả sử hôm nay sếp ngân hàng đến nói với bạn rằng hãy xây dựng thêm chức năng chuyển khoản giữa các tài khoản với nhau. Sau khi chức năng xây dựng xong, ở một gia đình nọ có hai vợ chồng. Anh chồng có mở tài khoản riêng, và cô vợ cũng có tài khoản riêng ở cùng một ngân hàng. Một ngày nọ, do không hiểu ý nhau, anh chồng vô tài khoản của ảnh chuyển cho cô vợ 3 triệu VND, đồng thời cùng lúc đó, cô vợ cũng vô tài khoản của cổ chuyển cho anh chồng 2 triệu VND. Vấn đề trớ trêu là 2 người này cùng gần như thực hiện đồng thời lệnh chuyển tiền. Và lạ thay, ứng dụng bị treo, có nghĩa là 2 vợ chồng họ đợi hoài mà lệnh chuyển tiền vẫn không thành công. Tại sao vậy, chúng ta cùng xem qua đoạn code mà bạn đã viết.

Giả sử lớp BankAccount của bạn có sẵn các phương thức rút (withdraw) và nạp (deposit) được xây dựng từ các bài trước. Ở đây mình viết ngắn lại hơn so với các bài trước, bỏ qua các kiểm tra số dư và giả lập thời gian rút/nạp tiền ra cho lớp này ngắn gọn nhất có thể.

public class BankAccount extends Object {
 
    long amount = 5000000; // Số tiền có trong tài khoản
    String accountName = "";
     
    public BankAccount(String accountName) {
        this.accountName = accountName;
    }
 
    public synchronized void withdraw(long withdrawAmount) {
        // In ra trạng thái bắt đầu trừ tiền
        System.out.println(accountName + " withdrawing...");
         
        // Trừ tiền
        amount -= withdrawAmount;
    }
     
    public synchronized void deposit(long depositAmount) {
        // In ra trạng thái bắt đầu nạp tiền
        System.out.println(accountName + " depositting...");
         
        // Nạp tiền
        amount += depositAmount;
    }
}

Và giờ bạn xây dựng thêm phương thức chuyển tiền cho lớp này. Bạn đặt tên nó là transferTo(). Do quá cẩn thận, bạn viết thêm các khối synchronized trong phương thức này. Code đầy đủ của lớp BankAccount sẽ như sau.

public class BankAccount extends Object {
     
    long amount = 5000000; // Số tiền có trong tài khoản
    String accountName = "";
     
    public BankAccount(String accountName) {
        this.accountName = accountName;
    }
 
    public synchronized void withdraw(long withdrawAmount) {
        // In ra trạng thái bắt đầu trừ tiền
        System.out.println(accountName + " withdrawing...");
         
        // Trừ tiền
        amount -= withdrawAmount;
    }
     
    public synchronized void deposit(long depositAmount) {
        // In ra trạng thái bắt đầu nạp tiền
        System.out.println(accountName + " depositting...");
         
        // Nạp tiền
        amount += depositAmount;
    }
     
    public void transferTo(BankAccount toAccount, long transferAmount) {
        synchronized(this) {
            // Rút tiền từ tài khoản này
            this.withdraw(transferAmount);
             
            synchronized(toAccount) {
                // Nạp tiền vào toAccount
                toAccount.deposit(transferAmount);
            }
             
            // In số dư tài khoản khi kết thúc quá trình chuyển tiền
            System.out.println("The amount of " + accountName + " is: " + amount);
        }
    }
}

Ở phương thức main() chỉ việc gọi các lệnh chuyển khoản như sau.

public static void main(String[] args) {
    // Khai báo tài khoản của anh chồng và cô vợ riêng
    BankAccount husbandAccount = new BankAccount("Husband's Account");
    BankAccount wifeAccount = new BankAccount("Wife's Account");
 
    // Anh chồng muốn chuyển 3 triệu từ tài khoản của ảnh qua tài khoản cô vợ
    Thread husbandThread = new Thread() {
        @Override
        public void run() {
            husbandAccount.transferTo(wifeAccount, 3000000);
        }
    };
 
    // Cô vợ muốn chuyển 2 triệu từ tài khoản của cổ qua tài khoản của anh chồng
    Thread wifeThread = new Thread() {
        @Override
        public void run() {
            wifeAccount.transferTo(husbandAccount, 2000000);
        }
    };
 
    // Hai người thực hiện lệnh chuyển tiền gần như đồng thời
    husbandThread.start();
    wifeThread.start();
}

Và đây là kết quả khi thực thi chương trình.

Kết quả in ra console khi thực thi chương trìnhKết quả in ra console khi thực thi chương trìnhKết quả in ra console khi thực thi chương trình

Như bạn cũng đã hiểu rồi đó. Bạn xem, cả 2 Thread khi được khởi chạy, chỉ làm được mỗi thao tác trừ tiền của chính tài khoản nguồn. Còn sau đó đến phương thức nạp tiền cho tài khoản đích thì… không thể gọi đến được. Ứng dụng lúc này vẫn đang chạy, bằng chứng là nút Stop hình vuông màu đỏ bên cạnh tab Console vẫn sáng, tức là ứng dụng vẫn chạy và Eclipse khi này vẫn đang cho phép bạn dừng ứng dụng lại bất cứ khi nào. Cái sự ứng dụng mãi mãi không thể kết thúc được là vì khi này bản thân mỗi Thread khi được khởi tạo đã giữ lấy Lock trên Monitor của một tài khoản, các Thread khác không thể can thiệp vào tài khoản mà mỗi Thread đang giữ được. Việc mỗi Thread đều giữ một tài khoản và chờ đến lượt sử dụng tài khoản khác (cũng đang bị giữ bởi một Thread khác) như vậy được gọi là Deadlock.

Nó tương tự như sơ đồ sau.

Sơ đồ gây ra Deadlock của ví dụSơ đồ gây ra Deadlock của ví dụSơ đồ gây ra Deadlock của ví dụ trên

Deadlock Xuất Hiện Khi Nào?

Như bạn đã làm quen trên đây, Deadlock thường xuất hiện khi chúng ta quá lạm dụng từ khóa synchronized. Nó khiến cho các Thread nắm giữ các đối tượng dùng chung mãi mãi mà không chịu trả ra cho các đối tượng khác dùng.

Tuy nhiên thì bài học về Deadlock này cũng chỉ là một bài học về lý thuyết, theo mình thì nó mang tính cảnh báo là chính. Trong thực tế sẽ rất khó để có thể xảy ra tình trạng Deadlock như thế này. Tuy nhiên, dù khó xảy ra nhưng nó cũng đã từng xảy ra, và nhiệm vụ của chúng ta là các lập trình viên, chúng ta vẫn cần phải biết và chuẩn bị các kiến thức cần thiết về nó.

Tránh Deadlock Và Xử Lý Như Thế Nào Nếu Gặp Deadlock?

Như mình nói, thì Deadlock rất khó xảy ra trong thực tế. Thực sự thì trong quãng đời lập trình của mình, mình chưa hề đụng đến trường hợp này. Một phần như mình biết thì các ứng dụng của chúng ta tuy có sử dụng nhiều Thread nhưng chưa đến mức đủ nhiều và phức tạp để gây ra sự xung đột như các ví dụ phía trên.

Nhưng dù cho nó có khó xảy ra đi nữa. Chúng ta vẫn phải nên biết để mà tránh đến mức thấp nhất nguy cơ xảy ra hiện tượng Deadlock này. Và cho dù tránh né như vậy, mà nếu lỡ chẳng may một ngày nào đó Deadlock xảy ra với ứng dụng của bạn thì sao. Thì bạn vẫn nên chuẩn bị sẵn các kiến thức để mà sửa lại source code và phát hành bản sửa lỗi ngay lập tức chớ sao. Mục này sẽ nói chung về việc tránh, và sửa lỗi, đối với Deadlock như thế nào.

Đầu tiên, theo mình, để tránh Deadlock, bạn vẫn phải nên hiểu rõ code của bạn. Bạn phải nắm được các Thread đã dùng có sử dụng các tài nguyên nào. Có nhiều Thread đang chiếm dụng các tài nguyên dùng chung hay không. Đảm bảo các Thread nếu đang chiếm dụng tài nguyên rồi thì cuối cùng cũng phải trả tài nguyên về hệ thống một cách nhanh nhất, để các Thread khác có cơ hội sử dụng và kết thúc các đời sống của các Thread đó. Dễ nhất là bạn đừng có viết lồng các khối synchronized lại như ví dụ trên kia là bảo đảm rất rất khó có thể xảy ra Deadlock.

Sau đó, nếu ứng dụng của bạn khi thực thi ở môi trường thực tế, mà gặp phải Deadlock. Nếu project của bạn tương đối nhỏ, bạn cũng có thể phát hiện bằng cách đọc code và suy luận. Nhưng nếu như project quá lớn, bạn có thể dùng đến một số công cụ có chức năng Thread Dump. Như hình bên dưới mình nhờ đến công cụ có sẵn trong thư mục /bin của JDK, công cụ có tên jvisualvm. Cách sử dụng công cụ và tìm Thread Dump thì bạn có thể tham khảo thêm trên mạng, hoặc bạn có thể xem ở link này để biết tất cả các công cụ, kể cả jvisualvm nhé.

Công cụ phát hiện Deadlock ở các Thread và các tài nguyên liên quanCông cụ phát hiện Deadlock ở các Thread và các tài nguyên liên quanCông cụ phát hiện Deadlock ở các Thread và các tài nguyên liên quan

Kết Luận

Bài học về Deadlock kết thúc tại đây. Bạn có thể thấy rằng Deadlock vẫn liên quan đến chuỗi kiến thức về Thread mạnh mẽ. Qua đó bạn đã thấy tầm quan trọng của Thread trong Java rồi đúng không nào. Tuy nhiên chúng ta vẫn còn phải nói nhiều về Thread, và bài học sau cũng không nằm ngoài kiến thức thú vị về Thread.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy ủng hộ blog bằng cách:
– Đánh giá 5 sao ở mỗi bài viết nếu thấy thích.
– Comment bên dưới mỗi bài viết nếu có thắc mắc.
– Để lại địa chỉ email của bạn ở thanh bên phải để nhận được thông báo sớm nhất khi có bài viết mới.
– Chia sẻ các bài viết của Yellow Code Books đến nhiều người khác.
– Ủng hộ blog theo hướng dẫn ở thanh bên phải để blog ngày càng phát triển hơn.

Bài Kế Tiếp

Như đã nói, chúng ta sẽ làm quen với một kiến thức thâm sâu khác của Thread. Nó có tên là Threadpool.