前兩天看極客時間 Java
併發課程的時候,刷到一個概念:活鎖。死鎖,卻是不陌生,活鎖倒是第一次聽到。html
在介紹活鎖以前,咱們先來複習一下死鎖。下面的例子模擬一個轉帳業務,多線程環境,爲了帳戶金額安全,對帳戶進行了加鎖。java
1public class Account { 2 public Account(int balance, String card) { 3 this.balance = balance; 4 this.card = card; 5 } 6 private int balance; 7 private String card; 8 public void addMoney(int amount) { 9 balance += amount; 10 } 11 // 省略 get set 方法 12} 13public class AccountDeadLock { 14 public static void transfer(Account from, Account to, int amount) throws InterruptedException { 15 // 模擬正常的前置業務 16 TimeUnit.SECONDS.sleep(1); 17 synchronized (from) { 18 System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard()); 19 synchronized (to) { 20 System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard()); 21 // 轉出帳號扣錢 22 from.addMoney(-amount); 23 // 轉入帳號加錢 24 to.addMoney(amount); 25 } 26 } 27 System.out.println("transfer success"); 28 } 29 30 public static void main(String[] args) { 31 Account from = new Account(100, "6000001"); 32 Account to = new Account(100, "6000002"); 33 34 ExecutorService threadPool = Executors.newFixedThreadPool(2); 35 36 // 線程 1 37 threadPool.execute(() -> { 38 try { 39 transfer(from, to, 50); 40 } catch (InterruptedException e) { 41 e.printStackTrace(); 42 } 43 }); 44 45 // 線程 2 46 threadPool.execute(() -> { 47 try { 48 transfer(to, from, 30); 49 } catch (InterruptedException e) { 50 e.printStackTrace(); 51 } 52 }); 53 54 55 } 56}
上述例子中,當兩個線程進入轉帳方法,線程 1 獲取帳戶 6000001 這把鎖,線程 2 鎖住了帳戶 6000002 鎖。安全
接着當線程 1 想去獲取 6000002 的鎖時,因爲這把鎖已經被線程 2 持有,線程 1 將會陷入阻塞,線程狀態轉爲 BLOCKED。同理,線程 2 也是一樣狀態。多線程
1pool-1-thread-1 lock from account 6000001 2pool-1-thread-2 lock from account 6000002
經過日誌,能夠看到兩個線程開始轉帳方法以後,就陷入等待。併發
synchronized
獲取不到鎖就會阻塞,進行等待。既然這樣,咱們可使用 ReentrantLock#tryLock(long timeout, TimeUnit unit)
進行改造。tryLock
若能獲取鎖,將會返回 true
,若不能獲取鎖將會進行等待,直到知足下列條件:dom
true
false
改造後代碼以下:ide
1public class Account { 2 public Account(int balance, String card) { 3 this.balance = balance; 4 this.card = card; 5 } 6 private int balance; 7 private String card; 8 public void addMoney(int amount) { 9 balance += amount; 10 } 11 // 省略 get set 方法 12} 13public class AccountLiveLock { 14 15 public static void transfer(Account from, Account to, int amount) throws InterruptedException { 16 // 模擬正常的前置業務 17 TimeUnit.SECONDS.sleep(1); 18 // 保證轉帳必定成功 19 while (true) { 20 if (from.lock.tryLock(1, TimeUnit.SECONDS)) { 21 try { 22 System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard()); 23 if (to.lock.tryLock(1, TimeUnit.SECONDS)) { 24 try { 25 System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard()); 26 // 轉出帳號扣錢 27 from.addMoney(-amount); 28 // 轉入帳號加錢 29 to.addMoney(amount); 30 break; 31 } finally { 32 to.lock.unlock(); 33 } 34 35 } 36 } finally { 37 from.lock.unlock(); 38 } 39 } 40 } 41 System.out.println("transfer success"); 42 43 } 44 45 public static void main(String[] args) { 46 Account from = new Account(100, "A"); 47 Account to = new Account(100, "B"); 48 49 ExecutorService threadPool = Executors.newFixedThreadPool(2); 50 51 // 線程 1 52 threadPool.execute(() -> { 53 try { 54 transfer(from, to, 50); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 } 58 }); 59 60 // 線程 2 61 threadPool.execute(() -> { 62 try { 63 transfer(to, from, 30); 64 } catch (InterruptedException e) { 65 e.printStackTrace(); 66 } 67 }); 68 } 69}
上面代碼使用了 while(true)
,獲取鎖失敗,不斷重試,直到成功。運行這個方法,運氣好點,一把就能成功,運氣很差,就會以下:this
1pool-1-thread-1 lock from account 6000001 2pool-1-thread-2 lock from account 6000002 3pool-1-thread-2 lock from account 6000002 4pool-1-thread-1 lock from account 6000001 5pool-1-thread-1 lock from account 6000001 6pool-1-thread-2 lock from account 6000002
transfer
方法一直在運行,可是最終卻得不到成功結果,這就是個活鎖的例子。.net
死鎖將會形成線程阻塞,程序看起來就像陷入假死同樣。就像路上碰到人,你盯着我,我盯着你,互相等待對方讓道,最後誰也過不去。線程
你愁啥?瞅你咋啦?
而活鎖不同,線程不斷重複一樣的操做,但也卻執行不成功。還拿上面舉例,此次你往左一步,他往右邊一步,巧了,又碰上。而後不斷循環,最後仍是誰也過不去。
圖片來源:知乎
分析死鎖這個例子,兩個線程獲取的鎖的順序不一致,最後致使互相須要對方手中的鎖。若是兩個線程加鎖順序一致,所需條件就會同樣,勢必就不會產生死鎖了。
咱們以卡號大小爲順序,每次都給卡號比較大的帳戶先加鎖,這樣就能夠解決死鎖問題,代碼修改以下:
1// 其餘代碼不變 2public static void transfer(Account from, Account to, int amount) throws InterruptedException { 3 // 模擬正常的前置業務 4 TimeUnit.SECONDS.sleep(1); 5 Account maxAccount=from; 6 Account minAccount=to; 7 if(Long.parseLong(from.getCard())<Long.parseLong(to.getCard())){ 8 maxAccount=to; 9 minAccount=from; 10 } 11 12 synchronized (maxAccount) { 13 System.out.println(Thread.currentThread().getName() + " lock account " + maxAccount.getCard()); 14 synchronized (minAccount) { 15 System.out.println(Thread.currentThread().getName() + " lock account " + minAccount.getCard()); 16 // 轉出帳號扣錢 17 from.addMoney(-amount); 18 // 轉入帳號加錢 19 to.addMoney(amount); 20 } 21 } 22 System.out.println("transfer success"); 23 }
對於活鎖的例子,存在兩個問題:
一是鎖的鎖超時時間都同樣,致使兩個線程幾乎同時釋放鎖,重試時又同時上鎖,而後陷入死循環。解決這個問題,咱們可使超時時間不同,引入必定的隨機性。
二是這裏使用 while(true)
,實際開發中萬萬不能這麼玩。這種狀況咱們須要設置最大的重試次數。
畫外音:若是重試這麼屢次,一直不成功,可是業務卻想成功。如今不成功,不要傻着一直試,先放下,記錄下來,待會再重試補償唄~
活鎖的代碼能夠改爲以下:
1 public static final int MAX_TIME = 5; 2 public static void transfer(Account from, Account to, int amount) throws InterruptedException { 3 // 模擬正常的前置業務 4 TimeUnit.SECONDS.sleep(1); 5 // 保證轉帳必定成功 6 Random random = new Random(); 7 int retryTimes = 0; 8 boolean flag=false; 9 while (retryTimes++ < MAX_TIME) { 10 // 等待時間隨機 11 if (from.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) { 12 try { 13 System.out.println(Thread.currentThread().getName() + " lock from account " + from.getCard()); 14 if (to.lock.tryLock(random.nextInt(1000), TimeUnit.MILLISECONDS)) { 15 try { 16 System.out.println(Thread.currentThread().getName() + " lock to account " + to.getCard()); 17 // 轉出帳號扣錢 18 from.addMoney(-amount); 19 // 轉入帳號加錢 20 to.addMoney(amount); 21 flag=true; 22 break; 23 } finally { 24 to.lock.unlock(); 25 } 26 27 } 28 } finally { 29 from.lock.unlock(); 30 } 31 } 32 } 33 if(flag){ 34 System.out.println("transfer success"); 35 }else { 36 System.out.println("transfer failed"); 37 } 38 }
總結
死鎖是平常開發中比較容易碰到的狀況,咱們須要當心,注意加鎖的順序。活鎖,碰到狀況可能不常見,本質上咱們只須要注意設置最大的重試次數,就不會永遠陷入一直重試中。
參考連接