并发基础知识:死锁和对象监视器
本文是我們學(xué)院課程中名為Java Concurrency Essentials的一部分 。
在本課程中,您將深入探討并發(fā)的魔力。 將向您介紹并發(fā)和并發(fā)代碼的基礎(chǔ)知識(shí),并學(xué)習(xí)諸如原子性,同步和線程安全之類的概念。 在這里查看 !
目錄
1.活潑1.活潑
在開(kāi)發(fā)使用并發(fā)實(shí)現(xiàn)其目標(biāo)的應(yīng)用程序時(shí),您可能會(huì)遇到不同線程可能相互阻塞的情況。 由于整個(gè)應(yīng)用程序的運(yùn)行速度比預(yù)期的慢,因此我們可以說(shuō)該應(yīng)用程序無(wú)法按預(yù)期的時(shí)間完成。 在本節(jié)中,我們將仔細(xì)研究可能危害多線程應(yīng)用程序正常運(yùn)行的問(wèn)題。
僵局
術(shù)語(yǔ)“死鎖”對(duì)于軟件開(kāi)發(fā)人員來(lái)說(shuō)是眾所周知的,即使是大多數(shù)普通的計(jì)算機(jī)用戶也經(jīng)常使用“死鎖”這個(gè)術(shù)語(yǔ),盡管它并非總是以正確的含義使用。 嚴(yán)格地說(shuō),這意味著兩個(gè)(或多個(gè))線程分別在另一個(gè)線程上等待以釋放它已鎖定的資源,而線程本身已鎖定另一個(gè)線程在等待的資源:
Thread 1: locks resource A, waits for resource BThread 2: locks resource B, waits for resource A為了更好地理解該問(wèn)題,讓我們看一下以下源代碼:
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.");}}}}} }從上面的代碼可以看出,啟動(dòng)了兩個(gè)線程并嘗試鎖定兩個(gè)靜態(tài)資源。 但是對(duì)于死鎖,兩個(gè)線程需要不同的順序,因此我們利用Random實(shí)例選擇線程首先要鎖定的資源。 如果布爾變量b為true,則首先鎖定resource1,然后線程嘗試獲取對(duì)資源2的鎖定。如果b為false,則線程首先鎖定resource2,然后嘗試鎖定resource1。 在我們到達(dá)第一個(gè)死鎖之前,該程序不必運(yùn)行很長(zhǎng)時(shí)間,即,如果我們不終止它,該程序?qū)⒂肋h(yuǎn)掛起:
[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.在此執(zhí)行中,線程1持有資源2的鎖并等待對(duì)resource1的鎖,而線程2持有資源1的鎖并等待resource2。
如果將上面示例代碼中的布爾變量b設(shè)置為true,則不會(huì)遇到任何死鎖,因?yàn)榫€程1和線程2請(qǐng)求鎖的順序始終相同。 因此,兩個(gè)線程中的一個(gè)首先獲取鎖,然后請(qǐng)求第二個(gè)鎖,由于其他線程等待第一個(gè)鎖,第二個(gè)鎖仍然可用。
通常,可以確定以下死鎖要求:
- 互斥:有一種資源在任何時(shí)間點(diǎn)只能由一個(gè)線程訪問(wèn)。
- 資源持有:鎖定一個(gè)資源后,線程嘗試獲取對(duì)某個(gè)其他排他資源的另一個(gè)鎖定。
- 無(wú)搶占:沒(méi)有機(jī)制,如果一個(gè)線程在特定時(shí)間段內(nèi)持有鎖,則該機(jī)制可以釋放資源。
- 循環(huán)等待:在運(yùn)行時(shí)發(fā)生一個(gè)星座,其中兩個(gè)(或更多)線程分別在另一個(gè)線程上等待以釋放已鎖定的資源。
盡管要求列表看起來(lái)很長(zhǎng),但是更高級(jí)的多線程應(yīng)用程序存在死鎖問(wèn)題并不罕見(jiàn)。 但是,如果您能夠放松上面列出的要求之一,則可以嘗試避免死鎖:
- 互斥:這是一項(xiàng)通常不能放寬的要求,因?yàn)楸仨殞iT使用資源。 但這并非總是如此。 使用DBMS系統(tǒng)時(shí),可以使用一種稱為Optimistic Locking的技術(shù),而不是在必須更新的某些表行上使用悲觀鎖,這是一種可能的解決方案。
- 在等待另一個(gè)排他資源時(shí)避免資源持有的可能解決方案是在算法開(kāi)始時(shí)鎖定所有必要的資源,并在不可能獲得所有鎖定的情況下釋放所有資源。 當(dāng)然,這并非總是可能的,也許鎖定的資源并不為人所知,或者就像浪費(fèi)資源一樣。
- 如果無(wú)法立即獲得鎖定,則避免超時(shí)的可能解決方案是引入超時(shí)。 例如,SDK類ReentrantLock提供了指定鎖定超時(shí)的可能性。
- 從上面的示例代碼可以看出,如果不同線程之間的鎖定請(qǐng)求順序沒(méi)有不同,則不會(huì)出現(xiàn)死鎖。 如果您能夠?qū)⑺墟i定代碼放入所有線程都必須通過(guò)的一種方法中,則可以輕松地控制它。
在更高級(jí)的應(yīng)用程序中,您甚至可以考慮實(shí)現(xiàn)死鎖檢測(cè)系統(tǒng)。 在這里,您將必須實(shí)現(xiàn)某種類型的線程監(jiān)視,其中每個(gè)線程都報(bào)告已成功獲取鎖以及其嘗試獲取鎖的嘗試。 如果將線程和鎖建模為有向圖,則可以檢測(cè)到兩個(gè)不同的線程何時(shí)擁有資源,同時(shí)請(qǐng)求另一個(gè)阻塞的資源。 如果然后您可以強(qiáng)制阻塞線程釋放獲得的資源,則可以自動(dòng)解決死鎖情況。
饑餓
調(diào)度程序決定下一步應(yīng)該在狀態(tài)RUNNABLE中執(zhí)行的線程 。 該決定基于線程的優(yōu)先級(jí); 因此,具有較低優(yōu)先級(jí)的線程比具有較高優(yōu)先級(jí)的線程獲得的CPU時(shí)間更少。 聽(tīng)起來(lái)很合理的功能在濫用時(shí)也會(huì)引起問(wèn)題。 如果大多數(shù)時(shí)間具有高優(yōu)先級(jí)的線程被執(zhí)行,則低優(yōu)先級(jí)的線程似乎“餓死了”,因?yàn)樗鼈儧](méi)有足夠的時(shí)間正確執(zhí)行其工作。 因此,建議僅在有充分理由的情況下設(shè)置線程的優(yōu)先級(jí)。
線程匱乏的一個(gè)復(fù)雜示例是例如finalize()方法。 Java語(yǔ)言的此功能可用于在對(duì)象被垃圾回收之前執(zhí)行代碼。 但是,當(dāng)您查看終結(jié)器線程的優(yōu)先級(jí)時(shí),您可能會(huì)發(fā)現(xiàn)它的運(yùn)行優(yōu)先級(jí)最高。 因此,如果與其他代碼相比,對(duì)象的finalize()方法花費(fèi)太多時(shí)間,則可能導(dǎo)致線程不足。
執(zhí)行時(shí)間的另一個(gè)問(wèn)題是問(wèn)題,即未定義線程以哪個(gè)順序傳遞同步塊。 當(dāng)許多并行線程必須傳遞封裝在同步塊中的某些代碼時(shí),某些線程可能比其他線程要等待更長(zhǎng)的時(shí)間才能進(jìn)入該塊。 從理論上講,它們可能永遠(yuǎn)不會(huì)進(jìn)入障礙。
后一種問(wèn)題的解決方案是所謂的“公平”鎖定。 在選擇下一個(gè)要傳遞的線程時(shí),公平鎖會(huì)考慮線程的等待時(shí)間。 Java SDK提供了一個(gè)公平鎖的示例實(shí)現(xiàn):java.util.concurrent.locks.ReentrantLock。 如果使用布爾標(biāo)志設(shè)置為true的構(gòu)造函數(shù),則ReentrantLock授予對(duì)等待時(shí)間最長(zhǎng)的線程的訪問(wèn)權(quán)限。 這保證了沒(méi)有饑餓,但是同時(shí)引入了以下問(wèn)題:沒(méi)有考慮線程優(yōu)先級(jí),因此可能會(huì)更頻繁地執(zhí)行經(jīng)常在此屏障處等待的優(yōu)先級(jí)較低的線程。 最后但并非最不重要的一點(diǎn)是,ReentrantLock類當(dāng)然只能考慮正在等待鎖的線程,即,執(zhí)行頻率足以達(dá)到鎖的線程。 如果線程優(yōu)先級(jí)太低,則可能不會(huì)經(jīng)常發(fā)生這種情況,因此,具有更高優(yōu)先級(jí)的線程仍會(huì)更頻繁地通過(guò)鎖。
2.使用wait()和notify()進(jìn)行對(duì)象監(jiān)控
多線程計(jì)算中的一項(xiàng)常見(jiàn)任務(wù)是讓一些工作線程正在等待其生產(chǎn)者為其創(chuàng)建工作。 但是,據(jù)我們了解,就CPU時(shí)間而言,在循環(huán)中忙于等待并檢查某些值并不是一個(gè)好的選擇。 在此用例中,Thread.sleep()方法也沒(méi)有太大價(jià)值,因?yàn)槲覀兿M谔峤缓罅⒓撮_(kāi)始工作。
因此,Java編程語(yǔ)言具有另一種可在這種情況下使用的構(gòu)造:wait()和notify()。 每個(gè)對(duì)象都從java.lang.Object類繼承的wait()方法可用于暫停當(dāng)前線程執(zhí)行,并等待直到另一個(gè)線程使用notify()方法將我們喚醒。 為了正常工作,調(diào)用wait()方法的線程必須持有它已獲得的鎖,然后才能使用synced關(guān)鍵字。 當(dāng)調(diào)用wait()時(shí),鎖被釋放,線程等待直到擁有該鎖的另一個(gè)線程在同一對(duì)象實(shí)例上調(diào)用notify()為止。
在多線程應(yīng)用程序中,當(dāng)然可能有多個(gè)線程在等待某個(gè)對(duì)象的通知。 因此,有兩種不同的喚醒線程的方法:notify()和notifyAll()。 第一個(gè)方法僅喚醒一個(gè)等待線程,而notifyAll()方法將它們?nèi)繂拘选?但是請(qǐng)注意,類似于synced關(guān)鍵字,沒(méi)有規(guī)則指定調(diào)用notify()時(shí)接下來(lái)喚醒哪個(gè)線程。 在簡(jiǎn)單的生產(chǎn)者和消費(fèi)者示例中,這無(wú)關(guān)緊要,因?yàn)槲覀儗?duì)哪個(gè)線程完全喚醒的事實(shí)不感興趣。
下面的代碼演示了如何使用wait()和notify()機(jī)制來(lái)讓使用者線程等待從某些生產(chǎn)者線程推送到隊(duì)列中的新工作:
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();} }main()方法啟動(dòng)五個(gè)使用者和一個(gè)生產(chǎn)者線程,然后等待它們完成。 然后,生產(chǎn)者線程將新值插入隊(duì)列,然后通知所有等待線程發(fā)生了某些事情。 使用者線程獲取隊(duì)列鎖,然后進(jìn)入睡眠狀態(tài),以便稍后再次填充隊(duì)列時(shí)被喚醒。 生產(chǎn)者線程完成工作后,會(huì)通知所有消費(fèi)者線程喚醒。 如果我們不做最后一步,那么消費(fèi)者線程將永遠(yuǎn)等待下一個(gè)通知,因?yàn)槲覀儧](méi)有為等待指定任何超時(shí)。 取而代之的是,我們至少可以在經(jīng)過(guò)一定時(shí)間后使用wait(long timeout)方法來(lái)喚醒它。
帶wait()和notify()的嵌套同步塊
如上一節(jié)所述,在對(duì)象監(jiān)視器上調(diào)用wait()僅釋放該對(duì)象監(jiān)視器上的鎖。 由同一線程持有的其他鎖不會(huì)被釋放。 因?yàn)檫@很容易理解,所以在日常工作中,調(diào)用wait()的線程可能會(huì)進(jìn)一步鎖定。 而且,如果其他線程也在等待這些鎖,則會(huì)發(fā)生死鎖情況。 讓我們看下面的示例代碼:
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();} }正如我們之前所了解的 ,將同步添加到方法簽名等同于創(chuàng)建一個(gè)synced(this){}塊。 在上面的示例中,我們意外地向該方法添加了synced關(guān)鍵字,然后在對(duì)象監(jiān)視器隊(duì)列上進(jìn)行了同步,以便在等待隊(duì)列中的下一個(gè)值時(shí)將當(dāng)前線程置于睡眠狀態(tài)。 然后,當(dāng)前線程釋放隊(duì)列上的鎖保持,但不釋放對(duì)此的鎖保持。 putInt()方法通知睡眠線程已添加新值。 但是,偶然地,我們還向該方法添加了關(guān)鍵字sync。 現(xiàn)在,第二個(gè)線程進(jìn)入睡眠狀態(tài)時(shí),它仍然保持鎖定狀態(tài)。 然后,第一個(gè)線程無(wú)法進(jìn)入方法putInt(),因?yàn)榇随i由第一個(gè)線程持有。 因此,我們陷入僵局,程序掛起。 如果執(zhí)行上面的代碼,則在程序開(kāi)始后立即發(fā)生。
在日常生活中,情況可能不像上面那樣清楚。 線程持有的鎖可能取決于運(yùn)行時(shí)參數(shù)和條件,導(dǎo)致問(wèn)題的同步塊可能與代碼中我們放置wait()調(diào)用的位置不太接近。 這使得很難找到此類問(wèn)題,并且可能是這些問(wèn)題僅在一段時(shí)間后或在高負(fù)荷下才會(huì)出現(xiàn)。
同步塊中的條件
在同步對(duì)象上執(zhí)行某些操作之前,通常必須檢查是否滿足某些條件。 例如,當(dāng)您有一個(gè)隊(duì)列時(shí),您要等待直到該隊(duì)列被填滿。 因此,您可以編寫一種檢查隊(duì)列是否已滿的方法。 如果不是,則在喚醒當(dāng)前線程之前使其處于睡眠狀態(tài):
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; }上面的代碼在調(diào)用wait()之前在隊(duì)列上進(jìn)行同步,然后在while循環(huán)內(nèi)等待,直到隊(duì)列中至少有一個(gè)條目。 第二個(gè)同步塊再次將隊(duì)列用作對(duì)象監(jiān)視器。 它輪詢()隊(duì)列中的內(nèi)部值。 為了演示起見(jiàn),當(dāng)poll()返回null時(shí),拋出IllegalStateException。 當(dāng)隊(duì)列中沒(méi)有要輪詢的值時(shí),就是這種情況。
運(yùn)行此示例時(shí),您將看到IllegalStateException很快就會(huì)拋出。 盡管我們已經(jīng)在隊(duì)列監(jiān)視器上正確地同步了,但是會(huì)引發(fā)異常。 原因是我們有兩個(gè)單獨(dú)的同步塊。 假設(shè)我們有兩個(gè)線程到達(dá)了第一個(gè)同步塊。 第一個(gè)線程進(jìn)入該塊并由于隊(duì)列為空而進(jìn)入睡眠狀態(tài)。 第二個(gè)線程也是如此。 現(xiàn)在,當(dāng)兩個(gè)線程都喚醒時(shí)(通過(guò)另一個(gè)在監(jiān)視器上調(diào)用notifyAll()的線程),它們都在隊(duì)列中看到一個(gè)值(生產(chǎn)者添加的值。然后,兩個(gè)線程到達(dá)第二個(gè)屏障。在這里,第一個(gè)線程進(jìn)入輪詢隊(duì)列中的值,當(dāng)?shù)诙€(gè)線程進(jìn)入時(shí),隊(duì)列已為空,因此它從poll()調(diào)用返回的值作為null并引發(fā)異常。
為避免出現(xiàn)上述情況,您將必須在同一同步塊中執(zhí)行所有取決于監(jiān)視器狀態(tài)的操作:
public Integer getNextInt() {Integer retVal = null;synchronized (queue) {try {while (queue.isEmpty()) {queue.wait();}} catch (InterruptedException e) {e.printStackTrace();}retVal = queue.poll();}return retVal; }在這里,我們?cè)谂cisEmpty()方法相同的同步塊中執(zhí)行poll()方法。 通過(guò)同步塊,我們可以確保在給定的時(shí)間點(diǎn)上只有一個(gè)線程正在此監(jiān)視器上執(zhí)行方法。 因此,沒(méi)有其他線程可以從isEmpty()和poll()調(diào)用之間的隊(duì)列中刪除元素。
3.設(shè)計(jì)多線程
正如我們?cè)谧詈髱坠?jié)中所看到的,實(shí)現(xiàn)多線程應(yīng)用程序有時(shí)比乍一看要復(fù)雜。 因此,在啟動(dòng)項(xiàng)目時(shí),請(qǐng)務(wù)必牢記清晰的設(shè)計(jì)。
不變的對(duì)象
在這種情況下,非常重要的一種設(shè)計(jì)規(guī)則是不變性。 如果在不同線程之間共享對(duì)象實(shí)例,則必須注意兩個(gè)線程不會(huì)同時(shí)修改同一對(duì)象。 但是在無(wú)法更改的情況下,不可修改的對(duì)象很容易處理。 要修改數(shù)據(jù)時(shí),始終必須構(gòu)造一個(gè)新實(shí)例。 基本類java.lang.String是不可變類的示例。 每次您要更改字符串時(shí),都會(huì)得到一個(gè)新實(shí)例:
String str = "abc";String substr = str.substring(1);盡管創(chuàng)建對(duì)象的操作并非沒(méi)有成本,但是這些成本經(jīng)常被高估。 但是,如果具有不可變對(duì)象的簡(jiǎn)單設(shè)計(jì)勝過(guò)不使用不可變對(duì)象,則總要權(quán)衡一下,因?yàn)榇嬖诖嬖诓l(fā)錯(cuò)誤的風(fēng)險(xiǎn),在項(xiàng)目中可能會(huì)發(fā)現(xiàn)并發(fā)錯(cuò)誤。
在下面的內(nèi)容中,您將找到一組要使類不可變的適用規(guī)則:
- 所有字段均應(yīng)為最終字段和私有字段。
- 不應(yīng)使用setter方法。
- 應(yīng)該將類本身聲明為final,以防止子類違反不變性原則。
- 如果字段不是原始類型,而是對(duì)另一個(gè)對(duì)象的引用:
- 不應(yīng)有將引用直接暴露給調(diào)用者的getter方法。
下列類的實(shí)例表示一條消息,其中包含主題,消息正文和一些鍵/值對(duì):
public final class ImmutableMessage {private final String subject;private final String message;private final Map<String,String> header;public ImmutableMessage(Map<String,String> header, String subject, String message) {this.header = new HashMap<String,String>(header);this.subject = subject;this.message = message;}public String getSubject() {return subject;}public String getMessage() {return message;}public String getHeader(String key) {return this.header.get(key);}public Map<String,String> getHeaders() {return Collections.unmodifiableMap(this.header);} }該類是不可變的,因?yàn)樗乃凶侄味际莊inal和private。 在構(gòu)造實(shí)例后,沒(méi)有任何方法可以修改實(shí)例的狀態(tài)。 返回對(duì)主題和消息的引用是安全的,因?yàn)镾tring本身是一個(gè)不變的類。 例如,獲得消息引用的呼叫者無(wú)法直接對(duì)其進(jìn)行修改。 對(duì)于標(biāo)題映射,我們必須更加注意。 只要返回對(duì)Map的引用,調(diào)用者就可以更改其內(nèi)容。 因此,我們必須返回通過(guò)調(diào)用Collections.unmodifiableMap()獲得的不可修改Map。 這將返回Map上的視圖,該視圖允許調(diào)用者讀取值(再次為字符串),但不允許修改。 嘗試修改Map實(shí)例時(shí),將引發(fā)UnsupportedOperationException。 在此示例中,返回特定鍵的值也是安全的,就像在getHeader(String key)中完成操作一樣,因?yàn)榉祷氐腟tring再次是不可變的。 如果Map包含本身不可變的對(duì)象,則此操作將不是線程安全的。
API設(shè)計(jì)
在設(shè)計(jì)類的公共方法(即此類的API)時(shí),您也可以嘗試將其設(shè)計(jì)用于多線程使用。 當(dāng)對(duì)象處于特定狀態(tài)時(shí),您可能有不應(yīng)執(zhí)行的方法。 克服這種情況的一個(gè)簡(jiǎn)單解決方案是擁有一個(gè)私有標(biāo)志,該標(biāo)志指示我們處于哪種狀態(tài),并且在不應(yīng)調(diào)用特定方法時(shí)拋出IllegalStateException:
public class Job {private boolean running = false;private final String filename;public Job(String filename) {this.filename = filename;}public synchronized void start() {if(running) {throw new IllegalStateException("...");}...}public synchronized List getResults() {if(!running) {throw new IllegalStateException("...");}...} }上面的模式通常也稱為“禁止模式”,因?yàn)樵摲椒ㄒ坏┰阱e(cuò)誤的狀態(tài)下執(zhí)行便會(huì)失敗。 但是您可以使用靜態(tài)工廠方法設(shè)計(jì)相同的功能,而無(wú)需在每個(gè)方法中檢查對(duì)象的狀態(tài):
public class Job {private final String filename;private Job(String filename) {this.filename = filename;}public static Job createAndStart(String filename) {Job job = new Job(filename);job.start();return job;}private void start() {...}public synchronized List getResults() {...} }靜態(tài)工廠方法使用私有構(gòu)造函數(shù)創(chuàng)建Job的新實(shí)例,并已在實(shí)例上調(diào)用start()。 返回的Job引用已經(jīng)處于可以使用的正確狀態(tài),因此getResults()方法僅需要同步,而不必檢查對(duì)象的狀態(tài)。
線程本地存儲(chǔ)
到目前為止,我們已經(jīng)看到線程共享相同的內(nèi)存。 就性能而言,這是在線程之間共享數(shù)據(jù)的好方法。 如果我們將使用單獨(dú)的進(jìn)程來(lái)并行執(zhí)行代碼,那么我們將擁有更繁重的數(shù)據(jù)交換方法,例如遠(yuǎn)程過(guò)程調(diào)用或文件系統(tǒng)或網(wǎng)絡(luò)級(jí)別的同步。 但是,如果同步不正確,則在不同線程之間共享內(nèi)存也將難以處理。
Java中的java.lang.ThreadLocal類提供了僅由我們自己的線程而不是其他線程使用的專用內(nèi)存:
private static final ThreadLocal myThreadLocalInteger = new ThreadLocal();通用模板參數(shù)T給出了應(yīng)存儲(chǔ)在ThreadLocal內(nèi)的數(shù)據(jù)類型。在上面的示例中,我們僅使用了Integer,但在這里我們也可以使用任何其他數(shù)據(jù)類型。 以下代碼演示了ThreadLocal的用法:
public class ThreadLocalExample implements Runnable {private static final ThreadLocal threadLocal = new ThreadLocal();private final int value;public ThreadLocalExample(int value) {this.value = value;}@Overridepublic void run() {threadLocal.set(value);Integer integer = threadLocal.get();System.out.println("[" + Thread.currentThread().getName() + "]: " + integer);}public static void main(String[] args) throws InterruptedException {Thread threads[] = new Thread[5];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new ThreadLocalExample(i), "thread-" + i);threads[i].start();}for (int i = 0; i < threads.length; i++) {threads[i].join();}} }您可能想知道,即使變量threadLocal被聲明為靜態(tài)的,每個(gè)線程也輸出的正是通過(guò)構(gòu)造函數(shù)獲得的值。 ThreadLocal的內(nèi)部實(shí)現(xiàn)確保每次調(diào)用set()時(shí),給定值都存儲(chǔ)在僅當(dāng)前線程有權(quán)訪問(wèn)的內(nèi)存區(qū)域中。 因此,當(dāng)您事后調(diào)用get()時(shí),盡管存在其他線程可能已經(jīng)調(diào)用過(guò)set()的事實(shí),但仍會(huì)檢索之前設(shè)置的值。
Java EE世界中的應(yīng)用程序服務(wù)器大量使用ThreadLocal功能,因?yàn)槟性S多并行線程,但是每個(gè)線程都有自己的事務(wù)或安全上下文。 由于您不想在每次方法調(diào)用中傳遞這些對(duì)象,因此只需將其存儲(chǔ)在線程自己的內(nèi)存中,并在以后需要時(shí)訪問(wèn)它。
翻譯自: https://www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html
總結(jié)
以上是生活随笔為你收集整理的并发基础知识:死锁和对象监视器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 下载相机电脑版下载安装(相机软件下载安装
- 下一篇: js 引入 缓存_引入故意缓存