每日一技|活鎖,也許你須要瞭解一下

前兩天看極客時間 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    }

總結

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

參考連接

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

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

相關文章
相關標籤/搜索