活锁是什么?也许你需要了解一下

点击上方“码农沉思录”,选择“设为星标”

优质文章,及时送达

前两天看极客时间 Java 并发课程的时候,刷到一个概念:活锁。死锁,倒是不陌生,活锁却是第一次听到。

在介绍活锁之前,我们先来复习一下死锁。下面的例子模拟一个转账业务,多线程环境,为了账户金额安全,对账户进行了加锁。

代码语言:javascript复制 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 也是同样状态。

代码语言:javascript复制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,若不能获取锁将会进行等待,直到满足下列条件:

超时时间内获取到了锁,返回 true超时时间内未获取到锁,返回 false中断,抛出异常改造后代码如下:

代码语言:javascript复制 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),获取锁失败,不断重试,直到成功。运行这个方法,运气好点,一把就能成功,运气不好,就会如下:

代码语言:javascript复制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 6000002transfer 方法一直在运行,但是最终却得不到成功结果,这就是个活锁的例子。

死锁将会造成线程阻塞,程序看起来就像陷入假死一样。就像路上碰到人,你盯着我,我盯着你,互相等待对方让道,最后谁也过不去。

你愁啥?瞅你咋啦?

而活锁不一样,线程不断重复同样的操作,但也却执行不成功。还拿上面举例,这次你往左一步,他往右边一步,巧了,又碰上。然后不断循环,最后还是谁也过不去。

图片来源:知乎

分析死锁这个例子,两个线程获取的锁的顺序不一致,最后导致互相需要对方手中的锁。如果两个线程加锁顺序一致,所需条件就会一样,势必就不会产生死锁了。

我们以卡号大小为顺序,每次都给卡号比较大的账户先加锁,这样就可以解决死锁问题,代码修改如下:

代码语言:javascript复制 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())

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),实际开发中万万不能这么玩。这种情况我们需要设置最大的重试次数。

画外音:如果重试这么多次,一直不成功,但是业务却想成功。现在不成功,不要傻着一直试,先放下,记录下来,待会再重试补偿呗~

活锁的代码可以改成如下:

代码语言:javascript复制 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