JavaRush /Java Blog /Random-ID /Dasar-dasar Konkurensi: Deadlock dan Object Monitor (bagi...
Snusmum
Level 34
Хабаровск

Dasar-dasar Konkurensi: Deadlock dan Object Monitor (bagian 1, 2) (terjemahan artikel)

Dipublikasikan di grup Random-ID
Artikel sumber: http://www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html Diposting oleh Martin Mois Artikel ini adalah bagian dari kursus Java Concurrency Fundamentals kami . Dalam kursus ini, Anda akan mempelajari keajaiban paralelisme. Anda akan mempelajari dasar-dasar paralelisme dan kode paralel, serta memahami konsep-konsep seperti atomisitas, sinkronisasi, dan keamanan thread. Lihatlah di sini !

Isi

1. Keaktifan  1.1 Kebuntuan  1.2 Kelaparan 2. Pemantau objek dengan wait() dan notify()  2.1 Blok tersinkronisasi bertumpuk dengan wait() dan notify()  2.2 Kondisi dalam blok tersinkronisasi 3. Desain untuk multi-threading  3.1 Objek abadi  3.2 Desain API  3.3 Penyimpanan thread lokal
1. Vitalitas
Saat mengembangkan aplikasi yang menggunakan paralelisme untuk mencapai tujuannya, Anda mungkin menghadapi situasi di mana thread yang berbeda dapat saling memblokir. Jika aplikasi berjalan lebih lambat dari yang diharapkan dalam situasi ini, kami dapat mengatakan bahwa aplikasi tersebut tidak berjalan seperti yang diharapkan. Di bagian ini, kita akan melihat lebih dekat masalah-masalah yang dapat mengancam kelangsungan hidup aplikasi multi-thread.
1.1 Saling memblokir
Istilah deadlock sudah terkenal di kalangan pengembang perangkat lunak dan bahkan sebagian besar pengguna awam menggunakannya dari waktu ke waktu, meskipun tidak selalu dalam arti yang benar. Sebenarnya, istilah ini berarti bahwa masing-masing dari dua (atau lebih) thread menunggu thread lainnya melepaskan sumber daya yang dikunci olehnya, sedangkan thread pertama sendiri telah mengunci sumber daya yang menunggu untuk diakses oleh thread kedua: Untuk lebih memahami masalahnya, lihat Thread 1: locks resource A, waits for resource B Thread 2: locks resource B, waits for resource A kode berikut: public class Deadlock implements Runnable { private static final Object resource1 = new Object(); private static final Object resource2 = new Object(); private final Random random = new Random(System.currentTimeMillis()); public static void main(String[] args) { Thread myThread1 = new Thread(new Deadlock(), "thread-1"); Thread myThread2 = new Thread(new Deadlock(), "thread-2"); myThread1.start(); myThread2.start(); } public void run() { for (int i = 0; i < 10000; i++) { boolean b = random.nextBoolean(); if (b) { System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1."); synchronized (resource1) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1."); System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2."); synchronized (resource2) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2."); } } } else { System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 2."); synchronized (resource2) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 2."); System.out.println("[" + Thread.currentThread().getName() + "] Trying to lock resource 1."); synchronized (resource1) { System.out.println("[" + Thread.currentThread().getName() + "] Locked resource 1."); } } } } } } Seperti yang Anda lihat dari kode di atas, dua utas dimulai dan mencoba mengunci dua sumber daya statis. Namun untuk melakukan deadlock, kita memerlukan urutan yang berbeda untuk kedua thread, jadi kita menggunakan instance objek Random untuk memilih sumber daya mana yang ingin dikunci oleh thread terlebih dahulu. Jika variabel boolean b benar, maka sumber daya1 dikunci terlebih dahulu, lalu thread mencoba memperoleh kunci untuk sumber daya2. Jika b salah, maka thread mengunci sumber daya2 dan kemudian mencoba memperoleh sumber daya1. Program ini tidak perlu berjalan lama untuk mencapai kebuntuan pertama, yaitu Program akan hang selamanya jika kita tidak menghentikannya: [thread-1] Trying to lock resource 1. [thread-1] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 1. [thread-2] Locked resource 1. [thread-1] Trying to lock resource 2. [thread-1] Locked resource 2. [thread-2] Trying to lock resource 2. [thread-1] Trying to lock resource 1. Dalam proses ini, tapak-1 telah memperoleh kunci sumber daya2 dan menunggu kunci sumber daya1, sedangkan tapak-2 memiliki kunci sumber daya1 dan menunggu sumber daya2. Jika kita menetapkan nilai variabel boolean b dalam kode di atas menjadi true, kita tidak akan dapat mengamati kebuntuan apa pun karena urutan kunci permintaan thread-1 dan thread-2 akan selalu sama. Dalam situasi ini, salah satu dari dua thread akan memperoleh kunci terlebih dahulu dan kemudian meminta kunci kedua, yang masih tersedia karena thread lainnya sedang menunggu kunci pertama. Secara umum, kita dapat membedakan kondisi-kondisi yang diperlukan agar kebuntuan dapat terjadi: - Eksekusi bersama: Ada sumber daya yang hanya dapat diakses oleh satu thread pada setiap saat. - Resource Hold: Saat memperoleh satu sumber daya, thread mencoba memperoleh kunci lain pada beberapa sumber daya unik. - Tidak ada preemption: Tidak ada mekanisme untuk melepaskan sumber daya jika salah satu thread menahan kunci untuk jangka waktu tertentu. - Circular Wait: Selama eksekusi, kumpulan thread terjadi di mana dua (atau lebih) thread menunggu satu sama lain untuk melepaskan sumber daya yang telah dikunci. Meskipun daftar kondisinya tampak panjang, tidak jarang aplikasi multi-thread yang dijalankan dengan baik mengalami masalah kebuntuan. Namun Anda dapat mencegahnya jika Anda dapat menghilangkan salah satu kondisi di atas: - Eksekusi bersama: kondisi ini seringkali tidak dapat dihilangkan ketika sumber daya harus digunakan oleh satu orang saja. Tapi ini tidak harus menjadi alasannya. Saat menggunakan sistem DBMS, solusi yang mungkin, daripada menggunakan kunci pesimis pada beberapa baris tabel yang perlu diperbarui, adalah dengan menggunakan teknik yang disebut Penguncian Optimis . - Cara untuk menghindari menahan sumber daya sambil menunggu sumber daya eksklusif lainnya adalah dengan mengunci semua sumber daya yang diperlukan di awal algoritme dan melepaskan semuanya jika tidak mungkin mengunci semuanya sekaligus. Tentu saja, hal ini tidak selalu memungkinkan; mungkin sumber daya yang memerlukan penguncian tidak diketahui sebelumnya, atau pendekatan ini hanya akan menyebabkan pemborosan sumber daya. - Jika kunci tidak dapat diperoleh dengan segera, cara untuk mengatasi kemungkinan kebuntuan adalah dengan memberikan batas waktu. Misalnya, kelas ReentrantLockdari SDK memberikan kemampuan untuk mengatur tanggal kedaluwarsa kunci. - Seperti yang kita lihat dari contoh di atas, kebuntuan tidak terjadi jika urutan permintaan tidak berbeda di antara thread yang berbeda. Ini mudah untuk dikontrol jika Anda dapat memasukkan semua kode pemblokiran ke dalam satu metode yang harus dilalui semua thread. Dalam aplikasi yang lebih canggih, Anda bahkan mungkin mempertimbangkan untuk menerapkan sistem deteksi kebuntuan. Di sini Anda perlu menerapkan beberapa kemiripan pemantauan thread, di mana setiap thread melaporkan bahwa ia telah berhasil memperoleh kunci dan mencoba memperoleh kunci tersebut. Jika thread dan kunci dimodelkan sebagai grafik berarah, Anda dapat mendeteksi ketika dua thread berbeda menyimpan sumber daya saat mencoba mengakses sumber daya terkunci lainnya secara bersamaan. Jika Anda kemudian dapat memaksa thread pemblokiran untuk melepaskan sumber daya yang diperlukan, Anda dapat menyelesaikan situasi kebuntuan secara otomatis.
1.2 Puasa
Penjadwal memutuskan thread mana dalam status RUNNABLE yang harus dijalankan selanjutnya. Keputusan ini didasarkan pada prioritas thread; oleh karena itu, thread dengan prioritas lebih rendah menerima waktu CPU lebih sedikit dibandingkan dengan thread dengan prioritas lebih tinggi. Solusi yang tampaknya masuk akal juga dapat menimbulkan masalah jika disalahgunakan. Jika thread berprioritas tinggi mengeksekusi sebagian besar waktu, maka thread berprioritas rendah tampaknya kelaparan karena tidak mendapatkan cukup waktu untuk melakukan pekerjaannya dengan benar. Oleh karena itu, disarankan untuk menetapkan prioritas thread hanya jika ada alasan kuat untuk melakukannya. Contoh yang tidak jelas mengenai kekurangan thread diberikan, misalnya, dengan metode finalize(). Ini menyediakan cara bagi bahasa Java untuk mengeksekusi kode sebelum suatu objek dikumpulkan dari sampah. Namun jika Anda melihat prioritas thread penyelesaian, Anda akan melihat bahwa thread tersebut tidak berjalan dengan prioritas tertinggi. Akibatnya, kekurangan thread terjadi ketika metode finalize() objek Anda menghabiskan terlalu banyak waktu dibandingkan dengan kode lainnya. Masalah lain dengan waktu eksekusi muncul dari kenyataan bahwa tidak ditentukan urutan thread yang melintasi blok yang disinkronkan. Ketika banyak thread paralel melintasi beberapa kode yang dibingkai dalam blok tersinkronisasi, mungkin saja beberapa thread harus menunggu lebih lama dari yang lain sebelum memasuki blok. Secara teori, mereka mungkin tidak akan pernah sampai di sana. Solusi untuk masalah ini adalah apa yang disebut pemblokiran “adil”. Penguncian yang adil memperhitungkan waktu tunggu thread saat menentukan siapa yang harus dilewati selanjutnya. Contoh implementasi penguncian yang adil tersedia di Java SDK: java.util.concurrent.locks.ReentrantLock. Jika konstruktor digunakan dengan flag boolean yang disetel ke true, maka ReentrantLock memberikan akses ke thread yang telah menunggu paling lama. Hal ini menjamin tidak adanya kelaparan namun, pada saat yang sama, menyebabkan masalah pengabaian prioritas. Oleh karena itu, proses dengan prioritas lebih rendah yang sering menunggu pada penghalang ini mungkin berjalan lebih sering. Terakhir, kelas ReentrantLock hanya dapat mempertimbangkan thread yang menunggu untuk dikunci, mis. benang yang diluncurkan cukup sering dan mencapai penghalang. Jika prioritas thread terlalu rendah, maka hal ini tidak akan sering terjadi, dan oleh karena itu thread dengan prioritas tinggi akan tetap melewati kunci lebih sering.
2. Monitor objek bersama dengan wait() dan notify()
Dalam komputasi multi-thread, situasi yang umum terjadi adalah beberapa thread pekerja menunggu produsernya membuatkan beberapa pekerjaan untuk mereka. Namun, seperti yang telah kita pelajari, menunggu secara aktif dalam satu lingkaran sambil memeriksa nilai tertentu bukanlah pilihan yang baik dalam hal waktu CPU. Menggunakan metode Thread.sleep() dalam situasi ini juga tidak terlalu cocok jika kita ingin memulai pekerjaan segera setelah tiba. Untuk tujuan ini, bahasa pemrograman Java memiliki struktur lain yang dapat digunakan dalam skema ini: wait() dan notify(). Metode wait(), yang diwarisi oleh semua objek dari kelas java.lang.Object, dapat digunakan untuk menangguhkan thread saat ini dan menunggu hingga thread lain membangunkan kita menggunakan metode notify(). Agar dapat bekerja dengan benar, thread yang memanggil metode wait() harus memiliki kunci yang sebelumnya diperoleh menggunakan kata kunci tersinkronisasi. Ketika wait() dipanggil, kunci dilepaskan dan thread menunggu hingga thread lain yang sekarang memegang kunci tersebut memanggil notify() pada instance objek yang sama. Dalam aplikasi multi-thread, secara alami mungkin ada lebih dari satu thread yang menunggu notifikasi pada beberapa objek. Oleh karena itu, ada dua metode berbeda untuk membangunkan thread: notify() dan notifyAll(). Saat metode pertama membangunkan salah satu thread yang menunggu, metode notifyAll() membangunkan semuanya. Namun perlu diketahui bahwa, seperti halnya kata kunci sinkronisasi, tidak ada aturan yang menentukan thread mana yang akan dibangunkan berikutnya ketika notify() dipanggil. Dalam contoh sederhana dengan produsen dan konsumen, hal ini tidak menjadi masalah, karena kita tidak peduli thread mana yang dibangun. Kode berikut menunjukkan bagaimana wait() dan notify() dapat digunakan untuk menyebabkan thread konsumen menunggu pekerjaan baru diantrekan oleh thread produsen: package a2; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; public class ConsumerProducer { private static final Queue queue = new ConcurrentLinkedQueue(); private static final long startMillis = System.currentTimeMillis(); public static class Consumer implements Runnable { public void run() { while (System.currentTimeMillis() < (startMillis + 10000)) { synchronized (queue) { try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (!queue.isEmpty()) { Integer integer = queue.poll(); System.out.println("[" + Thread.currentThread().getName() + "]: " + integer); } } } } public static class Producer implements Runnable { public void run() { int i = 0; while (System.currentTimeMillis() < (startMillis + 10000)) { queue.add(i++); synchronized (queue) { queue.notify(); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (queue) { queue.notifyAll(); } } } public static void main(String[] args) throws InterruptedException { Thread[] consumerThreads = new Thread[5]; for (int i = 0; i < consumerThreads.length; i++) { consumerThreads[i] = new Thread(new Consumer(), "consumer-" + i); consumerThreads[i].start(); } Thread producerThread = new Thread(new Producer(), "producer"); producerThread.start(); for (int i = 0; i < consumerThreads.length; i++) { consumerThreads[i].join(); } producerThread.join(); } } Metode main() memulai lima thread konsumen dan satu thread produsen, lalu menunggu hingga thread tersebut selesai. Thread produser kemudian menambahkan nilai baru ke antrian dan memberitahukan semua thread yang menunggu bahwa sesuatu telah terjadi. Konsumen mendapatkan kunci antrian (yaitu satu konsumen acak) kemudian tidur, untuk dimunculkan kemudian ketika antrian sudah penuh kembali. Ketika produsen menyelesaikan pekerjaannya, ia memberitahukan semua konsumen untuk membangunkan mereka. Jika kami tidak melakukan langkah terakhir, thread konsumen akan menunggu selamanya untuk notifikasi berikutnya karena kami tidak menetapkan batas waktu untuk menunggu. Sebagai gantinya, kita dapat menggunakan metode wait(long timeout) untuk dibangunkan setidaknya setelah beberapa waktu berlalu.
2.1 Blok tersinkronisasi bersarang dengan wait() dan notify()
Seperti yang dinyatakan di bagian sebelumnya, memanggil wait() pada monitor suatu objek hanya akan melepaskan kunci pada monitor tersebut. Kunci lain yang dipegang oleh utas yang sama tidak dilepaskan. Seperti yang mudah dimengerti, dalam pekerjaan sehari-hari mungkin saja thread yang memanggil wait() menahan kunci lebih jauh. Jika thread lain juga menunggu kunci ini, situasi kebuntuan mungkin terjadi. Mari kita lihat penguncian pada contoh berikut: public class SynchronizedAndWait { private static final Queue queue = new ConcurrentLinkedQueue(); public synchronized Integer getNextInt() { Integer retVal = null; while (retVal == null) { synchronized (queue) { try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } retVal = queue.poll(); } } return retVal; } public synchronized void putInt(Integer value) { synchronized (queue) { queue.add(value); queue.notify(); } } public static void main(String[] args) throws InterruptedException { final SynchronizedAndWait queue = new SynchronizedAndWait(); Thread thread1 = new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { queue.putInt(i); } } }); Thread thread2 = new Thread(new Runnable() { public void run() { for (int i = 0; i < 10; i++) { Integer nextInt = queue.getNextInt(); System.out.println("Next int: " + nextInt); } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } } Seperti yang kita pelajari sebelumnya , menambahkan sinkronisasi ke tanda tangan metode sama dengan membuat blok sinkronisasi(ini){}. Dalam contoh di atas, kami secara tidak sengaja menambahkan kata kunci sinkronisasi ke metode, dan kemudian menyinkronkan antrian dengan monitor objek antrian untuk mengirim thread ini ke mode tidur sambil menunggu nilai berikutnya dari antrian. Kemudian, thread saat ini melepaskan kunci antrian, tetapi tidak melepaskan kunci ini. Metode putInt() memberitahukan thread tidur bahwa nilai baru telah ditambahkan. Namun secara kebetulan kami juga menambahkan kata kunci tersinkronisasi ke metode ini. Sekarang setelah thread kedua tertidur, thread tersebut masih memegang kuncinya. Oleh karena itu, thread pertama tidak dapat masuk ke metode putInt() sementara kunci dipegang oleh thread kedua. Akibatnya, kita mengalami situasi kebuntuan dan program terhenti. Jika Anda menjalankan kode di atas, ini akan terjadi segera setelah program mulai berjalan. Dalam kehidupan sehari-hari, situasi ini mungkin tidak begitu kentara. Kunci yang dipegang oleh thread mungkin bergantung pada parameter dan kondisi yang ditemui saat runtime, dan blok tersinkronisasi yang menyebabkan masalah mungkin tidak sedekat kode tempat kita melakukan panggilan wait(). Hal ini menyulitkan untuk menemukan masalah seperti itu, terutama karena masalah tersebut dapat terjadi seiring waktu atau di bawah beban yang tinggi.
2.2 Kondisi di blok tersinkronisasi
Seringkali Anda perlu memeriksa bahwa beberapa kondisi terpenuhi sebelum melakukan tindakan apa pun pada objek yang disinkronkan. Misalnya, ketika Anda memiliki antrian, Anda ingin menunggu hingga terisi. Oleh karena itu, Anda dapat menulis metode yang memeriksa apakah antrian sudah penuh. Jika masih kosong, maka Anda mengirim thread saat ini ke mode tidur hingga terbangun: public Integer getNextInt() { Integer retVal = null; synchronized (queue) { try { while (queue.isEmpty()) { queue.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } synchronized (queue) { retVal = queue.poll(); if (retVal == null) { System.err.println("retVal is null"); throw new IllegalStateException(); } } return retVal; } Kode di atas disinkronkan dengan antrian sebelum memanggil wait() dan kemudian menunggu dalam beberapa saat hingga setidaknya satu elemen muncul dalam antrian. Blok kedua yang disinkronkan kembali menggunakan antrian sebagai monitor objek. Ia memanggil metode poll() antrian untuk mendapatkan nilainya. Untuk tujuan demonstrasi, IllegalStateException dilemparkan ketika jajak pendapat menghasilkan nol. Hal ini terjadi ketika antrian tidak memiliki elemen untuk diambil. Saat Anda menjalankan contoh ini, Anda akan melihat bahwa IllegalStateException sangat sering dilempar. Meskipun kami menyinkronkan dengan benar menggunakan monitor antrian, pengecualian muncul. Alasannya adalah kami memiliki dua blok tersinkronisasi yang berbeda. Bayangkan kita memiliki dua thread yang telah sampai pada blok tersinkronisasi pertama. Thread pertama memasuki blok dan tertidur karena antriannya kosong. Hal serupa juga terjadi pada thread kedua. Sekarang kedua thread sudah aktif (berkat panggilan notifyAll() yang dipanggil oleh thread lain untuk monitor), keduanya melihat nilai(item) dalam antrian yang ditambahkan oleh produsen. Kemudian keduanya sampai di penghalang kedua. Di sini thread pertama masuk dan mengambil nilai dari antrian. Saat thread kedua masuk, antrian sudah kosong. Oleh karena itu, ia menerima null sebagai nilai yang dikembalikan dari antrian dan memberikan pengecualian. Untuk mencegah situasi seperti itu, Anda perlu melakukan semua operasi yang bergantung pada status monitor di blok tersinkronisasi yang sama: public Integer getNextInt() { Integer retVal = null; synchronized (queue) { try { while (queue.isEmpty()) { queue.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } retVal = queue.poll(); } return retVal; } Di sini kita mengeksekusi metode poll() di blok tersinkronisasi yang sama dengan metode isEmpty(). Berkat blok tersinkronisasi, kami yakin hanya satu thread yang menjalankan metode untuk monitor ini pada waktu tertentu. Oleh karena itu, tidak ada thread lain yang dapat menghapus elemen dari antrian antara panggilan ke isEmpty() dan poll(). Terjemahan lanjutan di sini .
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION