Java的死鎖及解決思路(延伸: 活鎖,飢餓,無鎖)


死鎖

A線程持有 鎖1,接下來要獲取鎖2;與此同時,B線程持有鎖2,要獲取鎖1。兩個線程都在等對方釋放自己需要的鎖,這時兩方會永遠等待下去,就形成了死鎖。

 

死鎖的四個必要條件:

1.互斥:資源(鎖)同時只能被一個線程占用。

2.占有且等待:線程已經占用資源A,同時等待資源B時,不釋放資源A。

3.不可搶占:其他線程不能強行獲取當前線程占有的資源

4.循環等待:存在一個等待鏈,即T1等待T2占有的資源,T2等待T3占有的資源,T3等待T1占有的資源。

 

如果要解決死鎖,則需要破壞任意一死鎖的必要條件。

一.破壞占有且等待條件

 解決方法:只要限定所有資源鎖同時獲取,同時釋放。就可以預防掉死鎖。其實就是破壞掉占有且等待條件。

下面以銀行轉賬的代碼為例子

/**
 * 鎖分配類(單例)
 *
 * @author Liumz
 * @since 2019-04-02  15:57:32
 */
@Component
public class Allocator {
    /**
     * 已被申請鎖的集合
     */
    private List<Object> locks = new ArrayList<>();
    /**
     * 申請鎖
     *
     * @param timeOut   過期時間(秒)
     * @param lockArray 要申請的鎖集合
     */
    public synchronized void apply(int timeOut, Object... lockArray) throws Exception {
        //如果當前不滿足申請條件,則等待。直到資源被釋放時進行notifyAll喚醒當前線程
        //while(condition){wait()} 是一個標准范式,線程如果被喚醒,執行時會再判斷一次條件。
        LocalDateTime dtStart = LocalDateTime.now();
        while (Arrays.stream(lockArray).anyMatch(i -> this.locks.contains(i))) {
            //時間間隔達到5秒還未獲取到條件鎖,則拋出異常
            if (Duration.between(dtStart, LocalDateTime.now()).toMillis() > timeOut * 1000) {
                throw new Exception("放棄任務");
            }
            //釋放當前對象鎖,並等待
            try {
                this.wait(1000);
            } catch (InterruptedException ignore) {
            }
        }
        //如果已被申請鎖的集合中沒有要申請的鎖,表示申請成功,並把申請成功的鎖加入集合
        this.locks.addAll(Arrays.asList(lockArray));
    }
    /**
     * 釋放鎖
     *
     * @param lockArray 要釋放的鎖集合
     */
    public synchronized void free(Object... lockArray) {
        for (Object o : lockArray) {
            this.locks.remove(o);
        }
        //喚醒所有wait的線程,正在等待locks被移除釋放的線程。盡量使用notifyAll,避免有的線程會不被喚醒,一直wait
        this.notifyAll();
    }
}
View Code
/**
 * 銀行賬戶類
 *
 * @author Liumz
 * @since 2019-04-02  15:36:15
 */
@Component
@Scope("prototype")
public class BankAccount {
    /**
     * 余額
     */
    private int balance;
    /**
     * 鎖分配對象
     */
    @Autowired
    private Allocator allocator;
    /**
     * 轉賬
     *
     * @param target 目標賬戶
     * @param amount 轉賬金額
     */
    public void transfer(BankAccount target, int amount) {
        //申請鎖,如果申請不到會一直等待。除非超時時拋出異常
        try {
            this.allocator.apply(5, target, this);
        } catch (Exception e) {
            return;
        }
        try {
            //同時鎖定目標和當前賬戶,避免出現死鎖情況.並且進行賬戶余額加減操作
            synchronized (this) {
                synchronized (target) {
                    this.balance -= amount;
                    target.balance += amount;
                }
            }
        } finally {
            //同時釋放加的兩個鎖
            this.allocator.free(target, this);
        }
    }
}
View Code

 

.破壞循環等待條件

 解決方法:對鎖進行排序,每次申請鎖需要按從小到大順序申請。這樣就不存在循環等待了

/**
 * 銀行賬戶類
 *
 * @author Liumz
 * @since 2019-04-02  15:36:15
 */
@Component
@Scope("prototype")
public class BankAccount {
    /**
     * 余額
     */
    private int balance;
    /**
     * 序號id
     */
    private int id;

    /**
     * 轉賬
     *
     * @param target 目標賬戶
     * @param amount 轉賬金額
     */
    public void transfer(BankAccount target, int amount) {
        //對賬戶序號排序
        BankAccount firstLock = target;
        BankAccount secondLock = this;
        if (firstLock.id > secondLock.id) {
            firstLock = this;
            secondLock = target;
        }
        //先鎖定序號小的賬戶,再鎖定序號大的賬戶
        synchronized (firstLock){
            synchronized (secondLock){
                this.balance -= amount;
                target.balance += amount;
            }
        }
    }
}
View Code

 

.破壞不可搶占條件

 解決方法: 使用 Lock 和UnLock,在finally里執行unlock,主動釋放資源。此時別人就可以搶占了。

 

活鎖:

多個線程獲取不到資源,就放開已獲得的資源,重試。相當於系統空轉,一直在做無用功。

例如,行人走路相向而行,互相謙讓,一直重復謙讓的過程。

如以下一直死循環:

start: p1 lock A p2 lock B p1 lock B failed p2 lock A failed p1 release A p2 release B goto start

解決方法:引入一些隨機性,比如暫停隨機時間重試。

 

飢餓:

1:優先級高的線程總是搶占到資源,而優先級低的線程可能會一直等待,從而無法獲取資源無法執行;

2:一個線程一直不釋放資源,別的線程也會出現飢餓的情況。

3:wait()等待情況下的線程一直都不被notify,而其他的線程總是能被喚醒

解決方法:引入公平鎖

 

無鎖:

CAS(campare and swap):內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,將內存值修改為B並返回true,否則什么都不做並返回false。CAS是原子操作,只有一條cpu指令

無鎖即不對資源鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。一個修改操作在一個循環內進行,線程會不斷的嘗試修改共享資源,如果沒有沖突(CAS判斷)就修改成功並退出否則就會繼續下一次循環嘗試。

如jdk的基於CAS實現的原子操作類,就是對無鎖的實現。 還有無鎖隊列,也是循環線程對變量進行CAS操作的數據結構。

CAS的缺點:

1.ABA問題:V值為A,T1,T2從內存取出V值為A.。然后T2 CAS修改變量V為B , 接着T2 又CAS修改變量V為A。這時T1 CAS 變量V時發現內存中V還是A ,CAS操作成功。

2.循環消耗大

3.只能保證一個共享變量的原子操作

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM