Java Thread Deadlock dan Livelock

1. Gambaran keseluruhan

Walaupun multi-threading membantu meningkatkan prestasi aplikasi, ia juga dilengkapi dengan beberapa masalah. Dalam tutorial ini, kita akan melihat dua masalah seperti itu, kebuntuan dan kunci hidup, dengan bantuan contoh Java.

2. Kebuntuan

2.1. Apakah Kebuntuan?

Kebuntuan berlaku apabila dua atau lebih utas menunggu selamanya untuk kunci atau sumber yang dipegang oleh utas yang lain . Akibatnya, aplikasi mungkin terhenti atau gagal kerana benang yang buntu tidak dapat maju.

Masalah ahli falsafah makan klasik menunjukkan masalah penyegerakan dalam persekitaran berbilang benang dan sering digunakan sebagai contoh kebuntuan.

2.2. Contoh Kebuntuan

Pertama, mari kita lihat contoh Java sederhana untuk memahami kebuntuan.

Dalam contoh ini, kami akan membuat dua utas, T1 dan T2 . Thread T1 panggilan operation1 , dan benang T2 panggilan operasi .

Untuk menyelesaikan operasi mereka, utas T1 perlu mendapatkan kunci1 terlebih dahulu dan kemudian kunci2 , sedangkan utas T2 perlu memperoleh kunci2 terlebih dahulu dan kemudian kunci1 . Jadi, pada dasarnya, kedua-dua utas berusaha mendapatkan kunci dalam urutan yang bertentangan.

Sekarang, mari tulis kelas DeadlockExample :

public class DeadlockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { DeadlockExample deadlock = new DeadlockExample(); new Thread(deadlock::operation1, "T1").start(); new Thread(deadlock::operation2, "T2").start(); } public void operation1() { lock1.lock(); print("lock1 acquired, waiting to acquire lock2."); sleep(50); lock2.lock(); print("lock2 acquired"); print("executing first operation."); lock2.unlock(); lock1.unlock(); } public void operation2() { lock2.lock(); print("lock2 acquired, waiting to acquire lock1."); sleep(50); lock1.lock(); print("lock1 acquired"); print("executing second operation."); lock1.unlock(); lock2.unlock(); } // helper methods }

Mari kita jalankan contoh kebuntuan ini dan perhatikan outputnya:

Thread T1: lock1 acquired, waiting to acquire lock2. Thread T2: lock2 acquired, waiting to acquire lock1.

Sebaik sahaja kita menjalankan program, kita dapat melihat bahawa program tersebut mengakibatkan kebuntuan dan tidak pernah keluar. Log menunjukkan bahawa utas T1 sedang menunggu kunci2 , yang dipegang oleh utas T2 . Begitu juga, benang T2 sedang menunggu kunci1 , yang dipegang oleh utas T1 .

2.3. Mengelakkan Kebuntuan

Kebuntuan adalah masalah bersamaan di Jawa. Oleh itu, kita harus merancang aplikasi Java untuk mengelakkan kemungkinan berlaku kebuntuan.

Sebagai permulaan, kita harus mengelakkan keperluan memperoleh banyak kunci untuk utas. Walau bagaimanapun, jika benang memerlukan banyak kunci, kita harus memastikan bahawa setiap utas memperoleh kunci dalam urutan yang sama, untuk mengelakkan ketergantungan siklik dalam pemerolehan kunci .

Kami juga dapat menggunakan percubaan kunci berjangka , seperti kaedah tryLock di antara muka Kunci , untuk memastikan bahawa utas tidak menyekat tanpa batas jika tidak dapat memperoleh kunci.

3. Livelock

3.1. Apa itu Livelock

Livelock adalah masalah lain yang sama dan serupa dengan kebuntuan. Dalam kunci hidup, dua atau lebih utas terus memindahkan keadaan antara satu sama lain daripada menunggu tanpa henti seperti yang kita lihat dalam contoh kebuntuan. Akibatnya, utas tidak dapat menjalankan tugas masing-masing.

