1. 程式人生 > >每日一技|活鎖,也許你需要了解一下

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

前兩天看極客時間 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 方法一直在執行,但是最終卻得不到成功結果,這就是個活鎖的例子。

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

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

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

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

// 其他程式碼不變    
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

相關推薦

每日|也許需要一下

前兩天看極客時間 Java 併發課程的時候,刷到一個概念:活鎖。死鎖,倒是不陌生,活鎖卻是第一次聽到。 在介紹活鎖之前,我們先來複習一下死鎖,下面的例子模擬一個轉賬業務,多執行緒環境,為了賬戶金額安全,對賬戶進行了加鎖。 public class Account { public Account(

關於深度學習這些知識點需要一下

深度學習概述o受限玻爾茲曼機和深度信念網路oDropouto處理不平衡的技巧oSMOTE:合成少數過取樣技術o神經網路中對成本敏感的學習深度學習概述在2006年之前,訓練深度監督前饋神經網路總是失敗的,其主要原因都是導致過度擬合,即訓練錯誤減少,而驗證錯誤增加。深度網路通常意

關於驗證碼需要這些

使用場景 strong 兼容 logs nbsp 簽名 業務 swe yun (一)什麽是驗證碼業務id? captchaId, 驗證碼唯一標識,公開可見,用於區分不同的驗證碼使用場景,如登錄、投票、發帖等。可在易盾管理中心驗證碼業務ID管理處自行創建。2017年6月1

微服務架構盛行的時代需要點 Spring Boot

措辭 理由 直接 響應 con 可伸縮 角度 徹底 構建 隨著互聯網的高速發展,龐大的用戶群體和快速的需求變化已經成為了傳統架構的痛點。 在這種情況下,如何從系統架構的角度出發,構建出靈活、易擴展的系統來快速響應需求的變化,同時,隨著用戶量的增加,如何保證系統的穩定性、高可

【廣州服務器回收】服務器維護過程中需要的5個小常識

windows ron 就是 圖片 渲染 天都 驚人的 領域 其他人 大多數人認為,服務器僅僅是升級後的臺式機。但任何在數據中心工作過的人都知道,它們的差別挺大的。 盡管web服務器每天都要承擔數百萬訪問者的負載,但對於普通用戶來說,它們仍然神秘莫測。以下是關於服務器你可能

身為前端開發工程師需要的搜尋引擎優化SEO.

網站url網站建立具有良好描述性、規範、簡單的url,有利於使用者更方便的記憶和判斷網頁的內容,也有利於搜尋引擎更有效的抓取您的網站。網站設計之初,就應該有合理的url規劃。 處理方式: 1.在系統中只使用正常形式url,不讓使用者接觸到非正常形式的url。 2.不把session id、統計程式碼等不必

身為前端開發工程師需要的搜索引擎優化SEO.

ide 收藏 htm des 頻道 最適 主題 開發工程師 用戶 網站url網站創建具有良好描述性、規範、簡單的url,有利於用戶更方便的記憶和判斷網頁的內容,也有利於搜索引擎更有效的抓取您的網站。網站設計之初,就應該有合理的url規劃。 處理方式: 1.在系統中只使用正

數字貨幣交易需要這幾所海外交易所!

現在數字貨幣非常的火熱,而數字貨幣交易所作為數字貨幣交易的地方也吸引了許多人的關注,不過大部分人只知道兩三個常用的交易所,下面小編就講一下世界上最大的八個加密貨幣交易所。 1.GDAX 傳說中的G網,GDAX是Coinbase旗下的全球數字資產交易所,是美國第一家持有正規牌照的比特幣

Spring Boot 2.1.0 已釋出7 個重大更新需要

Spring Boot 2.1.0 在 10 月底就釋出了,我們來看下 Spring Boot 2.1.0 都更新了什麼,每一個 Java 技術人都值得關注。 棧長其實早就看到了更新了,現在才有時間來更新下。 1、第三方類庫升級 Hibernate 5.3 Micrometer 1.1 Reacto

Spring Boot 2.1.0 已發布7 個重大更新需要

pool for rep ctu err 自動配置 表示 req spring Spring Boot 2.1.0 在 10 月底就發布了,我們來看下 Spring Boot 2.1.0 都更新了什麽,每一個 Java 技術人都值得關註。 棧長其實早就看到了更新了,現在才有

進入職場之前需要這個致命弱點

這裡其實隱含了一個資訊:你們已經從學生角色慢慢進入職場,或即將成為一個職場新人。 首先要明確思維本身無對錯高下,重要的是:不同的場合或不同的身份,應該用不同的思維去解決問題。正因如此,你才會被要求具備職場思維。 學生思維和職場思維最大的區別就是——職場從現實利益出發,看重最後結果

關於深度學習優化器 optimizer 的選擇需要這些

在很多機器學習和深度學習的應用中,我們發現用的最多的優化器是 Adam,為什麼呢? 在 keras 中也有 SGD,RMSprop,Adagrad,Adadelta,Adam 等: https://keras.io/optimizers/ 我們可以發

資料分析師:大資料建模需要九大形式

“沒有免費的午餐”理論已經應用於機器學習領域,無偏的狀態好於(如一個具體的演算法)任何其他可能的問題(資料集)出現的平均狀態。沒有一個演算法適合每一個問題。但是經 過資料探勘處理的問題或資料集絕不是隨機的,也不是所有可能問題的均勻分佈,他們代表的是一個有偏差的樣本,那麼為什麼要應用NFL的結論?答案涉及到上

安裝 Linux 與 Windows 10 雙系統需要的一切

該選Windows 10還是Linux Mint?魚與熊掌當然可以兼得,但咱們得掌握點小技巧才能順利搞定。 Windows 10絕不是唯一一款值得我們安裝在自己計算機之上的免費作業系統。Linux只靠一塊U盤就能順利執行,而且完全無需對現有系統作出任何修改。當然,如果大家打

學習 webpack 前需要的那些概念

什麼是webpack 關於什麼是webpack,一般的教程裡面都會提到webpack是一個模組化打包工作,但是很多初學者沒有模組化這個概念,所以往往在第一步就被攔住了。所以在講什麼是webpack之前,我想先講一下和模組化相關的概念。 javascript的執行環境 因

入坑幣圈需要的數字貨幣錢包那些事兒

問題 strong fff 區塊 重置密碼 單詞 cto 而是 oss 如果你剛剛入坑幣圈,正欲入手數字貨幣,別著急,先靜下心來看一看下面的內容,我猜一定會對你有所幫助。 一、什麽是數字貨幣錢包 很多人說,數字貨幣錢包就是用來裝數字貨幣的,通俗來講這樣理解沒有問題,但實際上

即將到來的金三銀四這10道springboot常見面試題需要

org glassfish 到來 開發人員 ava server ron 有助於 use 1.什麽是Spring Boot? 多年來,隨著新功能的增加,spring變得越來越復雜。只需訪問https://spring.io/projects頁面,我們就會看到可以在我們的應用

小白到大神需要的 sqlite 最佳實踐

本文微信公眾號「AndroidTraveler」首發。 背景 本文是對一篇英文文件的翻譯,原文請見文末連結。 併發資料庫訪問 假設你實現了自己的 SQLiteOpenHelper。 public class DatabaseHelper extends SQLiteOpenHelper { ... } 現在

Spring Cloud Config 配置中心實踐過程中需要這些細節!

本文導讀: Spring Cloud Config 基本概念 Spring Cloud Config 客戶端載入流程 Spring Cloud Config 基於訊息匯流排配置 Spring Cloud Config 中的佔位符 Spring Cloud Config 倉庫最佳實踐 Spring Cloud

關於redis需要的幾點!

一、關於 redis key: 1、是二進位制安全的,也就是說,你可以使用任何形式的二進位制序列來作為key,比如一個string,或者一個jpg圖片的資料,需要說明的是,空字串也是一個有效的key。 2、不建議使用過長的key,影響記憶體佔用及資料查效能,對於過長的key,可以通過hash(例如SHA1)處