Java由於資源競爭實現死鎖
阿新 • • 發佈:2021-10-10
在多執行緒執行中,Java對於一個物件中的共享資源提供了可重入鎖機制,允許在使用共享資源或程式碼時加鎖、使用完畢解鎖為程式碼段賦予原子性。
下面通過產生死鎖的例子,來分析這個機制:
public class MethodBlock { private ReentrantLock lock1 = new ReentrantLock(); private ReentrantLock lock2 = new ReentrantLock(); public void method1(String name) throws InterruptedException { lock1.lock(); System.out.println("這裡是方法1 " + name); Thread.sleep(500); System.out.println("方法1結束呼叫方法2 " + name); method2(name); lock1.unlock(); } public void method2(String name) throws InterruptedException { lock2.lock(); System.out.println("這裡是方法2 " + name); Thread.sleep(500); method1(name); System.out.println("方法2結束呼叫方法1 " + name); lock2.unlock(); } } public static void main(String[] args) throws InterruptedException { MethodBlock obj1 = new MethodBlock(); // MethodBlock obj2 = new MethodBlock(); //測試死鎖 Runnable r1 = () -> { try { obj1.method1("t1"); } catch (InterruptedException e) { e.printStackTrace(); } }; Runnable r2 = () -> { try { obj1.method2("t2"); } catch (InterruptedException e) { e.printStackTrace(); } }; Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); }
執行結果:
發生死鎖
執行過程如下:
1.執行緒t1啟動,呼叫start()方法進入可執行狀態並執行
2.t1執行method1方法並得到可重入鎖lock1,執行到sleep(500)時進入阻塞態持續500ms,此時cpu空閒。
3.執行緒t2啟動,執行,還行method2方法並得到可重入鎖lock2,同樣執行到sleep(500)時進入阻塞態持續500ms。
4.t1阻塞結束重新執行,希望呼叫method2方法,但此時method2方法被執行緒t2加上了lock2鎖,因此t1阻塞。
5.t2阻塞結束重新執行,希望呼叫method1方法,但此時method1方法被執行緒t1加上了lock1鎖,t2也進入了阻塞態。
此時兩個執行緒互相等待,且佔有資源不可剝奪。出現死鎖。
因此可以看出,鎖是對於一個物件的共享資源來說的,多個執行緒訪問這些資源時,會等待有鎖的程序釋放鎖才可以訪問。
什麼是空重入鎖?
持有該鎖的執行緒可以重複地獲得已經持有的鎖。鎖保持一個持有技術來跟蹤對lock方法的巢狀呼叫。執行緒在每一次呼叫lock都要呼叫unlock來釋放鎖。由於這一特性,被一個鎖保護的程式碼可繼續訪問另一個使用相同鎖的方法。
比如,將MethodBlock類做如下修改:
private ReentrantLock lock1 = new ReentrantLock(); //所有方法全部使用同一個鎖 // private ReentrantLock lock2 = new ReentrantLock(); public void method1(String name) throws InterruptedException { lock1.lock(); System.out.println("這裡是方法1 " + name); Thread.sleep(2000); System.out.println("方法1結束呼叫方法2 " + name); method2(name); lock1.unlock(); } public void method2(String name) throws InterruptedException { lock1.lock(); System.out.println("這裡是方法2 " + name); Thread.sleep(500); method1(name); System.out.println("方法2結束呼叫方法1 " + name); lock1.unlock(); }
重新執行:
可以看到,一直都是執行緒t1在執行,這是因為只有一個鎖lock1,t1執行方法method1(..)獲得鎖之後,t2想要訪問程式碼只能阻塞等待t1釋放鎖lock1。然而,t1在獲得鎖之後可以繼續呼叫使用相同鎖的method2(..)方法。
PS:由於run中產生未捕獲的異常也會導致執行緒結束,若該執行緒還持有鎖,則這個鎖將永遠不會釋放,因此最好將程式碼寫成如下形式:
lockObj.lock()
try
{
do something..
}
finally
{
lockObj.unlock(); //鎖最後一定會被釋放
}