mysql 死锁监视器_并发基础知识:死锁和对象监视器
mysql 死鎖監視器
本文是我們名為Java Concurrency Essentials的學院課程的一部分。
在本課程中,您將深入探討并發的魔力。 將向您介紹并發和并發代碼的基礎知識,并學習諸如原子性,同步和線程安全性的概念。 在這里查看 !
目錄
1.活潑1.活潑
在開發使用并發實現目標的應用程序時,您可能會遇到不同線程可能相互阻塞的情況。 由于整個應用程序的運行速度比預期的慢,因此我們可以說應用程序無法按預期的時間完成。 在本節中,我們將仔細研究可能危害多線程應用程序正常運行的問題。
僵局
術語“死鎖”對于軟件開發人員來說是眾所周知的,即使是大多數普通計算機用戶也經常會使用“死鎖”這個術語,盡管它并非總是以正確的含義使用。 嚴格說來,這意味著兩個(或更多)線程分別在另一個線程上等待以釋放其已鎖定的資源,而線程本身已鎖定另一個線程在等待的資源:
Thread 1: locks resource A, waits for resource BThread 2: locks resource B, waits for resource A為了更好地理解該問題,讓我們看一下以下源代碼:
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.");}}}}} }從上面的代碼可以看出,啟動了兩個線程并嘗試鎖定兩個靜態資源。 但是對于死鎖,兩個線程需要不同的順序,因此我們利用Random實例選擇線程首先要鎖定的資源。 如果布爾變量b為true,則首先鎖定resource1,然后線程嘗試獲取對資源2的鎖定。如果b為false,則線程首先鎖定resource2,然后嘗試鎖定resource1。 在我們到達第一個死鎖之前,該程序不必運行很長時間,即,如果我們不終止它,該程序將永遠掛起:
[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.在此執行中,線程1持有資源2的鎖,并等待對resource1的鎖,而線程2持有資源1的鎖,并等待resource2。
如果將上面示例代碼中的布爾變量b設置為true,則不會遇到任何死鎖,因為線程1和線程2請求鎖的順序始終相同。 因此,兩個線程中的一個首先獲取鎖,然后請求第二個鎖,因為其他線程都在等待第一個鎖,所以第二個鎖仍然可用。
通常,可以確定以下死鎖要求:
- 互斥:有一種資源在任何時間點只能由一個線程訪問。
- 資源持有:鎖定一個資源后,線程嘗試獲取某個其他排他資源上的另一個鎖定。
- 無搶占:沒有機制,如果一個線程在特定時間段內持有鎖,則該機制可以釋放資源。
- 循環等待:在運行時發生一個星座,其中兩個(或更多)線程分別在另一個線程上等待以釋放已鎖定的資源。
盡管要求清單看起來很長,但更高級的多線程應用程序存在死鎖問題并不罕見。 但是,如果您能夠放寬上述要求之一,則可以嘗試避免死鎖:
- 互斥:這是一項通常不能放寬的要求,因為必須專門使用資源。 但這并非總是如此。 使用DBMS系統時,可以使用一種稱為Optimistic Locking的技術,而不是在必須更新的某些表行上使用悲觀鎖,這是一種可能的解決方案。
- 在等待另一個排他資源時避免資源持有的可能解決方案是在算法開始時鎖定所有必要的資源,并在不可能獲得所有鎖定的情況下釋放所有資源。 當然,這并非總是可能的,鎖定的資源可能并不事先知道,或者就像浪費資源一樣。
- 如果無法立即獲得鎖定,則避免超時的可能解決方案是引入超時。 例如,SDK類ReentrantLock提供了指定鎖定超時的可能性。
- 從上面的示例代碼可以看出,如果不同線程之間的鎖定請求順序沒有不同,則不會出現死鎖。 如果您能夠將所有鎖定代碼放入所有線程都必須通過的一種方法中,則可以輕松地控制它。
在更高級的應用程序中,您甚至可以考慮實現死鎖檢測系統。 在這里,您將必須實現某種類型的線程監視,其中每個線程都報告已成功獲取鎖以及其嘗試獲取鎖的嘗試。 如果將線程和鎖建模為有向圖,則可以檢測到兩個不同的線程何時擁有資源,同時請求另一個阻塞的資源。 然后,如果您可以強制阻塞線程釋放獲得的資源,則可以自動解決死鎖情況。
饑餓
調度程序決定下一步應該在狀態RUNNABLE中執行的線程 。 該決定基于線程的優先級; 因此,具有較低優先級的線程比具有較高優先級的線程獲得的CPU時間更少。 聽起來很合理的功能在濫用時也會引起問題。 如果大多數時間都執行高優先級的線程,則低優先級的線程似乎“餓死了”,因為它們沒有足夠的時間正確執行其工作。 因此,建議僅在有充分理由的情況下設置線程的優先級。
線程匱乏的一個復雜示例是例如finalize()方法。 Java語言的此功能可用于在對象被垃圾回收之前執行代碼。 但是,當您查看終結器線程的優先級時,您可能會發現它的運行優先級最高。 因此,如果與其他代碼相比,對象的finalize()方法花費太多時間,則可能導致線程不足。
執行時間的另一個問題是問題,即未定義線程傳遞同步塊的順序。 當許多并行線程必須傳遞某些封裝在同步塊中的代碼時,某些線程可能要比其他線程等待更長的時間,直到它們可以進入該塊為止。 從理論上講,它們可能永遠不會進入障礙。
后一種問題的解決方案是所謂的“公平”鎖定。 當選擇下一個要傳遞的線程時,公平鎖會考慮線程的等待時間。 Java SDK提供了一個公平鎖的示例實現:java.util.concurrent.locks.ReentrantLock。 如果使用布爾標志設置為true的構造函數,則ReentrantLock授予對最長等待線程的訪問權限。 這保證了沒有饑餓,但是同時引入了以下問題:沒有考慮線程優先級,因此可能會更頻繁地執行經常在此屏障處等待的優先級較低的線程。 最后但并非最不重要的一點是,ReentrantLock類當然只能考慮正在等待鎖的線程,即,執行頻率足以達到鎖的線程。 如果線程優先級太低,則可能不會經常發生這種情況,因此,具有更高優先級的線程仍會更頻繁地通過鎖。
2.使用wait()和notify()進行對象監控
多線程計算中的一個常見任務是讓一些工作線程正在等待其生產者為其創建工作。 但是,據我們了解,就CPU時間而言,在循環中忙于等待并檢查某些值并不是一個好的選擇。 在此用例中,Thread.sleep()方法也沒有太大價值,因為我們希望在提交后立即開始工作。
因此,Java編程語言具有另一種可在這種情況下使用的構造:wait()和notify()。 每個對象都從java.lang.Object類繼承的wait()方法可用于暫停當前線程執行,并等待直到另一個線程使用notify()方法將我們喚醒。 為了正常工作,調用wait()方法的線程必須持有它已獲得的鎖,然后才能使用synced關鍵字。 當調用wait()時,鎖被釋放,線程等待直到擁有該鎖的另一個線程在同一對象實例上調用notify()。
在多線程應用程序中,當然可能有多個線程在等待某個對象的通知。 因此,有兩種不同的喚醒線程的方法:notify()和notifyAll()。 第一種方法僅喚醒其中一個等待線程,而notifyAll()方法將其全部喚醒。 但是請注意,與synced關鍵字類似,沒有規則指定在調用notify()時接下來喚醒哪個線程。 在簡單的生產者和消費者示例中,這無關緊要,因為我們對哪個線程完全喚醒的事實不感興趣。
下面的代碼演示了如何使用wait()和notify()機制來使使用者線程等待從某個生產者線程推送到隊列中的新工作:
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()方法啟動五個使用者線程和一個生產者線程,然后等待它們完成。 然后,生產者線程將新值插入隊列,然后通知所有等待線程發生了某些事情。 使用者線程獲取隊列鎖,然后進入睡眠狀態,以便稍后再次填充隊列時被喚醒。 生產者線程完成工作后,會通知所有消費者線程喚醒。 如果我們不做最后一步,那么消費者線程將永遠等待下一個通知,因為我們沒有為等待指定任何超時。 取而代之的是,我們至少可以在經過一定時間后使用wait(long timeout)方法來喚醒它。
帶wait()和notify()的嵌套同步塊
如上一節所述,在對象監視器上調用wait()僅釋放該對象監視器上的鎖。 由同一線程持有的其他鎖不會被釋放。 因為這很容易理解,所以在日常工作中,調用wait()的線程可能會進一步鎖定。 而且,如果其他線程也在等待這些鎖,則會發生死鎖情況。 讓我們看下面的示例代碼:
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();} }正如我們之前所了解的 ,將同步添加到方法簽名等于創建一個synced(this){}塊。 在上面的示例中,我們意外地向該方法添加了synced關鍵字,然后在對象監視器隊列上進行了同步,以便在等待隊列中的下一個值時將當前線程置于睡眠狀態。 然后,當前線程釋放隊列上的鎖保持,但不釋放對此的鎖保持。 putInt()方法通知睡眠線程已添加新值。 但是,偶然地,我們還向該方法添加了關鍵字sync。 現在,當第二個線程進入睡眠狀態時,它仍然持有該鎖。 然后,第一個線程無法進入方法putInt(),因為此鎖由第一個線程持有。 因此,我們陷入僵局,程序掛起。 如果執行上面的代碼,則在程序開始后立即發生。
在日常生活中,情況可能不像上面那樣清楚。 線程持有的鎖可能取決于運行時參數和條件,而導致問題的同步塊可能與代碼中我們放置wait()調用的位置不太接近。 這使得很難找到此類問題,并且可能是這些問題僅在一段時間后或在高負載下才會出現。
同步塊中的條件
在對同步對象執行某些操作之前,通常您必須檢查是否滿足某些條件。 例如,當您有一個隊列時,您要等待直到該隊列被填滿。 因此,您可以編寫一種檢查隊列是否已滿的方法。 如果沒有,則在喚醒當前線程之前使其處于睡眠狀態:
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; }上面的代碼在調用wait()之前在隊列上進行同步,然后在while循環內等待,直到隊列中至少有一個條目。 第二個同步塊再次將隊列用作對象監視器。 它輪詢()隊列中的內部值。 為了演示起見,當poll()返回null時,拋出IllegalStateException。 當隊列中沒有要輪詢的值時,就是這種情況。
運行此示例時,您將看到IllegalStateException很快就會拋出。 盡管我們已經在隊列監視器上正確地同步了,但是會拋出異常。 原因是我們有兩個單獨的同步塊。 假設我們有兩個線程到達了第一個同步塊。 第一個線程進入該塊并由于隊列為空而進入睡眠狀態。 第二個線程也是如此。 現在,當兩個線程都喚醒時(通過另一個在監視器上調用notifyAll()的線程),它們都在隊列中看到了一個值(生產者添加的值。然后,兩個線程到達第二個屏障。輪詢隊列中的值,當第二個線程進入時,隊列已為空,因此它從poll()調用返回的值作為null并引發異常。
為避免出現上述情況,您將必須在同一同步塊中執行所有取決于監視器狀態的操作:
public Integer getNextInt() {Integer retVal = null;synchronized (queue) {try {while (queue.isEmpty()) {queue.wait();}} catch (InterruptedException e) {e.printStackTrace();}retVal = queue.poll();}return retVal; }在這里,我們在與isEmpty()方法相同的同步塊中執行方法poll()。 通過同步塊,我們可以確保在給定的時間點上只有一個線程正在此監視器上執行方法。 因此,沒有其他線程可以從isEmpty()和poll()調用之間的隊列中刪除元素。
3.多線程設計
正如我們在上一節中所看到的,實現多線程應用程序有時比乍一看要復雜。 因此,在啟動項目時務必牢記清晰的設計。
不變的對象
在這種情況下,非常重要的一種設計規則是不變性。 如果在不同線程之間共享對象實例,則必須注意兩個線程不會同時修改同一對象。 但是在無法更改的情況下,不可修改的對象很容易處理。 要修改數據時,始終必須構造一個新實例。 基本類java.lang.String是不可變類的示例。 每次您要更改字符串時,都會得到一個新實例:
String str = "abc";String substr = str.substring(1);盡管創建對象的操作并非沒有成本,但是這些成本經常被高估。 但是,如果具有不可變對象的簡單設計勝過不使用不可變對象,則總要權衡一下,因為存在存在并發錯誤的風險,而該錯誤可能會在項目后期出現。
在下面的內容中,您將找到一組要使類不可變的規則:
- 所有字段均應為最終字段和私有字段。
- 不應使用setter方法。
- 為了防止子類違反不變性原則,應將類本身聲明為final。
- 如果字段不是原始類型,而是對另一個對象的引用:
- 不應有將引用直接暴露給調用方的getter方法。
下列類的實例表示一條消息,其中包含主題,消息正文和一些鍵/值對:
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);} }該類是不可變的,因為它的所有字段都是final和private。 在構造實例之后,沒有任何方法可以修改實例的狀態。 返回對主題和消息的引用是安全的,因為String本身是一個不變的類。 例如,獲得消息引用的呼叫者無法直接對其進行修改。 對于標題映射,我們必須更加注意。 只要返回對Map的引用,調用者就可以更改其內容。 因此,我們必須返回通過調用Collections.unmodifiableMap()獲得的不可修改的Map。 這將返回Map上的一個視圖,該視圖允許調用者讀取值(再次為字符串),但不允許修改。 嘗試修改Map實例時,將引發UnsupportedOperationException。 在此示例中,返回特定鍵的值也是安全的,就像在getHeader(String key)中完成操作一樣,因為返回的String再次是不可變的。 如果Map包含本身不可變的對象,則此操作將不是線程安全的。
API設計
在設計類的公共方法(即此類的API)時,您也可以嘗試將其設計用于多線程使用。 當對象處于特定狀態時,您可能有不應執行的方法。 克服這種情況的一種簡單解決方案是擁有一個私有標志,該標志指示我們處于哪種狀態,并在不應該調用特定方法的情況下拋出例如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("...");}...} }上面的模式通常也被稱為“禁止模式”,因為該方法一旦在錯誤的狀態下執行便會失敗。 但是您可以使用靜態工廠方法設計相同的功能,而無需在每個方法中檢查對象的狀態:
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() {...} }靜態工廠方法使用私有構造函數創建Job的新實例,并已在實例上調用start()。 返回的Job引用已經處于可以使用的正確狀態,因此getResults()方法僅需要同步,而不必檢查對象的狀態。
線程本地存儲
到目前為止,我們已經看到線程共享相同的內存。 就性能而言,這是在線程之間共享數據的好方法。 如果我們將使用單獨的進程來并行執行代碼,那么我們將擁有更繁重的數據交換方法,例如遠程過程調用或文件系統或網絡級別的同步。 但是,如果同步不正確,則在不同線程之間共享內存也將難以處理。
Java中通過java.lang.ThreadLocal類提供了僅由我們自己的線程而不是其他線程使用的專用內存:
private static final ThreadLocal myThreadLocalInteger = new ThreadLocal();通用模板參數T給出了應存儲在ThreadLocal中的數據類型。在上面的示例中,我們僅使用了Integer,但在這里我們也可以使用任何其他數據類型。 以下代碼演示了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被聲明為靜態的,但每個線程輸出的正是它通過構造函數獲得的值。 ThreadLocal的內部實現確保每次調用set()時,給定值都存儲在僅當前線程有權訪問的內存區域中。 因此,當您事后調用get()時,盡管存在其他線程可能已調用set()的事實,但您仍會檢索之前設置的值。
Java EE世界中的應用程序服務器大量使用ThreadLocal功能,因為您有許多并行線程,但是每個線程都有自己的事務或安全上下文。 由于您不想在每次方法調用中傳遞這些對象,因此只需將其存儲在線程自己的內存中,并在以后需要時訪問它。
翻譯自: https://www.javacodegeeks.com/2015/09/concurrency-fundamentals-deadlocks-and-object-monitors.html
mysql 死鎖監視器
總結
以上是生活随笔為你收集整理的mysql 死锁监视器_并发基础知识:死锁和对象监视器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑怎么设置内网连接(电脑怎么设置内网连
- 下一篇: 腾讯视频破解版电脑版下载(破解版腾讯视频