每日一技|活锁,也许你需要了解一下
前两天看极客时间
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 }
总结
死锁是日常开发中比较容易碰到的情况,我们需要小心,注意加锁的顺序。活锁,碰到情况可能不常见,本质上我们只需要注意设置最大的重试次数,就不会永远陷入一直重试中。
参考链接
- 彦舜原创,CSDN首发:你也许需要对接口多了解一下
- 用Unity制作游戏,你需要深入了解一下IL2CPP
- 学习Python语言,需要先了解一下Python有什么优缺点?
- 也许你需要了解Teams中的会议室分类
- 用Unity做游戏,你需要深入了解一下IL2CPP
- 需要接口管理的你了解一下?
- 介绍“Razor”— ASP.NET的一个新视图引擎(落后了好多,需要用这个就了解一下)
- 学习Python语言,需要先了解一下Python有什么优缺点?
- 用Unity制作游戏,你需要深入了解一下IL2CPP
- 传统企业运用人工智能,需要先了解一下这些问题?
- 对于写bash脚本的朋友,read命令是不可或缺的,需要实践一下就可以了解read命令的大致用途: 编写一个脚本: #!/bin/bash # hao32 test read echo -e "Pl
- 用Unity做游戏,你需要深入了解一下IL2CPP
- 编程也需要有大局观 新鲜出炉的大局观 了解一下
- 用Unity做游戏,你需要深入了解一下IL2CPP
- 用Unity做游戏,你需要深入了解一下IL2CPP(一)
- 用Unity做游戏,你需要深入了解一下IL2CPP
- 想学习大数据开发?想从事大数据行业?那你需要了解一下这个
- ES2018新特性——每个JS开发者都需要了解
- 了解一下STM32数据类型及各种书写方式
- 个性化WinPE封装方法----制作过程需要了解的“命令”