来自:程序通事

前两天看 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,若不能获取锁将会进行等待,直到满足下列条件:

  • 超时时间内获取到了锁,返回 true

  • 超时时间内未获取到锁,返回 false

  • 中断,抛出异常

改造后代码如下:

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

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 方法一直在运行,但是最终却得不到成功结果,这就是个活锁的例子。

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

你愁啥?瞅你咋啦?

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

图片来源:知乎

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

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

 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

特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

长按订阅更多精彩▼如有收获,点个在看,诚挚感谢

活锁,也许你需要了解一下相关推荐

  1. 死锁和活锁有什么区别?

    本文翻译自:What's the difference between deadlock and livelock? 有人可以举例说明(代码) 死锁和活锁有什么区别吗? #1楼 参考:https:// ...

  2. 爱情也许是最忧伤的童话

    也许爱情是一部忧伤的童话 惟其遥远与真实 惟其不可触摸与欠缺 方可成就起璀璨与神圣 或许,只有在难得最远的时候, 才能把曾经走过的那段日子 看得最真切.最清楚 放弃一个很爱你的人,并不痛苦 放弃一个你 ...

  3. 你也许只使用到了 VS Code 20% 的功能

    Visual Studio Code 作为广受好评的开发工具,已经被越来越多的开发者当作首选的开发工具.然而,你真的了解 VS Code 了吗?你真的会使用 VS Code,把 VS Code 的强大 ...

  4. 苹果地图副总裁_也许,苹果汽车的到来只是早晚问题

    文/小米 这两天,一篇苹果向加州机动车辆管理局提交的报告,再次引起了大家对苹果造车的热议,该报告内容为:"一辆自动驾驶模式下的苹果测试车辆在准备从基弗路向南并入劳伦斯高速公路时遭遇追尾.一辆 ...

  5. 关于MySQL线程池,这也许是目前最全面的实用帖!

    作者介绍 张秀云,网名飞鸿无痕,现任职于腾讯,负责腾讯金融数据库的运维和优化工作.2007年开始从事运维方面的工作,经历过网络管理员.Linux运维工程师.DBA.分布式存储运维等多个职位.对Linu ...

  6. Hr必看:也许你做的绩效考核都是错的

    作为管理者,如果你觉得绩效是一种负担的话,那你就要好好想想,你的目标有没有定好,人员是怎么管理的,你有没有践行公司的制度,有没有传承公司的文化. 绩效其实是管理者的百宝箱. 如何做好激励与绩效?我从下 ...

  7. 还在为投文章发愁吗,也许你更适合审别人的文章——JGG期刊专职编辑招聘(IF4)...

    主编按:还在为发表文章发愁吗,还在为文章被秒拒苦恼吗?也许你可以跳出投文章的怪圈,成为文章的审判者.现在就有一本优秀的杂志等着你,Journal of Genetics and Genomics (J ...

  8. 小米android10怎么样,感觉小米10太贵不完美?这些Android旗舰也许就有你的菜!

    昨天CFan对比了三星Galaxy S20和小米10系列的异同(详见<Galaxy S20和小米10谁更值?看完这篇文章你就懂了!>).除了这两款新秀外,接下来我们还将迎来很多旗舰级的An ...

  9. 互联网大佬口口声声的人工智能,笑到最后的也许是马云的云计算

    程凯 一届在乌镇开的互联网大会,简直就成了一个人工智能大会. 李彦宏直接说,移动互联网时代已经结束了,未来的机会在人工智能.他说,移动互联网市场已进入平稳发展阶段,在这个领域如果今天一个公司还没有成立 ...

最新文章

  1. 关于Python爬虫原理和数据抓取1.1
  2. 一个sql的执行过程详解
  3. matlab溢出的标志inf,关于C#:溢出与信息
  4. Lecture 21 Parallel Algorithms II
  5. 前端学习(381):CSS3 的视口单位vw、vh实现自适应(带有px,em,rem的简单介绍)
  6. 两个瓶子水怎样一样多_同事每天比我多睡两个小时!省下70万买了地铁站附近房子 杭州姑娘却感叹买房时一定是脑子进了水……...
  7. linux文件系统初始化过程(6)---执行init程序
  8. python unpack_python中struct.pack()函数和struct.unpack()函数
  9. Jquery ui的dialog使用文档概述
  10. mysql之分页_MySQL之分页查询(DQL)
  11. python入门——P44魔法方法:简单定制
  12. docker-compose的一些理解
  13. Service onStartCommand 返回值
  14. 微信分享 无法获取到分享状态的问题-微信分享功能调整
  15. 在VMware上安装Android虚拟机
  16. 超级计算机也无法算尽圆周率,如果圆周率算尽了,会出现什么后果?
  17. Redis(二)——复制
  18. Class热替换与卸载
  19. mysql in个数限制_mysql where in 条件中参数个数问题
  20. postfix连接不上mysql_mysql – Postfix sasl登录失败没有找到机制

热门文章

  1. python中使用html前端页面显示图像预测结果(Pycharm)
  2. python rsa 公钥解密_python利用rsa库做公钥解密的方法教程
  3. vscode pylint 错误_VScode中报Unable to import #x27;xxx#x27; pylint的解决方案
  4. linux添加ssl信任根证书,linux系统添加根证书linux证书信任列表
  5. unknown error mysql_解决MySQL执行SQL文件时报Error: Unknown storage engine 'InnoDB'的错误
  6. POJ 2411 Mondriaan‘s Dream(最清楚好懂的状压DP讲解)(连通性状态压缩DP)
  7. imu oracle,问一个关于IMU REDO的问题~
  8. 写在2014最后一天
  9. 高防服务器究竟能防御哪些攻击?
  10. 英特尔收购Movidius背后:为什么我们需要一款专门的CV处理芯片?