死鎖
多個線程各自占有一些共享資源,并且互相等待其他線程占有的資源才能運行,而導致兩個或者多個線程 都在等待對方釋放資源,都停止執行的情形,某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能 會發生“死鎖"的問題;
死鎖是指多個線程在執行過程中,因為爭奪資源而造成的一種互相等待的現象,導致這些線程都無法繼續執行下去。
練習代碼
package com.lock;/*
死鎖
多個線程各自占有一些共享資源,并且互相等待其他線程占有的資源才能運行,而導致兩個或者多個線程
都在等待對方釋放資源,都停止執行的情形,某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能
會發生“死鎖"的問題;*///死鎖:多個線程互相抱著對方需要的資源,然后形成僵持
public class DeadLock {public static void main(String[] args) {Makeup g1 = new Makeup(0,"灰姑涼");Makeup g2 = new Makeup(0,"白雪公主");g1.start();g2.start();}
}//口紅(Lipstick)
class Lipstick{}//鏡子(Mirror)
class Mirror{}//化妝(Makeup)
class Makeup extends Thread{//需要的資源只有一份,用static來抱著只有一份static Lipstick lipstick = new Lipstick();static Mirror mirror = new Mirror();int choice;//選擇String girlName;//使用化妝品的人Makeup(int choice,String girlName){this.choice = choice;this.girlName = girlName;}@Overridepublic void run() {//化妝try {makeup();} catch (InterruptedException e) {e.printStackTrace();}}//化妝,互相持有對方的鎖,就是需要拿到對方的資源private void makeup() throws InterruptedException {if (choice == 0) {synchronized (lipstick) {//獲得口紅的鎖System.out.println(this.girlName + "獲得口紅的鎖");Thread.sleep(1000);synchronized (mirror){//1s鐘后想獲得鏡子 的鎖System.out.println(this.girlName + "獲得鏡子的鎖");}}}else {synchronized (mirror) {//獲得鏡子的鎖System.out.println(this.girlName + "獲得鏡子的鎖");Thread.sleep(2000);synchronized (lipstick){//2s鐘后想獲得口紅 的鎖System.out.println(this.girlName + "獲得口紅的鎖");}}}}
}
代碼結構
-
DeadLock類:這是主類,包含
main
方法,用于啟動兩個線程。 -
Lipstick類和Mirror類:這兩個類分別代表口紅和鏡子,是共享資源。
-
Makeup類:繼承自
Thread
類,表示一個化妝的線程。每個線程代表一個女孩,她們需要使用口紅和鏡子來化妝。
代碼邏輯
-
共享資源:
-
Lipstick
和Mirror
是兩個共享資源,分別代表口紅和鏡子。 -
這兩個資源被聲明為
static
,確保它們在所有Makeup
實例之間共享。
-
-
Makeup類:
-
choice
:表示女孩的選擇,決定她們先獲取哪個資源。 -
girlName
:表示女孩的名字。 -
run()
方法:線程啟動后執行的方法,調用makeup()
方法。 -
makeup()
方法:模擬化妝過程,嘗試獲取口紅和鏡子的鎖。
-
-
死鎖的產生:
-
如果
choice
為0,線程會先獲取口紅的鎖,然后嘗試獲取鏡子的鎖。 -
如果
choice
為1,線程會先獲取鏡子的鎖,然后嘗試獲取口紅的鎖。 -
由于兩個線程的執行順序不同,可能會導致以下情況:
-
線程1(灰姑涼)持有口紅的鎖,等待鏡子的鎖。
-
線程2(白雪公主)持有鏡子的鎖,等待口紅的鎖。
-
-
這樣,兩個線程互相等待對方釋放資源,導致死鎖。
-
代碼執行流程
-
啟動線程:
-
g1
和g2
兩個線程分別啟動,代表灰姑涼和白雪公主。 -
g1
的choice
為0,g2
的choice
為1。
-
-
線程執行:
-
g1
先獲取口紅的鎖,然后嘗試獲取鏡子的鎖。 -
g2
先獲取鏡子的鎖,然后嘗試獲取口紅的鎖。
-
-
死鎖發生:
-
g1
持有口紅的鎖,等待g2
釋放鏡子的鎖。 -
g2
持有鏡子的鎖,等待g1
釋放口紅的鎖。 -
兩個線程都無法繼續執行,形成死鎖。
-
如何避免死鎖
-
鎖的順序:確保所有線程以相同的順序獲取鎖。例如,所有線程都先獲取口紅的鎖,再獲取鏡子的鎖。
-
超時機制:在獲取鎖時設置超時時間,如果超時則釋放已持有的鎖并重試。
-
死鎖檢測:使用工具或算法檢測死鎖,并采取相應措施解除死鎖。
優化后代碼
package com.lock;/*
死鎖
多個線程各自占有一些共享資源,并且互相等待其他線程占有的資源才能運行,而導致兩個或者多個線程
都在等待對方釋放資源,都停止執行的情形,某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能
會發生“死鎖"的問題;死鎖避免方法
產生死鎖的四個必要條件
1.互斥條件:一個資源每次只能被一個進程使用;
2.請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放;
3.不剝奪條件:進程已獲得的資源,在未使用完之前,不能強行剝奪;
4,循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。只要想辦法破其中的任意一個或多個條件就可以避免死鎖發生*///死鎖:多個線程互相抱著對方需要的資源,然后形成僵持
public class DeadLock {public static void main(String[] args) {Makeup g1 = new Makeup(0,"灰姑涼");Makeup g2 = new Makeup(0,"白雪公主");g1.start();g2.start();}
}//口紅(Lipstick)
class Lipstick{}//鏡子(Mirror)
class Mirror{}//化妝(Makeup)
class Makeup extends Thread{//需要的資源只有一份,用static來抱著只有一份static Lipstick lipstick = new Lipstick();static Mirror mirror = new Mirror();int choice;//選擇String girlName;//使用化妝品的人Makeup(int choice,String girlName){this.choice = choice;this.girlName = girlName;}@Overridepublic void run() {//化妝try {makeup();} catch (InterruptedException e) {e.printStackTrace();}}//化妝,互相持有對方的鎖,就是需要拿到對方的資源private void makeup() throws InterruptedException {if (choice == 0) {synchronized (lipstick) {//獲得口紅的鎖System.out.println(this.girlName + "獲得口紅的鎖");Thread.sleep(1000);}synchronized (mirror){//1s鐘后想獲得鏡子 的鎖System.out.println(this.girlName + "獲得鏡子的鎖");}}else {synchronized (mirror) {//獲得鏡子的鎖System.out.println(this.girlName + "獲得鏡子的鎖");Thread.sleep(2000);}synchronized (lipstick){//2s鐘后想獲得口紅 的鎖System.out.println(this.girlName + "獲得口紅的鎖");}}}
}
修改后的代碼分析
關鍵修改點
-
鎖的嵌套被移除:
-
在原始代碼中,
synchronized
塊是嵌套的,即一個線程在持有第一個鎖的情況下嘗試獲取第二個鎖。 -
在修改后的代碼中,
synchronized
塊是分開的,線程在釋放第一個鎖之后才會嘗試獲取第二個鎖。
-
-
鎖的獲取順序:
-
修改后的代碼中,線程不會同時持有兩個鎖,而是先釋放一個鎖,再嘗試獲取另一個鎖。
-
這樣就不會出現兩個線程互相等待對方釋放鎖的情況。
-
修改后的代碼執行邏輯
線程1(灰姑涼)的執行流程:
-
獲取
lipstick
的鎖。 -
打印“灰姑涼獲得口紅的鎖”。
-
釋放
lipstick
的鎖。 -
獲取
mirror
的鎖。 -
打印“灰姑涼獲得鏡子的鎖”。
-
釋放
mirror
的鎖。
線程2(白雪公主)的執行流程:
-
獲取
mirror
的鎖。 -
打印“白雪公主獲得鏡子的鎖”。
-
釋放
mirror
的鎖。 -
獲取
lipstick
的鎖。 -
打印“白雪公主獲得口紅的鎖”。
-
釋放
lipstick
的鎖。
為什么避免了死鎖?
-
鎖的釋放:
-
每個線程在獲取一個鎖后,會先釋放它,再嘗試獲取另一個鎖。
-
這樣就不會出現一個線程持有
lipstick
的鎖并等待mirror
的鎖,而另一個線程持有mirror
的鎖并等待lipstick
的鎖的情況。
-
-
沒有互相等待:
-
線程1和線程2不會同時持有對方需要的鎖,因此不會形成互相等待的僵局。
-
Lock鎖?
1、JDK5.0開始,Java提供了更強大的線程同步機制--通過顯式定義同步鎖對象來實現同步。
同步鎖使用Lock對象充當
2、java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。
鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象
ReentrantLoc類(可重入鎖)實現了Lock,它擁有與synchronized相同的并發性和內存語義,在實現線程安全的控制中,比較常用的是
ReentrantLock,可以顯式加鎖、釋放鎖
以下代碼演示了如何使用ReentrantLock
來實現線程同步,確保多個線程安全地訪問共享資源。ReentrantLock
是Java中提供的一種顯式鎖機制,相比于synchronized
關鍵字,它提供了更靈活的鎖控制方式。
package com.lock;import java.util.concurrent.locks.ReentrantLock;/*
Lock(鎖)
1、JDK5.0開始,Java提供了更強大的線程同步機制--通過顯式定義同步鎖對象來實現同步。
同步鎖使用Lock對象充當
2、java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。
鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象
ReentrantLoc類(可重入鎖)實現了Lock,它擁有與synchronized相同的并發性和內存語義,在實現線程安全的控制中,比較常用的是
ReentrantLock,可以顯式加鎖、釋放鎖*/
//測試lock鎖
public class TestLock {public static void main(String[] args) {TestLock2 testLock2 = new TestLock2();new Thread(testLock2).start();new Thread(testLock2).start();new Thread(testLock2).start();}
}class TestLock2 implements Runnable{int ticketNums = 10;//定義lock鎖private final ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true){try {lock.lock();//加鎖if (ticketNums > 0) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(ticketNums--);}else {break;}}finally {//解鎖lock.unlock();}}}
}
代碼結構
-
TestLock類:
-
這是主類,包含
main
方法,用于啟動多個線程。 -
創建了一個
TestLock2
對象,并啟動三個線程來執行該對象的run
方法。
-
-
TestLock2類:
-
實現了
Runnable
接口,表示一個任務,可以被多個線程執行。 -
包含一個共享資源
ticketNums
(票數),多個線程會競爭訪問和修改這個資源。 -
使用
ReentrantLock
來確保對ticketNums
的訪問是線程安全的。
-
代碼邏輯
1.?共享資源
-
ticketNums
:表示剩余的票數,初始值為10。 -
多個線程會同時訪問和修改
ticketNums
,因此需要確保線程安全。
2.?ReentrantLock
-
ReentrantLock
是一個可重入鎖,允許線程多次獲取同一把鎖。 -
通過
lock()
方法加鎖,通過unlock()
方法解鎖。 -
使用
try-finally
塊確保鎖一定會被釋放,避免死鎖。
3.?線程執行邏輯
-
每個線程執行
TestLock2
的run
方法。 -
在
while (true)
循環中,線程不斷嘗試獲取鎖并訪問共享資源ticketNums
。 -
如果
ticketNums > 0
,線程會休眠1秒(模擬耗時操作),然后打印并減少ticketNums
的值。 -
如果
ticketNums <= 0
,線程退出循環,任務結束。
代碼執行流程
-
啟動線程:
-
在
main
方法中,創建了一個TestLock2
對象,并啟動三個線程。 -
這三個線程會并發執行
TestLock2
的run
方法。
-
-
線程競爭鎖:
-
每個線程在執行
run
方法時,會先調用lock.lock()
嘗試獲取鎖。 -
只有一個線程能成功獲取鎖,其他線程會被阻塞,直到鎖被釋放。
-
-
訪問共享資源:
-
獲取鎖的線程會檢查
ticketNums
的值。 -
如果
ticketNums > 0
,線程會休眠1秒,然后打印ticketNums
的值并將其減1。 -
如果
ticketNums <= 0
,線程會退出循環。
-
-
釋放鎖:
-
線程在完成對共享資源的操作后,會調用
lock.unlock()
釋放鎖。 -
其他被阻塞的線程會競爭獲取鎖,繼續執行。
-
-
任務結束:
-
當
ticketNums
的值減少到0時,所有線程都會退出循環,任務結束。
-
關鍵點
-
ReentrantLock的作用:
-
確保多個線程對共享資源
ticketNums
的訪問是互斥的,避免數據競爭。 -
相比于
synchronized
,ReentrantLock
提供了更靈活的鎖控制,例如可中斷鎖、超時鎖等。
-
-
try-finally的作用:
-
在
try
塊中加鎖,在finally
塊中解鎖,確保鎖一定會被釋放,避免死鎖。
-
-
線程安全:
-
通過
ReentrantLock
實現了對共享資源的線程安全訪問。
-
改進建議
-
鎖的粒度:
-
當前代碼中,鎖的粒度較大(整個
while
循環都在鎖內),可能會影響并發性能。可以根據實際需求調整鎖的粒度。
-
-
公平鎖:
-
ReentrantLock
默認是非公平鎖,可以通過構造函數new ReentrantLock(true)
創建公平鎖,確保線程按順序獲取鎖。
-
-
鎖的可中斷性:
-
ReentrantLock
支持可中斷的鎖獲取(lockInterruptibly()
),可以在線程等待鎖時響應中斷。
-
改進后代碼:
package com.lock;import java.util.concurrent.locks.ReentrantLock;/*
改進后的TestLock示例:
1. 縮小鎖的粒度,只對共享資源的訪問和修改加鎖。
2. 使用公平鎖,確保線程按順序獲取鎖。
3. 優化代碼結構,提高可讀性和可維護性。
*/public class TestLock {public static void main(String[] args) {TestLock2 testLock2 = new TestLock2();// 啟動多個線程new Thread(testLock2, "線程1").start();new Thread(testLock2, "線程2").start();new Thread(testLock2, "線程3").start();}
}class TestLock2 implements Runnable {private int ticketNums = 10; // 共享資源,表示剩余的票數// 定義公平鎖private final ReentrantLock lock = new ReentrantLock(true);@Overridepublic void run() {while (true) {// 嘗試獲取鎖lock.lock();try {if (ticketNums > 0) {// 模擬耗時操作try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 打印當前線程名和剩余的票數System.out.println(Thread.currentThread().getName() + " 售出票號:" + ticketNums--);} else {// 票已售完,退出循環break;}} finally {// 釋放鎖lock.unlock();}// 模擬線程切換,增加并發性try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}
}
synchronized 與Lock的對比
1、lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖)synchronized是隱式鎖,出了作用域自動釋放
2、Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖;
3、使用Lock鎖,JVM將花費較少的時間來調度線程(性能更好,并且具有更好的擴展性(提供更多的子類))
4、優先使用順序:
??? Lock > 同步代碼塊(已經進入了方法體,分配了相應資源)> 同步方法(在方法體之外)