Java Thread Deadlock dan Livelock

1. Ikhtisar

Sementara multi-threading membantu meningkatkan kinerja aplikasi, itu juga disertai dengan beberapa masalah. Dalam tutorial ini, kita akan melihat dua masalah seperti itu, deadlock dan livelock, dengan bantuan contoh Java.

2. Kebuntuan

2.1. Apa Itu Deadlock?

Kebuntuan terjadi ketika dua atau lebih utas menunggu selamanya untuk kunci atau sumber daya yang dipegang oleh utas lainnya . Akibatnya, aplikasi mungkin terhenti atau gagal karena thread yang menemui jalan buntu tidak dapat berkembang.

Masalah filsuf makan klasik dengan baik menunjukkan masalah sinkronisasi dalam lingkungan multi-threaded 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 memanggil operasi1 , dan operasi panggilan thread T2 .

Untuk menyelesaikan operasinya, utas T1 perlu memperoleh kunci1 terlebih dahulu lalu mengunci2 , sedangkan utas T2 perlu memperoleh kunci2 terlebih dahulu dan kemudian mengunci1 . Jadi, pada dasarnya, kedua utas mencoba mendapatkan kunci dalam urutan yang berlawanan.

Sekarang, mari kita 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 }

Sekarang mari kita jalankan contoh kebuntuan ini dan perhatikan hasilnya:

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

Setelah kami menjalankan program, kami dapat melihat bahwa program tersebut menghasilkan jalan buntu dan tidak pernah keluar. Log menunjukkan bahwa thread T1 sedang menunggu lock2 , yang ditahan oleh thread T2 . Demikian pula, utas T2 menunggu kunci1 , yang ditahan oleh utas T1 .

2.3. Menghindari Deadlock

Kebuntuan adalah masalah konkurensi umum di Java. Oleh karena itu, kita harus merancang aplikasi Java untuk menghindari kemungkinan kondisi kebuntuan.

Untuk memulainya, kita harus menghindari kebutuhan untuk memperoleh banyak kunci untuk sebuah utas. Namun, jika utas memang memerlukan beberapa kunci, kita harus memastikan bahwa setiap utas memperoleh kunci dalam urutan yang sama, untuk menghindari ketergantungan siklik dalam perolehan kunci .

Kami juga dapat menggunakan upaya kunci berjangka waktu , seperti metode tryLock di antarmuka Lock , untuk memastikan bahwa utas tidak memblokir tanpa batas jika tidak dapat memperoleh kunci.

3. Livelock

3.1. Apa Itu Livelock

Livelock adalah masalah konkurensi lainnya dan mirip dengan kebuntuan. Di livelock, dua atau lebih utas terus mentransfer status antara satu sama lain alih-alih menunggu tanpa batas seperti yang kita lihat dalam contoh kebuntuan. Akibatnya, utas tidak dapat melakukan tugasnya masing-masing.

Contoh yang bagus dari kunci langsung adalah sistem pesan di mana, ketika pengecualian terjadi, konsumen pesan memutar kembali transaksi dan meletakkan pesan kembali ke kepala antrian. Kemudian pesan yang sama berulang kali dibaca dari antrian, hanya menyebabkan pengecualian lain dan dimasukkan kembali ke antrian. Konsumen tidak akan pernah menerima pesan lain dari antrian.

3.2. Contoh Livelock

Sekarang, untuk mendemonstrasikan kondisi livelock, kita akan mengambil contoh deadlock yang sama yang telah kita bahas sebelumnya. Dalam contoh ini juga, thread T1 memanggil operation1 dan thread T2 memanggil operation2 . Namun, kami akan sedikit mengubah logika operasi ini.

Kedua utas membutuhkan dua kunci untuk menyelesaikan pekerjaannya. Setiap utas memperoleh kunci pertamanya tetapi menemukan bahwa kunci kedua tidak tersedia. Jadi, untuk membiarkan utas lainnya selesai terlebih dahulu, setiap utas melepaskan kunci pertamanya dan mencoba mendapatkan kedua kunci itu lagi.

Mari kita peragakan livelock 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 bisa kita lihat di log, kedua utas berulang kali memperoleh dan melepaskan kunci. Karena itu, tidak ada utas yang dapat menyelesaikan operasi.

3.3. Menghindari Livelock

Untuk menghindari livelock, kita perlu melihat kondisi yang menyebabkan livelock dan kemudian muncul solusi yang sesuai.

Misalnya, jika kita memiliki dua utas yang berulang kali memperoleh dan melepaskan kunci, menghasilkan kunci langsung, kita dapat mendesain kode sehingga utas mencoba kembali mendapatkan kunci secara acak. Ini akan memberi benang kesempatan yang adil untuk mendapatkan kunci yang mereka butuhkan.

Cara lain untuk mengatasi masalah keaktifan dalam contoh sistem perpesanan yang telah kita bahas sebelumnya adalah dengan meletakkan pesan yang gagal dalam antrian terpisah untuk diproses lebih lanjut daripada menempatkannya kembali dalam antrian yang sama lagi.

4. Kesimpulan

Dalam tutorial ini, kita telah membahas deadlock dan livelock. Selain itu, kami telah melihat contoh Java untuk mendemonstrasikan masing-masing masalah ini dan secara singkat menyentuh bagaimana kami dapat menghindarinya.

Seperti biasa, kode lengkap yang digunakan dalam contoh ini dapat ditemukan di GitHub.