多線程與死鎖

Java多線程開發中,爲了避免多個線程對同一份數據的操作,我們需要對我們的線程做加鎖的操作,只要加鎖,就必然存在鎖競爭的問題,如果鎖競爭的問題處理不當就會出現死鎖問題。死鎖會讓程序一直卡住,程序不再往下執行。我們只能通過中止並重啓的方式來讓程序重新執行。

這是我們非常不願意看到的一種現象,我們要儘可能避免死鎖的情況發生!

造成死鎖的原因可以概括成三句話:

  • 當前線程擁有其他線程需要的資源
  • 當前線程等待其他線程已擁有的資源
  • 都不放棄自己擁有的資源

動態順序鎖死鎖

動態順序鎖導致死鎖是最常見的死鎖,例如我們在2個線程對2個賬戶進行轉賬操作,我們需要先鎖定匯賬賬戶減錢,然後再鎖定入款賬戶加錢,一般來講這個邏輯順序是沒有問題的,但是如果這2個線程併發同時處理,就會產生死鎖。

public class DeadLock {


    public static void main(String[] args) {
        Account accountA = new Account(1L,"A",10000L);
        Account accountB = new Account(1L,"B",10000L);

        Thread a = new Thread(()->{
            try {
                transferMoney(accountA,accountB,100L);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        Thread b = new Thread(()->{
            try {
                transferMoney(accountB,accountA,100L);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        a.start();
        b.start();

        while (true){
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }

    }

    // 轉賬
    public static void transferMoney(Account fromAccount,
                                     Account toAccount,
                                     Long amount) throws Exception {
        // 鎖定匯賬賬戶
        synchronized (fromAccount) {
            System.out.println(Thread.currentThread().getName()+"獲取賬戶"+fromAccount.getName()+"鎖");
            // 鎖定來賬賬戶
            synchronized (toAccount) {
                System.out.println(Thread.currentThread().getName()+"獲取賬戶"+toAccount.getName()+"鎖");
                // 判餘額是否大於0
                if (fromAccount.getAmount().compareTo(amount) < 0) {
                    throw new Exception("No enough money");
                } else {
                    // 匯賬賬戶減錢
                    fromAccount.debit(amount);
                    // 來賬賬戶增錢
                    toAccount.credit(amount);

                    System.out.println(Thread.currentThread().getName()+"完成轉賬");

                }
            }
        }
    }
}

@Data
class Account{

    private Long id;

    private String name;

    private Long amount;

    public Account(){}

    public Account(long id,String name, long amount){
        this.id = id;
        this.name = name;
        this.amount = amount;
    }


    public void debit(Long amount) {
        this.amount -= amount;
    }

    public void credit(Long amount) {
        this.amount += amount;
    }

}

程序執行結果如下:

Thread-1獲取賬戶B鎖
Thread-0獲取賬戶A鎖

我們會發現我們的轉賬一直無法完成交易,程序一直卡住,程序不再往下執行。即發生了死鎖。

Jconsole 查看死鎖

Jconsole是JDK自帶的圖形化界面工具,使用JDK給我們的的工具JConsole,我們可以直接查看Java進程中出現的死鎖。
控制檯輸入jconsole啓動圖形化界面工具

jconsole

在這裏插入圖片描述
選擇連接我們的本地調試進程
在這裏插入圖片描述
選擇線程欄,我們會看到我們的正在運存的測試代碼的3個線程
在這裏插入圖片描述
選擇我們的線程,檢測死鎖
在這裏插入圖片描述
在這裏插入圖片描述
很明顯看出,Thread-0需要的資源被Thread-1佔用,Thread-0被阻塞。

固定鎖順序避免死鎖

針對動態鎖順序導致的死鎖,我們可以通過固定加鎖的順序來解決

public class FixedOrderDeadLock {

    public static void main(String[] args) {
        Account accountA = new Account(1L,"A",10000L);
        Account accountB = new Account(1L,"B",10000L);

        Thread a = new Thread(()->{
            try {
                transferMoney(accountA,accountB,100L);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        Thread b = new Thread(()->{
            try {
                transferMoney(accountB,accountA,100L);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        a.start();
        b.start();

        while (true){
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

    public static void transferMoney(final Account fromAcct,
                              final Account toAcct,
                              final Long amount)
            throws Exception {
        class Helper {
            public void transfer() throws Exception {
                if (fromAcct.getAmount().compareTo(amount) < 0)
                    throw new Exception("No enough money");
                else {
                    fromAcct.debit(amount);
                    toAcct.credit(amount);

                    System.out.println(Thread.currentThread().getName()+"完成轉賬");
                }
            }
        }
        // 得到鎖的hash值
        int fromHash = System.identityHashCode(fromAcct);
        int toHash = System.identityHashCode(toAcct);

        // 根據hash值來上鎖  不管是匯款賬戶還是入款賬戶,總是Hash值小的先鎖,則對象鎖的順序是固定的
        if (fromHash < toHash) {
            synchronized (fromAcct) {
                System.out.println(Thread.currentThread().getName()+"獲取賬戶"+fromAcct.getName()+"鎖");
                synchronized (toAcct) {
                    System.out.println(Thread.currentThread().getName()+"獲取賬戶"+toAcct.getName()+"鎖");
                    new Helper().transfer();
                }
            }

        } else if (fromHash > toHash) {// 根據hash值來上鎖
            synchronized (toAcct) {
                System.out.println(Thread.currentThread().getName()+"獲取賬戶"+toAcct.getName()+"鎖");
                synchronized (fromAcct) {
                    System.out.println(Thread.currentThread().getName()+"獲取賬戶"+fromAcct.getName()+"鎖");
                    new Helper().transfer();
                }
            }
        } else {//如果是同對象,由於 synchronized 已經支持可重入鎖,所以併發的同賬戶互相轉賬不會產生死鎖,鎖順序不產生影響
            synchronized (fromAcct) {
                synchronized (toAcct) {
                    new Helper().transfer();
                }
            }
        }
    }
}

程序執行結果

Thread-0獲取賬戶A鎖
Thread-0獲取賬戶B鎖
Thread-0完成轉賬
Thread-1獲取賬戶A鎖
Thread-1獲取賬戶B鎖
Thread-1完成轉賬

如結果所示,並不會產生死鎖。

協作鎖之間發生死鎖

除了動態順序鎖發生死鎖的情況,還存在一些死鎖的情況,但是不是順序鎖那麼簡單可以被發現。因爲有可能並不是在同一個方法中顯示請求兩個鎖,而是嵌套另一個方法去獲取第二個鎖。這就是隱式獲取兩個鎖(對象之間協作)。

例如,ProductProducer生產者會不斷的生產商品並且向倉庫註冊商品,ProductDepository商品倉庫會不斷的處理註冊到倉庫的商品加工,生成編號。

public class CooperatingDeadlock {

    public static void main(String[] args) {
        ProductProducer productProducer = new ProductProducer();
        ProductDepository productDepository = new ProductDepository();
        productProducer.setProductDepository(productDepository);

        productProducer.start();
        productDepository.start();

        while (true){
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

}

class ProductProducer extends Thread{

    public void setProductDepository(ProductDepository productDepository) {
        this.productDepository = productDepository;
    }

    private ProductDepository productDepository;

    @Override
    public void run(){
        produce();
    }

    // produce()需要ProductProducer對象鎖
    private synchronized void produce(){
        do {
            Product p = new Product(this);
            System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,生產商品,並向倉庫註冊");
            this.productDepository.addAvailable(p);

            try{
                Thread.sleep(100);
            }catch (Exception e){
                e.printStackTrace();
            }
        }while (true);
    }

    public synchronized void handle(Product product) {
        System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,加工商品,生成編號");
        product.setNo(System.currentTimeMillis());
    }
}

class ProductDepository extends Thread{

    public ProductDepository(){
        availableProductList = new ArrayList<>();
    }

    private ArrayList<Product> availableProductList;

    @Override
    public void run(){
        handleAvailable();
    }

    public synchronized void addAvailable(Product product) {
        System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,註冊到倉庫");
        availableProductList.add(product);
    }

    // handleAvailable()需要ProductDepository對象鎖
    public synchronized void handleAvailable() {
        System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,將註冊的商品進行加工");
        do{
            for (Product t : availableProductList)
                // 調用handle()需要ProductProducer對象鎖
                t.handle();

            availableProductList.clear();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }while (true);
    }

}

class Product{
    private  ProductProducer productProducer;

    public Product(){}

    public void setNo(Long no) {
        this.no = no;
    }

    private Long no;

    public Product(ProductProducer productProducer){
        this.productProducer = productProducer;
    }

    public void handle(){
        this.productProducer.handle(this);
    }
}

此時我們執行程序:

Thread-0獲取com.pubutech.multithread.example.deadlock.ProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-1獲取com.pubutech.multithread.example.deadlock.ProductDepository對象鎖,將註冊的商品進行加工

很顯然,我們的程序將陷入死鎖無法繼續執行下去。

開放調用避免死鎖

在協作對象之間發生死鎖的例子中,主要是因爲在調用某個方法時就需要持有鎖,並且在方法內部也調用了其他帶鎖的方法!

如果在調用某個方法時不再持有鎖,而改爲同步代碼塊僅用於保護那些涉及共享狀態的操作!那麼這種調用被稱爲開放調用!

我們可以這樣來改造:

public class OpenMethodCooperationDeadLock {
    public static void main(String[] args) {
        OpenMethodProductProducer productProducer = new OpenMethodProductProducer();
        OpenMethodProductDepository productDepository = new OpenMethodProductDepository();
        productProducer.setProductDepository(productDepository);

        productProducer.start();
        productDepository.start();

        while (true){
            try {
                Thread.sleep(1000);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }

}

class OpenMethodProductProducer extends Thread{

    public void setProductDepository(OpenMethodProductDepository productDepository) {
        this.productDepository = productDepository;
    }

    private OpenMethodProductDepository productDepository;

    @Override
    public void run(){
        produce();
    }

    private void produce(){
        do {
            OpenMethodProduct p = null;
            //需要ProductProducer對象鎖,但是縮小鎖的範圍,不會同時去獲取兩個鎖
            synchronized (this){
                System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,生產商品,並向倉庫註冊");
                p = new OpenMethodProduct(this);
            }

            if (null != p){
                System.out.println(Thread.currentThread().getName()+"向倉庫註冊");
                this.productDepository.addAvailable(p);
            }

            try{
                Thread.sleep(100);
            }catch (Exception e){
                e.printStackTrace();
            }
        }while (true);
    }

    public synchronized void handle(OpenMethodProduct product) {
        System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,加工商品,生成編號");
        product.setNo(System.currentTimeMillis());
    }
}

class OpenMethodProductDepository extends Thread{

    public OpenMethodProductDepository(){
        availableProductList = new ArrayList<>();
    }

    private ArrayList<OpenMethodProduct> availableProductList;

    @Override
    public void run(){
        handleAvailable();
    }

    public synchronized void addAvailable(OpenMethodProduct product) {
        System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,註冊到倉庫");
        availableProductList.add(product);
    }

    public void handleAvailable() {
        do{
            ArrayList<OpenMethodProduct> availableProductListCopy;
            //需要ProductDepository對象鎖,但是縮小鎖的範圍,不會同時去獲取兩個鎖
            synchronized (this){
                System.out.println(Thread.currentThread().getName()+"獲取"+getClass().getName()+"對象鎖,準備將註冊的商品進行加工");
                availableProductListCopy = new ArrayList<>(availableProductList);
                availableProductList.clear();
            }

            for (OpenMethodProduct t : availableProductListCopy)
                // 調用handle()需要ProductProducer對象鎖
                t.handle();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }while (true);
    }

}

class OpenMethodProduct{
    private  OpenMethodProductProducer productProducer;

    public OpenMethodProduct(){}

    public void setNo(Long no) {
        this.no = no;
    }

    private Long no;

    public OpenMethodProduct(OpenMethodProductProducer productProducer){
        this.productProducer = productProducer;
    }

    public void handle(){
        this.productProducer.handle(this);
    }
}

程序執行結果如下,將會不斷的協作下去直至我們終端程序而不會產生死鎖

Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,準備將註冊的商品進行加工
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫
Thread-1獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,加工商品,生成編號
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductProducer對象鎖,生產商品,並向倉庫註冊
Thread-0向倉庫註冊
Thread-0獲取com.pubutech.multithread.example.deadlock.OpenMethodProductDepository對象鎖,註冊到倉庫

RetreenLock鎖超時解決死鎖

synchronized 關鍵字的鎖是由Java虛擬機實現的,它無法顯示獲取鎖超時,但是Java5以後Java RetreenLock提供了tryLock()方法來實現獲取鎖超時,我們在獲取鎖時可以使用tryLock()方法。當等待超過時限的時候,tryLock()不會一直等待,而是返回錯誤信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章