JavaRush /Java Blog /Random-TL /Fundamentals of Concurrency: Deadlocks and Object Monitor...
Snusmum
Antas
Хабаровск

Fundamentals of Concurrency: Deadlocks and Object Monitors (mga seksyon 1, 2) (pagsasalin ng artikulo)

Nai-publish sa grupo
Pinagmulan na artikulo: http://www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html Na-post ni Martin Mois Ang artikulong ito ay bahagi ng aming kurso sa Java Concurrency Fundamentals . Sa kursong ito, malalaman mo ang mahika ng paralelismo. Matututuhan mo ang mga pangunahing kaalaman ng parallelism at parallel code, at magiging pamilyar sa mga konsepto tulad ng atomicity, synchronization, at kaligtasan ng thread. Tingnan mo dito !

Nilalaman

1. Liveness  1.1 Deadlock  1.2 Starvation 2. Object monitors with wait() and notify()  2.1 Nested synchronized blocks with wait() and notify()  2.2 Conditions in synchronized blocks 3. Design for multi-threading  3.1 Immutable object  3.2 API design  3.3 Lokal na imbakan ng thread
1. Kasiglahan
Kapag bumubuo ng mga application na gumagamit ng parallelism upang makamit ang kanilang mga layunin, maaari kang makatagpo ng mga sitwasyon kung saan maaaring harangan ng iba't ibang thread ang isa't isa. Kung ang application ay tumatakbo nang mas mabagal kaysa sa inaasahan sa sitwasyong ito, sasabihin namin na hindi ito tumatakbo gaya ng inaasahan. Sa seksyong ito, susuriin natin nang mas malapitan ang mga isyu na maaaring magbanta sa kaligtasan ng buhay ng isang multi-threaded na application.
1.1 Mutual blocking
Ang terminong deadlock ay kilala sa mga developer ng software at kahit na karamihan sa mga ordinaryong user ay gumagamit nito paminsan-minsan, kahit na hindi palaging nasa tamang kahulugan. Sa mahigpit na pagsasalita, ang terminong ito ay nangangahulugan na ang bawat isa sa dalawa (o higit pa) na mga thread ay naghihintay para sa kabilang thread na maglabas ng isang mapagkukunan na naka-lock nito, habang ang unang thread mismo ay nag-lock ng isang mapagkukunan na ang pangalawa ay naghihintay na ma-access: Upang mas maunawaan ang problema, tingnan ang Thread 1: locks resource A, waits for resource B Thread 2: locks resource B, waits for resource A sumusunod na code: 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."); } } } } } } Tulad ng nakikita mo mula sa code sa itaas, magsisimula ang dalawang thread at subukang i-lock ang dalawang static na mapagkukunan. Ngunit para sa deadlocking, kailangan namin ng ibang sequence para sa parehong mga thread, kaya gumagamit kami ng isang instance ng Random object upang piliin kung aling mapagkukunan ang gustong i-lock muna ng thread. Kung totoo ang boolean variable b, i-lock muna ang resource1, at pagkatapos ay susubukan ng thread na kunin ang lock para sa resource2. Kung ang b ay mali, ang thread ay nagla-lock ng resource2 at pagkatapos ay sinusubukang kumuha ng resource1. Ang program na ito ay hindi kailangang tumakbo nang matagal upang makamit ang unang deadlock, i.e. Ang programa ay mananatili magpakailanman kung hindi namin ito aabala: [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. Sa pagtakbo na ito, nakuha ng tread-1 ang resource2 lock at naghihintay para sa resource1's lock, habang ang tread-2 ay may resource1 lock at naghihintay ng resource2. Kung itatakda namin ang halaga ng boolean variable b sa code sa itaas sa true, hindi namin mapapansin ang anumang deadlock dahil ang pagkakasunud-sunod kung saan ang thread-1 at thread-2 na mga lock ng kahilingan ay palaging pareho. Sa sitwasyong ito, isa sa dalawang thread ang unang kukuha ng lock at pagkatapos ay hihilingin ang pangalawa, na available pa rin dahil naghihintay ang kabilang thread para sa unang lock. Sa pangkalahatan, maaari nating makilala ang mga sumusunod na kinakailangang kundisyon para magkaroon ng deadlock: - Nakabahaging pagpapatupad: Mayroong mapagkukunan na maaaring ma-access ng isang thread lamang anumang oras. - Resource Hold: Habang kumukuha ng isang resource, sinusubukan ng thread na kumuha ng isa pang lock sa ilang natatanging resource. - Walang preemption: Walang mekanismong maglalabas ng resource kung hawak ng isang thread ang lock para sa isang tiyak na tagal ng panahon. - Circular Wait: Sa panahon ng execution, nangyayari ang isang koleksyon ng mga thread kung saan naghihintay ang dalawa (o higit pang) thread para sa isa't isa na maglabas ng resource na naka-lock. Bagama't mukhang mahaba ang listahan ng mga kundisyon, karaniwan na para sa mahusay na pagpapatakbo ng mga multi-threaded na application na magkaroon ng mga problema sa deadlock. Ngunit mapipigilan mo ang mga ito kung maaari mong alisin ang isa sa mga kundisyon sa itaas: - Nakabahaging pagpapatupad: madalas na hindi maalis ang kundisyong ito kapag ang mapagkukunan ay dapat gamitin ng isang tao lamang. Ngunit hindi ito kailangang maging dahilan. Kapag gumagamit ng mga DBMS system, ang isang posibleng solusyon, sa halip na gumamit ng pessimistic lock sa ilang table row na kailangang i-update, ay gumamit ng technique na tinatawag na Optimistic Locking . - Ang isang paraan upang maiwasan ang paghawak ng isang mapagkukunan habang naghihintay ng isa pang eksklusibong mapagkukunan ay upang i-lock ang lahat ng kinakailangang mapagkukunan sa simula ng algorithm at ilabas ang lahat ng ito kung imposibleng i-lock ang mga ito nang sabay-sabay. Siyempre, hindi ito laging posible; marahil ang mga mapagkukunan na nangangailangan ng pag-lock ay hindi alam nang maaga, o ang diskarte na ito ay hahantong lamang sa isang pag-aaksaya ng mga mapagkukunan. - Kung hindi agad makuha ang lock, ang isang paraan para ma-bypass ang posibleng deadlock ay ang pagpasok ng timeout. Halimbawa, ang ReentrantLock classmula sa SDK ay nagbibigay ng kakayahang magtakda ng petsa ng pag-expire para sa lock. - Tulad ng nakita natin mula sa halimbawa sa itaas, hindi nangyayari ang deadlock kung ang pagkakasunud-sunod ng mga kahilingan ay hindi naiiba sa iba't ibang mga thread. Madali itong kontrolin kung maaari mong ilagay ang lahat ng blocking code sa isang paraan na kailangang pagdaanan ng lahat ng thread. Sa mas advanced na mga application, maaari mo ring isaalang-alang ang pagpapatupad ng deadlock detection system. Dito kakailanganin mong ipatupad ang ilang pagkakatulad ng pagsubaybay sa thread, kung saan ang bawat thread ay nag-uulat na matagumpay nitong nakuha ang lock at sinusubukang makuha ang lock. Kung ang mga thread at mga kandado ay naka-modelo bilang isang nakadirekta na graph, maaari mong makita kapag ang dalawang magkaibang mga thread ay may hawak na mga mapagkukunan habang sinusubukang i-access ang iba pang mga naka-lock na mapagkukunan sa parehong oras. Kung maaari mong pilitin ang mga nakaharang na thread na ilabas ang mga kinakailangang mapagkukunan, maaari mong awtomatikong lutasin ang deadlock na sitwasyon.
1.2 Pag-aayuno
Ang scheduler ang magpapasya kung aling thread sa RUNNABLE na estado ang dapat nitong isagawa sa susunod. Ang desisyon ay batay sa priyoridad ng thread; samakatuwid, ang mga thread na may mas mababang priyoridad ay tumatanggap ng mas kaunting oras ng CPU kumpara sa mga may mas mataas na priyoridad. Ang mukhang isang makatwirang solusyon ay maaari ding magdulot ng mga problema kung inabuso. Kung ang mga thread na may mataas na priyoridad ay madalas na gumagana, ang mga thread na may mababang priyoridad ay tila nagugutom dahil hindi sila nakakakuha ng sapat na oras upang gawin ang kanilang trabaho nang maayos. Samakatuwid, inirerekumenda na magtakda lamang ng priyoridad ng thread kapag may mapanghikayat na dahilan para gawin ito. Ang isang hindi halatang halimbawa ng thread starvation ay ibinibigay, halimbawa, sa pamamagitan ng finalize() method. Nagbibigay ito ng paraan para sa wikang Java na magsagawa ng code bago makolekta ang isang bagay. Ngunit kung titingnan mo ang priyoridad ng pagtatapos ng thread, mapapansin mong hindi ito tumatakbo nang may pinakamataas na priyoridad. Dahil dito, nangyayari ang gutom sa thread kapag ang mga pamamaraan ng finalize() ng iyong object ay gumugugol ng masyadong maraming oras kumpara sa natitirang bahagi ng code. Ang isa pang problema sa oras ng pagpapatupad ay nagmumula sa katotohanan na hindi ito tinukoy sa kung anong pagkakasunud-sunod ng mga thread na dumadaan sa naka-synchronize na bloke. Kapag maraming magkakatulad na mga thread ang dumadaan sa ilang code na naka-frame sa isang naka-synchronize na bloke, maaaring mangyari na ang ilang mga thread ay kailangang maghintay nang mas matagal kaysa sa iba bago pumasok sa block. Sa teorya, maaaring hindi sila makarating doon. Ang solusyon sa problemang ito ay ang tinatawag na “fair” blocking. Isinasaalang-alang ng mga patas na lock ang mga oras ng paghihintay ng thread kapag tinutukoy kung sino ang susunod na papasa. Available ang isang halimbawang pagpapatupad ng patas na pag-lock sa Java SDK: java.util.concurrent.locks.ReentrantLock. Kung ang isang constructor ay ginagamit na may boolean flag na nakatakda sa true, ang ReentrantLock ay nagbibigay ng access sa thread na pinakamatagal nang naghihintay. Ginagarantiyahan nito ang kawalan ng gutom ngunit, sa parehong oras, humahantong sa problema ng pagwawalang-bahala sa mga priyoridad. Dahil dito, ang mas mababang priyoridad na proseso na madalas na naghihintay sa hadlang na ito ay maaaring tumakbo nang mas madalas. Panghuli ngunit hindi bababa sa, ang ReentrantLock class ay maaari lamang isaalang-alang ang mga thread na naghihintay ng lock, i.e. mga thread na madalas na inilunsad at umabot sa hadlang. Kung masyadong mababa ang priyoridad ng isang thread, hindi ito madalas mangyari para dito, at samakatuwid ang mga thread na may mataas na priyoridad ay dadaan pa rin sa lock nang mas madalas.
2. Sinusubaybayan ng object kasama ang wait() at notify()
Sa multi-threaded computing, ang isang karaniwang sitwasyon ay ang pagkakaroon ng ilang mga thread ng manggagawa na naghihintay para sa kanilang producer na gumawa ng ilang trabaho para sa kanila. Ngunit, tulad ng natutunan namin, ang aktibong paghihintay sa isang loop habang sinusuri ang isang tiyak na halaga ay hindi isang magandang opsyon sa mga tuntunin ng oras ng CPU. Ang paggamit ng Thread.sleep() na pamamaraan sa sitwasyong ito ay hindi rin partikular na angkop kung gusto nating simulan kaagad ang ating trabaho pagkatapos ng pagdating. Para sa layuning ito, ang Java programming language ay may isa pang istraktura na maaaring gamitin sa scheme na ito: wait() at notify(). Ang wait() method, na minana ng lahat ng object mula sa java.lang.Object class, ay maaaring gamitin para suspindihin ang kasalukuyang thread at maghintay hanggang sa magising tayo ng isa pang thread gamit ang notify() method. Upang gumana nang tama, ang thread na tumatawag sa wait() na paraan ay dapat magkaroon ng lock na dati nitong nakuha gamit ang naka-synchronize na keyword. Kapag ang wait() ay tinawag, ang lock ay ilalabas at ang thread ay maghihintay hanggang sa isa pang thread na ngayon ay may hawak ng lock calls notify() sa parehong object instance. Sa isang multi-threaded na application, maaaring natural na mayroong higit sa isang thread na naghihintay ng abiso sa ilang bagay. Samakatuwid, mayroong dalawang magkaibang paraan para sa paggising ng mga thread: notify() at notifyAll(). Habang ginigising ng unang paraan ang isa sa mga naghihintay na thread, ang paraan ng notifyAll() ay gumising sa lahat ng ito. Ngunit magkaroon ng kamalayan na, tulad ng naka-synchronize na keyword, walang panuntunan na tumutukoy kung aling thread ang susunod na gisingin kapag tinawag ang notify(). Sa isang simpleng halimbawa sa isang producer at isang consumer, hindi ito mahalaga, dahil wala kaming pakialam kung aling thread ang nagising. Ipinapakita ng sumusunod na code kung paano magagamit ang wait() at notify() para maghintay ang mga consumer thread para sa bagong trabaho na ma-queue ng thread ng producer: 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(); } } Ang pangunahing() na pamamaraan ay magsisimula ng limang consumer thread at isang producer thread at pagkatapos ay maghihintay na matapos ang mga ito. Pagkatapos ay idinaragdag ng thread ng producer ang bagong halaga sa queue at inaabisuhan ang lahat ng naghihintay na thread na may nangyari. Ang mga mamimili ay nakakakuha ng lock ng pila (ibig sabihin, isang random na mamimili) at pagkatapos ay matutulog, na itataas sa ibang pagkakataon kapag puno na muli ang pila. Kapag natapos na ng producer ang trabaho nito, inaabisuhan nito ang lahat ng consumer na gisingin sila. Kung hindi namin gagawin ang huling hakbang, maghihintay nang tuluyan ang mga consumer thread para sa susunod na notification dahil hindi kami nagtakda ng timeout para maghintay. Sa halip, maaari naming gamitin ang wait(long timeout) na paraan upang magising kahit na lumipas ang ilang oras.
2.1 Naka-nest na naka-synchronize na mga bloke na may wait() at notify()
Gaya ng nakasaad sa nakaraang seksyon, ang pagtawag sa wait() sa monitor ng isang bagay ay naglalabas lamang ng lock sa monitor na iyon. Ang iba pang mga lock na hawak ng parehong thread ay hindi inilabas. Tulad ng madaling maunawaan, sa pang-araw-araw na gawain ay maaaring mangyari na ang thread na tumatawag sa wait() ay humawak pa sa lock. Kung naghihintay din ang ibang mga thread para sa mga lock na ito, maaaring magkaroon ng deadlock na sitwasyon. Tingnan natin ang pag-lock sa sumusunod na halimbawa: 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(); } } Gaya ng natutunan natin kanina , ang pagdaragdag ng naka-synchronize sa isang signature ng pamamaraan ay katumbas ng paggawa ng naka-synchronize(ito){} block. Sa halimbawa sa itaas, hindi sinasadyang naidagdag namin ang naka-synchronize na keyword sa pamamaraan, at pagkatapos ay na-synchronize ang queue sa monitor ng queue object upang mai-sleep ang thread na ito habang naghihintay ito ng susunod na value mula sa queue. Pagkatapos, inilalabas ng kasalukuyang thread ang lock sa queue, ngunit hindi ang lock dito. Ang putInt() method ay nag-aabiso sa sleeping thread na may naidagdag na bagong value. Ngunit kung nagkataon ay idinagdag din namin ang naka-synchronize na keyword sa paraang ito. Ngayong nakatulog na ang pangalawang thread, hawak pa rin nito ang lock. Samakatuwid, ang unang thread ay hindi maaaring pumasok sa putInt() na paraan habang ang lock ay hawak ng pangalawang thread. Bilang resulta, mayroon kaming isang deadlock na sitwasyon at isang nakapirming programa. Kung patakbuhin mo ang code sa itaas, mangyayari ito kaagad pagkatapos magsimulang tumakbo ang program. Sa pang-araw-araw na buhay, ang sitwasyong ito ay maaaring hindi masyadong halata. Ang mga lock na hawak ng isang thread ay maaaring depende sa mga parameter at kundisyon na nakatagpo sa runtime, at ang naka-synchronize na block na nagdudulot ng problema ay maaaring hindi kasing lapit sa code kung saan namin inilagay ang wait() na tawag. Ginagawa nitong mahirap na makahanap ng mga ganitong problema, lalo na dahil maaaring mangyari ang mga ito sa paglipas ng panahon o sa ilalim ng mataas na pagkarga.
2.2 Mga kondisyon sa naka-synchronize na mga bloke
Kadalasan kailangan mong suriin kung natutugunan ang ilang kundisyon bago magsagawa ng anumang pagkilos sa isang naka-synchronize na bagay. Kapag mayroon kang pila, halimbawa, gusto mong hintayin itong mapuno. Samakatuwid, maaari kang magsulat ng isang paraan na nagsusuri kung puno na ang pila. Kung ito ay walang laman, pagkatapos ay ipadala mo ang kasalukuyang thread sa pagtulog hanggang sa ito ay magising: 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; } Ang code sa itaas ay nagsi-synchronize sa queue bago tumawag ng wait() at pagkatapos ay maghintay nang ilang sandali hanggang sa lumitaw ang kahit isang elemento sa queue. Ang pangalawang naka-synchronize na block ay muling gumagamit ng queue bilang isang object monitor. Tinatawag nito ang pamamaraan ng poll() ng pila upang makuha ang halaga. Para sa mga layunin ng pagpapakita, ang isang IllegalStateException ay itatapon kapag ang poll ay bumalik na null. Nangyayari ito kapag walang mga elementong kukunin ang queue. Kapag pinatakbo mo ang halimbawang ito, makikita mo na ang IllegalStateException ay napakadalas na itinapon. Bagama't nag-synchronize kami nang tama gamit ang queue monitor, isang exception ang itinapon. Ang dahilan ay mayroon kaming dalawang magkaibang naka-synchronize na mga bloke. Isipin na mayroon kaming dalawang thread na dumating sa unang naka-synchronize na bloke. Pumasok ang unang thread sa block at natulog dahil walang laman ang pila. Ang parehong ay totoo para sa pangalawang thread. Ngayong gising na ang parehong thread (salamat sa notifyAll() na tawag na tinawag ng kabilang thread para sa monitor), pareho nilang nakikita ang value(item) sa queue na idinagdag ng producer. Pagkatapos ay parehong dumating sa pangalawang hadlang. Dito pumasok ang unang thread at nakuha ang halaga mula sa pila. Kapag pumasok ang pangalawang thread, wala nang laman ang pila. Samakatuwid, ito ay tumatanggap ng null bilang ang halaga na ibinalik mula sa pila at naghagis ng isang pagbubukod. Upang maiwasan ang mga ganitong sitwasyon, kailangan mong isagawa ang lahat ng mga operasyon na nakasalalay sa estado ng monitor sa parehong naka-synchronize na bloke: public Integer getNextInt() { Integer retVal = null; synchronized (queue) { try { while (queue.isEmpty()) { queue.wait(); } } catch (InterruptedException e) { e.printStackTrace(); } retVal = queue.poll(); } return retVal; } Dito namin isinasagawa ang poll() na pamamaraan sa parehong naka-synchronize na block gaya ng isEmpty() na paraan. Salamat sa naka-synchronize na block, sigurado kami na isang thread lang ang nagpapatupad ng paraan para sa monitor na ito sa isang partikular na oras. Samakatuwid, walang ibang thread ang maaaring mag-alis ng mga elemento mula sa pila sa pagitan ng mga tawag sa isEmpty() at poll(). Ipinagpatuloy ang pagsasalin dito .
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION