JavaRush /Blog Java /Random-MS /Asas Konkurensi: Kebuntuan dan Pemantau Objek (bahagian 1...
Snusmum
Tahap
Хабаровск

Asas Konkurensi: Kebuntuan dan Pemantau Objek (bahagian 1, 2) (terjemahan artikel)

Diterbitkan dalam kumpulan
Artikel sumber: http://www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html Dihantar oleh Martin Mois Artikel ini adalah sebahagian daripada kursus Java Concurrency Fundamentals kami . Dalam kursus ini, anda akan mendalami keajaiban paralelisme. Anda akan mempelajari asas-asas selari dan kod selari, dan membiasakan diri dengan konsep seperti atomicity, penyegerakan dan keselamatan benang. Sila lihat di sini !

Kandungan

1. Liveness  1.1 Kebuntuan  1.2 Kebuluran 2. Pemantau objek dengan tunggu() dan notify()  2.1 Blok disegerakkan bersarang dengan tunggu() dan notify()  2.2 Keadaan dalam blok disegerakkan 3. Reka bentuk untuk berbilang benang  3.1 Objek tidak berubah  3.2 Reka bentuk API  3.3 Storan benang tempatan
1. Daya hidup
Apabila membangunkan aplikasi yang menggunakan selari untuk mencapai matlamatnya, anda mungkin menghadapi situasi di mana rangkaian berbeza boleh menyekat satu sama lain. Jika aplikasi berjalan lebih perlahan daripada yang dijangkakan dalam situasi ini, kami akan mengatakan bahawa ia tidak berjalan seperti yang diharapkan. Dalam bahagian ini, kita akan melihat dengan lebih dekat isu yang boleh mengancam kemandirian aplikasi berbilang benang.
1.1 Saling menyekat
Istilah kebuntuan terkenal di kalangan pembangun perisian malah kebanyakan pengguna biasa menggunakannya dari semasa ke semasa, walaupun tidak selalu dalam erti kata yang betul. Tegasnya, istilah ini bermaksud bahawa setiap satu daripada dua (atau lebih) utas sedang menunggu untuk utas lain melepaskan sumber yang dikunci olehnya, manakala utas pertama sendiri telah mengunci sumber yang kedua sedang menunggu untuk mengakses: Untuk lebih memahami masalahnya, lihat Thread 1: locks resource A, waits for resource B Thread 2: locks resource B, waits for resource A kod 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 daripada kod di atas, dua utas bermula dan cuba mengunci dua sumber statik. Tetapi untuk kebuntuan, kami memerlukan urutan yang berbeza untuk kedua-dua utas, jadi kami menggunakan contoh objek Rawak untuk memilih sumber mana benang itu ingin dikunci dahulu. Jika pembolehubah boolean b adalah benar, maka resource1 dikunci dahulu, dan kemudian benang cuba memperoleh kunci untuk resource2. Jika b adalah palsu, maka benang mengunci sumber2 dan kemudian cuba memperoleh sumber1. Program ini tidak perlu berjalan lama untuk mencapai kebuntuan pertama, i.e. Program ini akan digantung selama-lamanya jika kita tidak mengganggunya: [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 larian ini, tread-1 telah memperoleh kunci resource2 dan sedang menunggu kunci resource1, manakala tread-2 mempunyai kunci resource1 dan sedang menunggu resource2. Jika kita menetapkan nilai pembolehubah boolean b dalam kod di atas kepada benar, kita tidak akan dapat melihat sebarang kebuntuan kerana urutan kunci permintaan thread-1 dan thread-2 akan sentiasa sama. Dalam situasi ini, salah satu daripada dua utas akan mendapatkan kunci terlebih dahulu dan kemudian meminta yang kedua, yang masih tersedia kerana utas lain sedang menunggu kunci pertama. Secara umum, kita boleh membezakan syarat-syarat berikut yang diperlukan untuk kebuntuan berlaku: - Pelaksanaan bersama: Terdapat sumber yang boleh diakses oleh hanya satu utas pada bila-bila masa. - Pegangan Sumber: Semasa memperoleh satu sumber, benang cuba memperoleh kunci lain pada beberapa sumber unik. - Tiada preemption: Tiada mekanisme untuk melepaskan sumber jika satu benang memegang kunci untuk tempoh masa tertentu. - Menunggu Pekeliling: Semasa pelaksanaan, koleksi utas berlaku di mana dua (atau lebih) utas menunggu antara satu sama lain untuk melepaskan sumber yang telah dikunci. Walaupun senarai syarat kelihatan panjang, ia adalah perkara biasa bagi aplikasi berbilang benang yang dikendalikan dengan baik mengalami masalah kebuntuan. Tetapi anda boleh menghalangnya jika anda boleh mengalih keluar salah satu syarat di atas: - Pelaksanaan dikongsi: syarat ini selalunya tidak boleh dialih keluar apabila sumber mesti digunakan oleh hanya seorang. Tetapi ini tidak semestinya menjadi sebab. Apabila menggunakan sistem DBMS, penyelesaian yang mungkin, bukannya menggunakan kunci pesimis pada beberapa baris jadual yang perlu dikemas kini, adalah menggunakan teknik yang dipanggil Penguncian Optimis . - Satu cara untuk mengelak daripada memegang sumber semasa menunggu sumber eksklusif yang lain adalah dengan mengunci semua sumber yang diperlukan pada permulaan algoritma dan melepaskan semuanya jika mustahil untuk mengunci semuanya sekaligus. Sudah tentu, ini tidak selalu mungkin; mungkin sumber yang memerlukan penguncian tidak diketahui terlebih dahulu, atau pendekatan ini hanya akan membawa kepada pembaziran sumber. - Jika kunci tidak dapat diperoleh dengan segera, cara untuk memintas kemungkinan kebuntuan adalah dengan memperkenalkan tamat masa. Contohnya, kelas ReentrantLockdaripada SDK menyediakan keupayaan untuk menetapkan tarikh tamat tempoh untuk kunci. - Seperti yang kita lihat daripada contoh di atas, kebuntuan tidak berlaku jika urutan permintaan tidak berbeza antara benang yang berbeza. Ini mudah dikawal jika anda boleh meletakkan semua kod sekatan ke dalam satu kaedah yang perlu dilalui oleh semua utas. Dalam aplikasi yang lebih maju, anda mungkin mempertimbangkan untuk melaksanakan sistem pengesanan jalan buntu. Di sini anda perlu melaksanakan beberapa kesamaan pemantauan benang, di mana setiap utas melaporkan bahawa ia telah berjaya memperoleh kunci dan cuba memperoleh kunci. Jika benang dan kunci dimodelkan sebagai graf terarah, anda boleh mengesan apabila dua utas berbeza memegang sumber semasa cuba mengakses sumber terkunci lain pada masa yang sama. Jika anda kemudian boleh memaksa benang penyekat untuk melepaskan sumber yang diperlukan, anda boleh menyelesaikan situasi kebuntuan secara automatik.
1.2 Berpuasa
Penjadual memutuskan benang dalam keadaan RUNNABLE yang harus dilaksanakan seterusnya. Keputusan adalah berdasarkan keutamaan benang; oleh itu, benang dengan keutamaan yang lebih rendah menerima lebih sedikit masa CPU berbanding dengan benang yang mempunyai keutamaan yang lebih tinggi. Apa yang kelihatan seperti penyelesaian yang munasabah juga boleh menyebabkan masalah jika disalahgunakan. Jika utas keutamaan tinggi dilaksanakan pada kebanyakan masa, maka utas keutamaan rendah nampaknya kebuluran kerana mereka tidak mendapat masa yang cukup untuk melakukan kerja mereka dengan betul. Oleh itu, adalah disyorkan untuk menetapkan keutamaan utas hanya apabila terdapat alasan yang kukuh untuk berbuat demikian. Contoh kebuluran benang yang tidak jelas diberikan, contohnya, dengan kaedah finalize(). Ia menyediakan cara untuk bahasa Java melaksanakan kod sebelum objek dikumpul sampah. Tetapi jika anda melihat keutamaan utas pemuktamadkan, anda akan dapati bahawa ia tidak berjalan dengan keutamaan tertinggi. Akibatnya, kebuluran benang berlaku apabila kaedah finalize() objek anda menghabiskan terlalu banyak masa berbanding kod yang lain. Satu lagi masalah dengan masa pelaksanaan timbul daripada fakta bahawa ia tidak ditakrifkan dalam susunan apa benang melintasi blok yang disegerakkan. Apabila banyak utas selari merentasi beberapa kod yang dibingkai dalam blok yang disegerakkan, mungkin berlaku beberapa utas perlu menunggu lebih lama daripada yang lain sebelum memasuki blok. Secara teori, mereka mungkin tidak akan sampai ke sana. Penyelesaian kepada masalah ini adalah apa yang dipanggil "adil" menyekat. Kunci adil mengambil kira masa menunggu benang apabila menentukan siapa yang akan lulus seterusnya. Contoh pelaksanaan penguncian saksama tersedia dalam Java SDK: java.util.concurrent.locks.ReentrantLock. Jika pembina digunakan dengan bendera boolean ditetapkan kepada benar, maka ReentrantLock memberikan akses kepada utas yang telah menunggu paling lama. Ini menjamin ketiadaan kelaparan tetapi, pada masa yang sama, membawa kepada masalah mengabaikan keutamaan. Oleh sebab itu, proses keutamaan yang lebih rendah yang sering menunggu di halangan ini mungkin berjalan dengan lebih kerap. Akhir sekali, kelas ReentrantLock hanya boleh mempertimbangkan utas yang sedang menunggu kunci, i.e. benang yang cukup kerap dilancarkan dan mencapai halangan. Jika keutamaan benang adalah terlalu rendah, maka ini tidak akan kerap berlaku untuknya, dan oleh itu benang keutamaan tinggi masih akan melepasi kunci dengan lebih kerap.
2. Pemantauan objek bersama-sama dengan wait() dan notify()
Dalam pengkomputeran berbilang benang, situasi yang biasa adalah untuk mempunyai beberapa utas pekerja menunggu pengeluar mereka untuk mencipta beberapa kerja untuk mereka. Tetapi, seperti yang kita pelajari, menunggu secara aktif dalam gelung semasa menyemak nilai tertentu bukanlah pilihan yang baik dari segi masa CPU. Menggunakan kaedah Thread.sleep() dalam situasi ini juga tidak sesuai jika kita ingin memulakan kerja kita sejurus selepas ketibaan. Untuk tujuan ini, bahasa pengaturcaraan Java mempunyai struktur lain yang boleh digunakan dalam skema ini: wait() dan notify(). Kaedah wait(), yang diwarisi oleh semua objek daripada kelas java.lang.Object, boleh digunakan untuk menggantung thread semasa dan tunggu sehingga thread lain membangunkan kami menggunakan kaedah notify(). Untuk berfungsi dengan betul, utas yang memanggil kaedah tunggu() mesti memegang kunci yang diperolehnya sebelum ini menggunakan kata kunci yang disegerakkan. Apabila wait() dipanggil, kunci dilepaskan dan benang menunggu sehingga thread lain yang kini memegang kunci panggilan notify() pada contoh objek yang sama. Dalam aplikasi berbilang benang, mungkin terdapat lebih daripada satu utas yang menunggu pemberitahuan pada beberapa objek. Oleh itu, terdapat dua kaedah yang berbeza untuk membangkitkan benang: notify() dan notifyAll(). Semasa kaedah pertama membangunkan salah satu utas menunggu, kaedah notifyAll() membangunkan kesemuanya. Tetapi sedar bahawa, seperti kata kunci yang disegerakkan, tiada peraturan yang menentukan utas mana yang akan dibangkitkan seterusnya apabila notify() dipanggil. Dalam contoh mudah dengan pengeluar dan pengguna, ini tidak penting, kerana kami tidak peduli benang mana yang dibangkitkan. Kod berikut menunjukkan cara wait() dan notify() boleh digunakan untuk menyebabkan utas pengguna menunggu untuk kerja baharu dibariskan oleh benang pengeluar: 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(); } } Kaedah main() memulakan lima utas pengguna dan satu utas pengeluar dan kemudian menunggu mereka selesai. Benang pengeluar kemudian menambah nilai baharu pada baris gilir dan memberitahu semua utas menunggu bahawa sesuatu telah berlaku. Pengguna mendapat kunci baris gilir (iaitu, seorang pengguna rawak) dan kemudian tidur, untuk dinaikkan kemudian apabila baris gilir penuh semula. Apabila pengeluar menyelesaikan kerjanya, ia memberitahu semua pengguna untuk membangunkan mereka. Jika kami tidak melakukan langkah terakhir, urutan pengguna akan menunggu selama-lamanya untuk pemberitahuan seterusnya kerana kami tidak menetapkan tamat masa untuk menunggu. Sebaliknya, kita boleh menggunakan kaedah tunggu (masa tamat) untuk dibangunkan sekurang-kurangnya selepas beberapa waktu berlalu.
2.1 Blok disegerakkan bersarang dengan wait() dan notify()
Seperti yang dinyatakan dalam bahagian sebelumnya, memanggil wait() pada monitor objek hanya melepaskan kunci pada monitor tersebut. Kunci lain yang dipegang oleh benang yang sama tidak dilepaskan. Seperti yang mudah difahami, dalam kerja seharian mungkin berlaku bahawa benang yang memanggil wait() memegang kunci lebih jauh. Jika benang lain juga sedang menunggu kunci ini, keadaan kebuntuan mungkin berlaku. Mari kita lihat penguncian dalam 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 sebelum ini , menambah disegerakkan pada tandatangan kaedah adalah bersamaan dengan mencipta blok disegerakkan(ini){}. Dalam contoh di atas, kami secara tidak sengaja menambahkan kata kunci yang disegerakkan pada kaedah, dan kemudian menyegerakkan baris gilir dengan monitor objek baris gilir untuk menghantar urutan ini tidur sementara ia menunggu nilai seterusnya daripada baris gilir. Kemudian, benang semasa melepaskan kunci pada baris gilir, tetapi bukan kunci pada ini. Kaedah putInt() memberitahu benang tidur bahawa nilai baharu telah ditambahkan. Tetapi secara kebetulan kami menambah kata kunci yang disegerakkan pada kaedah ini juga. Sekarang benang kedua telah tertidur, ia masih memegang kunci. Oleh itu, benang pertama tidak boleh memasuki kaedah putInt() semasa kunci dipegang oleh benang kedua. Akibatnya, kami mengalami keadaan buntu dan program beku. Jika anda menjalankan kod di atas, ia akan berlaku serta-merta selepas program mula berjalan. Dalam kehidupan seharian, keadaan ini mungkin tidak begitu ketara. Kunci yang dipegang oleh benang mungkin bergantung pada parameter dan keadaan yang dihadapi semasa masa jalan, dan blok disegerakkan yang menyebabkan masalah mungkin tidak begitu dekat dalam kod dengan tempat kami meletakkan panggilan tunggu(). Ini menyukarkan untuk mencari masalah sedemikian, terutamanya kerana ia mungkin berlaku dari semasa ke semasa atau di bawah beban yang tinggi.
2.2 Keadaan dalam blok yang disegerakkan
Selalunya anda perlu menyemak bahawa beberapa syarat dipenuhi sebelum melakukan sebarang tindakan pada objek yang disegerakkan. Apabila anda mempunyai baris gilir, sebagai contoh, anda ingin menunggu sehingga ia penuh. Oleh itu, anda boleh menulis kaedah yang menyemak sama ada baris gilir penuh. Jika ia masih kosong, maka anda menghantar utas semasa untuk tidur sehingga ia dikejutkan: 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; } Kod di atas menyegerakkan dengan baris gilir sebelum memanggil wait() dan kemudian menunggu dalam gelung sementara sehingga sekurang-kurangnya satu elemen muncul dalam baris gilir. Blok disegerakkan kedua sekali lagi menggunakan baris gilir sebagai pemantau objek. Ia memanggil kaedah poll() baris gilir untuk mendapatkan nilai. Untuk tujuan demonstrasi, IllegalStateException dilemparkan apabila tinjauan pendapat mengembalikan batal. Ini berlaku apabila baris gilir tiada unsur untuk diambil. Apabila anda menjalankan contoh ini, anda akan melihat bahawa IllegalStateException dilemparkan dengan kerap. Walaupun kami menyegerakkan dengan betul menggunakan monitor baris gilir, pengecualian telah dilemparkan. Sebabnya ialah kita mempunyai dua blok disegerakkan yang berbeza. Bayangkan kita mempunyai dua utas yang telah tiba di blok pertama yang disegerakkan. Benang pertama memasuki blok dan tidur kerana barisan kosong. Perkara yang sama berlaku untuk benang kedua. Memandangkan kedua-dua utas terjaga (terima kasih kepada panggilan notifyAll() yang dipanggil oleh utas lain untuk monitor), mereka berdua melihat nilai(item) dalam baris gilir yang ditambahkan oleh pengeluar. Kemudian kedua-duanya tiba di penghalang kedua. Di sini benang pertama memasuki dan mendapatkan nilai dari baris gilir. Apabila benang kedua masuk, baris gilir sudah kosong. Oleh itu, ia menerima null sebagai nilai yang dikembalikan daripada baris gilir dan membuang pengecualian. Untuk mengelakkan situasi sedemikian, anda perlu melakukan semua operasi yang bergantung pada keadaan monitor dalam blok disegerakkan 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 kami melaksanakan kaedah poll() dalam blok disegerakkan yang sama seperti kaedah isEmpty(). Terima kasih kepada blok yang disegerakkan, kami pasti hanya satu utas yang melaksanakan kaedah untuk monitor ini pada masa tertentu. Oleh itu, tiada benang lain boleh mengalih keluar elemen daripada baris gilir antara panggilan ke isEmpty() dan poll(). Sambungan terjemahan di sini .
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION