每日一技|活鎖,也許你需要了解一下


前兩天看極客時間 Java 並發課程的時候,刷到一個概念:活鎖。死鎖,倒是不陌生,活鎖卻是第一次聽到。

在介紹活鎖之前,我們先來復習一下死鎖,下面的例子模擬一個轉賬業務,多線程環境,為了賬戶金額安全,對賬戶進行了加鎖。

public class Account {
    public Account(int balance, String card) {
        this.balance = balance;
        this.card = card;
    }
    private int balance;
    private String card;
    public void addMoney(int amount) {
        balance += amount;
    }
  	// 省略 get set 方法
}
public class AccountDeadLock {
    public static void transfer(Account from, Account to, int amount) throws InterruptedException {
        // 模擬正常的前置業務
        TimeUnit.SECONDS.sleep(1);
        synchronized (from) {
            System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());
            synchronized (to) {
                System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());
                // 轉出賬號扣錢
                from.addMoney(-amount);
                // 轉入賬號加錢
                to.addMoney(amount);
            }
        }
        System.out.println("transfer success");
    }

    public static void main(String[] args) {
        Account from = new Account(100, "6000001");
        Account to = new Account(100, "6000002");

        ExecutorService threadPool = Executors.newFixedThreadPool(2);

        // 線程 1
        threadPool.execute(() -> {
            try {
                transfer(from, to, 50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 線程 2
        threadPool.execute(() -> {
            try {
                transfer(to, from, 30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });


    }
}

上述例子中,當兩個線程進入轉賬方法,線程 1 獲取賬戶 6000001 這把鎖,線程 2 鎖住了賬戶 6000002 鎖。

接着當線程 1 想去獲取 6000002 的鎖時,由於這把鎖已經被線程 2 持有,線程 1 將會陷入阻塞,線程狀態轉為 BLOCKED。同理,線程 2 也是同樣狀態。

pool-1-thread-1 lock from account 6000001
pool-1-thread-2 lock from account 6000002

通過日志,可以看到兩個線程開始轉賬方法之后,就陷入等待。

synchronized 獲取不到鎖就會阻塞,進行等待。既然這樣,我們可以使用 ReentrantLock#tryLock(long timeout, TimeUnit unit) 進行改造。tryLock 若能獲取鎖,將會返回 true,若不能獲取鎖將會進行等待,直到滿足下列條件:

  • 超時時間內獲取到了鎖,返回 true
  • 超時時間內未獲取到鎖,返回 false
  • 中斷,拋出異常

改造后代碼如下:

public class Account {
    public Account(int balance, String card) {
        this.balance = balance;
        this.card = card;
    }
    private int balance;
    private String card;
    public void addMoney(int amount) {
        balance += amount;
    }
  	// 省略 get set 方法
}
public class AccountLiveLock {

    public static void transfer(Account from, Account to, int amount) throws InterruptedException {
        // 模擬正常的前置業務
        TimeUnit.SECONDS.sleep(1);
        // 保證轉賬一定成功
        while (true) {
            if (from.lock.tryLock(1, TimeUnit.SECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());
                    if (to.lock.tryLock(1, TimeUnit.SECONDS)) {
                        try {
                            System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());
                            // 轉出賬號扣錢
                            from.addMoney(-amount);
                            // 轉入賬號加錢
                            to.addMoney(amount);
                            break;
                        } finally {
                            to.lock.unlock();
                        }

                    }
                } finally {
                    from.lock.unlock();
                }
            }
        }
        System.out.println("transfer success");

    }

    public static void main(String[] args) {
        Account from = new Account(100, "A");
        Account to = new Account(100, "B");

        ExecutorService threadPool = Executors.newFixedThreadPool(2);

        // 線程 1
        threadPool.execute(() -> {
            try {
                transfer(from, to, 50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 線程 2
        threadPool.execute(() -> {
            try {
                transfer(to, from, 30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
}

上面代碼使用了 while(true),獲取鎖失敗,不斷重試,直到成功。運行這個方法,運氣好點,一把就能成功,運氣不好,就會如下:

pool-1-thread-1 lock from account 6000001
pool-1-thread-2 lock from account 6000002
pool-1-thread-2 lock from account 6000002
pool-1-thread-1 lock from account 6000001
pool-1-thread-1 lock from account 6000001
pool-1-thread-2 lock from account 6000002

transfer 方法一直在運行,但是最終卻得不到成功結果,這就是個活鎖的例子。

死鎖將會造成線程阻塞,程序看起來就像陷入假死一樣。就像路上碰到人,你盯着我,我盯着你,互相等待對方讓道,最后誰也過不去。

image-20200218182523993

而活鎖不一樣,線程不斷重復同樣的操作,但也卻執行不成功。還拿上面舉例,這次你往左一步,他往右邊一步,巧了,又碰上。然后不斷循環,最會還是誰也過不去。

圖片來源:知乎

分析死鎖這個例子,兩個線程獲取的鎖的順序不一致,最后導致互相需要對方手中的鎖。如果兩個線程加鎖順序一致,所需條件就會一樣,勢必就不會產生死鎖了。

我們以卡號大小為順序,每次都給卡號比較大的賬戶先加鎖,這樣就可以解決死鎖問題,代碼修改如下:

// 其他代碼不變    
public static void transfer(Account from, Account to, int amount) throws InterruptedException {
        // 模擬正常的前置業務
        TimeUnit.SECONDS.sleep(1);
        Account maxAccount=from;
        Account minAccount=to;
        if(Long.parseLong(from.getCard())<Long.parseLong(to.getCard())){
            maxAccount=to;
            minAccount=from;
        }

        synchronized (maxAccount) {
            System.out.println(Thread.currentThread().getName() + " lock  account " + maxAccount.getCard());
            synchronized (minAccount) {
                System.out.println(Thread.currentThread().getName() + " lock  account " + minAccount.getCard());
                // 轉出賬號扣錢
                from.addMoney(-amount);
                // 轉入賬號加錢
                to.addMoney(amount);
            }
        }
        System.out.println("transfer success");
    }

對於活鎖的例子,存在兩個問題:

一是鎖的鎖超時時間都一樣,導致兩個線程幾乎同時釋放鎖,重試時又同時上鎖,然后陷入死循環。解決這個問題,我們可以使超時時間不一樣,引入一定的隨機性。

二是這里使用 while(true),實際開發中萬萬不能這么玩。這種情況我們需要設置最大的重試次數。

畫外音:如果重試這么多次,一直不成功,但是業務卻想成功。現在不成功,不要傻着一直試,先放下,記錄下來,待會再重試補償唄~

活鎖的代碼可以改成如下:

		public static final int MAX_TIME = 5;
    public static void transfer(Account from, Account to, int amount) throws InterruptedException {
        // 模擬正常的前置業務
        TimeUnit.SECONDS.sleep(1);
        // 保證轉賬一定成功
        Random random = new Random();
        int retryTimes = 0;
        boolean flag=false;
        while (retryTimes++ < MAX_TIME) {
            // 等待時間隨機
            if (from.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) {
                try {
                    System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard());
                    if (to.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard());
                            // 轉出賬號扣錢
                            from.addMoney(-amount);
                            // 轉入賬號加錢
                            to.addMoney(amount);
                            flag=true;
                            break;
                        } finally {
                            to.lock.unlock();
                        }

                    }
                } finally {
                    from.lock.unlock();
                }
            }
        }
        if(flag){
            System.out.println("transfer success"); 
        }else {
            System.out.println("transfer failed");
        }
    }

總結

死鎖是日常開發中比較容易碰到的情況,我們需要小心,注意加鎖的順序。活鎖,碰到情況可能不常見,本質上我們只需要注意設置最大的重試次數,就不會永遠陷入一直重試中。

參考鏈接

http://c.biancheng.net/view/4786.html

https://www.javazhiyin.com/43117.html

歡迎關注我的公眾號:程序通事,獲得日常干貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn


免責聲明!

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



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