前两天看极客时间 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);// 线程 1threadPool.execute(() -> {try {transfer(from, to, 50);} catch (InterruptedException e) {e.printStackTrace();}});// 线程 2threadPool.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);// 线程 1threadPool.execute(() -> {try {transfer(from, to, 50);} catch (InterruptedException e) {e.printStackTrace();}});// 线程 2threadPool.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

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn

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

  1. 【Python 每日一技】根据序列中每个元素共同的数据域进行分组迭代

    文章目录 1 问题 2. 解决方案 3. 讨论 1 问题 你有一个元素均为字典或其他类型的序列,你希望根据每个元素中的同一个字段(例如:日期)对序列中的所有元素进行分组迭代. 2. 解决方案 iter ...

  2. 苹果nfc功能怎么开启_【每日一技】苹果iPhone如何开启NFC功能?

    苹果如何开启NFC功能,相信还有很多果粉都还不知道,下面小编就手把手教会你们. 方法步骤: 1.点击进入设置 2.找到"钱包与Apple Pay" 3.进入后开启"按两下 ...

  3. iphone主屏幕动态壁纸_【每日一技】使用“实况照片”打造iPhone锁屏动态壁纸

    原标题:[每日一技]使用"实况照片"打造iPhone锁屏动态壁纸 当我们使用 iPhone 拍照时,可以选择拍摄实况照片,iPhone 将会帮您录下拍照前后 1.5 秒所发生的一切 ...

  4. iPhone打开服务器文件很慢,【每日一技】iPhone网速慢怎么办?教你配置DNS让网速飞起...

    原标题:[每日一技]iPhone网速慢怎么办?教你配置DNS让网速飞起 iPhone网速慢,有很多原因.首先要看下是不是WiFi网络本身的问题,比如可以用测试软件测速看看. 另外WiFi连接的人太多, ...

  5. 每日一技|巧用 Telnet 调试 Dubbo 服务

    来自:程序通事 0x00. 前言 想象这样一个场景,线上某个服务突发异常,导致上游服务调用异常,数据处于中间状态.服务恢复之后,我们需要修复这笔数据至正常状态,怎么办? 如果仅是简单的服务,涉及少量数 ...

  6. iphone全部机型_【每日一技】iPhone重启手机和关机后开机有什么区别

    当手机出现卡顿等小问题时,大多数用户都会选择重启手机.对于 iPhone 用户来说,重启手机的方式有两种:一种是强制重启,一种是关机之后再开机.那这两者到底有什么区别呢? 是否会检测硬件: 重启是手机 ...

  7. iphone输入法换行_【每日一技】iPhone输入法不能换行的痛点,用这招0.5秒解决

    每当想换行的时候就会发现,可怜的iPhone并没有换行键. 这个时候换成是你会怎么做?是不是拆成一小段一小段发,这方法虽然可行但分太多行消息感觉就是一个人在刷屏,给其他人看体验也不好. 先发到备忘录分 ...

  8. 每日一技:给女友用代码做一个3D旋转相册,每天亿遍忘记初恋~

    前言 不会表白?!我来教你给女朋友或者正在追求的妹子一点小惊喜~ 今天这篇文章就是演示给女友做一个3D旋转相册,学会的小伙伴可以给自己的女朋友或者喜欢的女生做一个,相比几百上千的礼物,零成本的技术实现 ...

  9. 【Python 每日一技】根据任意分隔符分割字符串

    文章目录 1 问题 2. 解决方案 3. 讨论 1 问题 你需要分割一个字符串,但是字符串内的分隔符并不是同一个. 2. 解决方案 实际上,虽然字符串对象有一个 split() 方法,但是该方法一般只 ...

  10. 【Python 每日一技】建立多个值和单个键的映射

    1 问题 你希望创建一个字典,该字典可以建立多个值和单个键之间的映射(即所谓的多值字典). 2. 解决方案 在 Python 中,基于普通的字典类 dict 创建的对象一般只可以存储一个键和一个值的映 ...

最新文章

  1. 第十六届全国大学生智能车竞赛华南赛区竞赛事宜的通知
  2. 攻击链路识别——CAPEC(共享攻击模式的公共标准)、MAEC(恶意软件行为特征)和ATTCK(APT攻击链路上的子场景非常细)...
  3. 深度学习(二)——深度学习常用术语解释, Neural Network Zoo, CNN, Autoencoder
  4. 收藏 | 使用 Mask-RCNN 在实例分割应用中克服过拟合
  5. python语言案例教程 单元测试_python单元测试unittest实例详解
  6. web项目在iis配置好后不能正确访问问题集锦,以及IIS常规设置
  7. js 中json对象转字符串
  8. Android编程入门-第1天
  9. 集成第三方SDK之支付宝支付
  10. 手机越贵,打车越贵?复旦教授三万字打车报告,实锤打车软件“大数据杀熟”
  11. java开发工程师面试自我介绍_Java程序员自我介绍
  12. 变压器励磁模型 Matlab/simulink 可用于模拟电压暂降等电能质量问题
  13. 计算机管理员工作目标任务书,毕业论文任务书中主要任务及目标怎么写
  14. 非分区表转换为分区表的三种方式
  15. 题目:代码实现判断单链表是否有环
  16. 推广链接生成html操作流程,推广链接使用指引
  17. 手机抓包+注入黑科技HttpCanary——最强大的Android抓包注入工具
  18. 关于手机话费充值的方法
  19. leetcode 1. 黑白方格画
  20. HDU - 3594 Cactus (仙人掌图)

热门文章

  1. jQuery 文档操作方法 (四)
  2. Jquery获取web窗体关闭事件,排除刷新页面
  3. LeetCode5 最长回文子串
  4. 操作系统 生产者消费者问题解释
  5. 计算机操作系统笔记(三)
  6. 1、matplotlib绘制一个简单的图形
  7. sklearn--各分类算法简单应用
  8. 十行代码训练sklearn七种分类算法
  9. Firefox 2015 最受国人欢迎的十大扩展
  10. 感知器分类模型回顾与python实现