1. 程式人生 > 其它 >Java由於資源競爭實現死鎖

Java由於資源競爭實現死鎖

在多執行緒執行中,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(); //鎖最後一定會被釋放
}