Contoh terbaik dari kunci hidup adalah sistem pemesejan di mana, apabila berlaku pengecualian, pengguna mesej mengembalikan urus niaga dan meletakkan pesan kembali ke kepala barisan. Kemudian mesej yang sama berulang kali dibaca dari barisan, hanya untuk menyebabkan pengecualian lain dan dimasukkan kembali ke dalam barisan. Pengguna tidak akan mengambil pesanan lain dari barisan.

3.2. Contoh Livelock

Sekarang, untuk menunjukkan keadaan kunci hidup, kita akan mengambil contoh kebuntuan yang sama yang telah kita bincangkan sebelumnya. Dalam contoh ini juga, benang T1 panggilan operation1 dan benang T2 panggilan operation2 . Walau bagaimanapun, kami akan mengubah logik operasi ini sedikit.

Kedua-dua utas memerlukan dua kunci untuk menyelesaikan kerja mereka. Setiap utas memperoleh kunci pertama tetapi mendapati kunci kedua tidak tersedia. Oleh itu, untuk membiarkan utas yang lain selesai terlebih dahulu, setiap utas melepaskan kunci pertama dan berusaha mendapatkan kedua kunci itu sekali lagi.

Mari tunjukkan kunci hidup dengan kelas LivelockExample :

public class LivelockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { LivelockExample livelock = new LivelockExample(); new Thread(livelock::operation1, "T1").start(); new Thread(livelock::operation2, "T2").start(); } public void operation1() { while (true) { tryLock(lock1, 50); print("lock1 acquired, trying to acquire lock2."); sleep(50); if (tryLock(lock2)) { print("lock2 acquired."); } else { print("cannot acquire lock2, releasing lock1."); lock1.unlock(); continue; } print("executing first operation."); break; } lock2.unlock(); lock1.unlock(); } public void operation2() { while (true) { tryLock(lock2, 50); print("lock2 acquired, trying to acquire lock1."); sleep(50); if (tryLock(lock1)) { print("lock1 acquired."); } else { print("cannot acquire lock1, releasing lock2."); lock2.unlock(); continue; } print("executing second operation."); break; } lock1.unlock(); lock2.unlock(); } // helper methods }

Sekarang, mari kita jalankan contoh ini:

Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: cannot acquire lock2, releasing lock1. Thread T2: cannot acquire lock1, releasing lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T1: cannot acquire lock2, releasing lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: cannot acquire lock1, releasing lock2. ..

Seperti yang kita lihat di log, kedua-dua utas berulang kali memperoleh dan melepaskan kunci. Oleh kerana itu, tiada satu pun utas yang dapat menyelesaikan operasi.

3.3. Mengelakkan Livelock

Untuk mengelakkan kunci hidup, kita perlu melihat keadaan yang menyebabkan kunci hidup dan kemudian mencari penyelesaian yang sesuai.

Sebagai contoh, jika kita mempunyai dua utas yang berulang kali memperoleh dan melepaskan kunci, yang menghasilkan kunci hidup, kita dapat merancang kodnya supaya benang itu kembali memperoleh kunci pada selang waktu secara rawak. Ini akan memberi peluang yang baik untuk memperoleh kunci yang mereka perlukan.

Kaedah lain untuk mengatasi masalah hidup dalam contoh sistem pesanan yang telah kita bincangkan sebelumnya adalah meletakkan mesej yang gagal dalam barisan yang berasingan untuk diproses lebih lanjut dan bukannya meletakkannya kembali dalam barisan yang sama.

4. Kesimpulan

Dalam tutorial ini, kita telah membincangkan kebuntuan dan kunci hidup. Juga, kami telah melihat contoh Java untuk menunjukkan setiap masalah ini dan secara ringkas menyentuh bagaimana kita dapat menghindarinya.

Seperti biasa, kod lengkap yang digunakan dalam contoh ini boleh didapati di GitHub.