每日一技|活鎖,也許你需要了解一下
- 2020 年 2 月 26 日
- 筆記
前兩天看極客時間 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
本文轉載自公眾號:樓下小黑哥