1、简介
2、死锁产生的条件
死锁的产生有四个必要的条件
- 互斥使用,即当资源被一个线程占用时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放
- 请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有
- 循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路
对于死锁产生的四个条件只要能破坏其中一条即可让死锁消失,但是条件一是基础,不能被破坏。
3、各种死锁的介绍
3.1 锁顺序死锁
LeftRightDeadLock示例代码:
package com.liziba.dl; / * * 顺序死锁 *
* * @Author: Liziba */ public class LeftRightDeadLock {
private final Object right = new Object(); private final Object left = new Object(); / * 加锁顺序从left -> right */ public void leftToRight() {
synchronized (left) {
synchronized (right) {
System.out.println(Thread.currentThread().getName() + " left -> right lock."); } } } / * 加锁顺序right -> left */ public void rightToLeft() {
synchronized (right) {
synchronized (left) {
System.out.println(Thread.currentThread().getName() + " right -> left lock."); } } } }
测试代码,通过创建多个线程,并发执行上面的LeftRightDeadLock
public static void main(String[] args) {
LeftRightDeadLock lrDeadLock = new LeftRightDeadLock(); for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 为了更好的演示死锁,将两个方法的调用放置到同一个线程中执行 lrDeadLock.leftToRight(); lrDeadLock.rightToLeft(); }, "ThreadA-"+i).start(); } }

jstack查看进程中的线程信息,线程信息比较多,我把重要的复制出来,如下的图中能很明显的看到产生了死锁。

这里省略了很多线程当前状态信息

3.2 动态锁顺序死锁
3.2.1 动态锁顺序死锁的产生与示例
package com.liziba.dl; import java.math.BigDecimal; / * * 账户类 *
* * @Author: Liziba */ public class Account {
/ 账户 */ public String number; / 余额 */ public BigDecimal balance; public Account(String number, BigDecimal balance) {
this.number = number; this.balance = balance; } public void setNumber(String number) {
this.number = number; } public void setBalance(BigDecimal balance) {
this.balance = balance; } }
定义转账类TransferMoney,其中有transferMoney()方法用于accountFrom账户向accountTo转账金额amt:
package com.liziba.dl; import java.math.BigDecimal; / * * 转账类 *
* * @Author: Liziba */ public class TransferMoney {
/ * 转账方法 * * @param accountFrom 转账方 * @param accountTo 接收方 * @param amt 转账金额 * @throws Exception */ public static void transferMoney(Account accountFrom, Account accountTo, BigDecimal amt) throws Exception {
synchronized (accountFrom) {
synchronized (accountTo) {
BigDecimal formBalance = accountFrom.balance; if (formBalance.compareTo(amt) < 0) {
throw new Exception(accountFrom.number + " balance is not enough."); } else {
accountFrom.setBalance(formBalance.subtract(amt)); accountTo.setBalance(accountTo.balance.add(amt)); System.out.println("Form" + accountFrom.number + ": " + accountFrom.balance.toPlainString() +"\t" + "To" + accountTo.number + ": " + accountTo.balance.toPlainString()); } } } } }
上面这个类看似规定了锁的顺序由accountFrom到accountTo不会产生死锁,但是这个accountFrom和accountTo是由调用方来传入的,当A向B转账时accountFrom = A,accountTo = B;当B向A转账时accountFrom = B,accountTo = A;假设两者在同一时刻给对方发起转账,则仍然存在3.1中锁顺序死锁问题。比如如下测试:
public static void main(String[] args) {
// 账户A && 账户B Account accountA = new Account("", new BigDecimal(10000)); Account accountB = new Account("", new BigDecimal(10000)); // 循环创建线程 A -> B ; B -> A 各一百个线程 for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
// 转账顺序 A -> B transferMoney(accountA, accountB, new BigDecimal(10)); } catch (Exception e) {
return; } }).start(); new Thread(() -> {
try {
// 转账顺序 B -> A transferMoney(accountB, accountA, new BigDecimal(10)); } catch (Exception e) {
return; } }).start(); } }
3.2.2 动态锁顺序死锁的解决
package com.liziba.dl; import java.math.BigDecimal; / * * 转账类优化 -> 通过hash算法 *
* * @Author: Liziba */ public class TransferMoneyOptimize {
/ hash 冲突时使用第三个锁(优秀的hash算法冲突是很少的!) */ private static final Object conflictShareLock = new Object(); / * 转账方法 * * @param accountFrom 转账方 * @param accountTo 接收方 * @param amt 转账金额 * @throws Exception */ public static void transferMoney(Account accountFrom, Account accountTo, BigDecimal amt) throws Exception {
// 计算hash值 int accountFromHash = System.identityHashCode(accountFrom); int accountToHash = System.identityHashCode(accountTo); // 如下三个分支能一定控制账户之间的转是不会产生死锁的 if (accountFromHash > accountToHash) {
synchronized (accountFrom) {
synchronized (accountTo) {
transferMoneyHandler(accountFrom, accountTo, amt); } } } else if (accountToHash > accountFromHash) {
synchronized (accountTo) {
synchronized (accountFrom) {
transferMoneyHandler(accountFrom, accountTo, amt); } } } else {
// 解决hash冲突 synchronized (conflictShareLock) {
synchronized (accountFrom) {
synchronized (accountTo) {
transferMoneyHandler(accountFrom, accountTo, amt); } } } } } / * 账户金额增加处理 * * @param accountFrom 转账方 * @param accountTo 接收方 * @param amt 转账金额 * @throws Exception */ private static void transferMoneyHandler(Account accountFrom, Account accountTo, BigDecimal amt) throws Exception {
if (accountFrom.balance.compareTo(amt) < 0) {
throw new Exception(accountFrom.number + " balance is not enough."); } else {
accountFrom.setBalance(accountFrom.balance.subtract(amt)); accountTo.setBalance(accountTo.balance.add(amt)); System.out.println("Form" + accountFrom.number + ": " + accountFrom.balance.toPlainString() +"\t" + "To" + accountTo.number + ": " + accountTo.balance.toPlainString()); } } }
测试代码与上面错误的示例代码一致,经过数次其输出结果均为如下:

3.3 协作对象之间的死锁
3.3.1 协作对象死锁的产生与示例
- Coordinate -> 坐标类,出租车经纬度信息类
- Taxi -> 出租车类,出租车所属于某个出租车车队Fleet,此外包含当前坐标location和目的地坐标destination,出租车在更新目的地信息的时候会判断当前坐标与目的地坐标是否相等,相等则会通知所属车队车辆空闲,可以接收下一个目的地
- Fleet -> 出租车车队类,出租车类包含两个集合taxis和available,分别用来保存车队中所有车辆信息和车队中当前空闲的出租车信息,此外提供获取车队中所有出租车当前地址信息的快照方法getImage()
- Image -> 车辆地址信息快照类,用于获取出租车的地址信息
Coordinate(坐标类) 代码示例:
package com.liziba.dl; / * * 坐标类 *
* * @Author: Liziba */ public class Coordinate {
/ 经度 */ private Double longitude; / 纬度 */ private Double latitude; // 省略 getXxx,setXxx等方法 }
Taxi(出租车类)代码示例;
package com.liziba.dl; import java.util.Objects; / * * 出租车类 *
* * @Author: Liziba */ public class Taxi {
/ 出租车唯一标志 */ private String id; / 当前坐标 */ private Coordinate location; / 目的地坐标 */ private Coordinate destination; / 所属车队 */ private final Fleet fleet; / * 获取当前地址信息 * @return */ public synchronized Coordinate getLocation() {
return location; } / * 更新当前地址信息 * 如果当前地址与目的地地址一致,则表名到达目的地需要通知车队,当前出租车空闲可用前往下一个目的地 * * @param location */ public synchronized void setLocation(Coordinate location) {
this.location = location; if (location.equals(destination)) {
fleet.free(this); } } public Coordinate getDestination() {
return destination; } / * 设置目的地 * * @param destination */ public synchronized void setDestination(Coordinate destination) {
this.destination = destination; } public Taxi(Fleet fleet) {
this.fleet = fleet; } public String getId() {
return id; } public void setId(String id) {
this.id = id; } @Override public boolean equals(Object o) {
if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Taxi taxi = (Taxi) o; return Objects.equals(location, taxi.location) && Objects.equals(destination, taxi.destination); } @Override public int hashCode() {
return Objects.hash(location, destination); } }
Fleet(出租车车队类)示例代码:
package com.liziba.dl; import java.util.Set; / * * 车队类 -> 调度管理出租车 *
* * @Author: Liziba */ public class Fleet {
/ 车队中所有出租车 */ private final Set<Taxi> taxis; / 车队中目前空闲的出租车 */ private final Set<Taxi> available; public Fleet(Set<Taxi> taxis) {
this.taxis = this.available = taxis; / * 出租车到达目的地后调用该方法,向车队发出当前出租车空闲信息 * * @param taxi */ public synchronized void free(Taxi taxi) {
available.add(taxi); } / * 获取所有出租车在不同时刻的地址快照 * @return */ public synchronized Image getImage() {
Image image = new Image(); for (Taxi taxi : taxis) {
image.drawMarker(taxi); } return image; } }
Image(车辆地址信息快照类)示例代码:
package com.liziba.dl; import java.util.HashMap; import java.util.Map; / * * 获取所有出租车在某一时刻的位置快照 *
* * @Author: Liziba */ public class Image {
Map<String, Coordinate> locationSnapshot = new HashMap<>(); public void drawMarker(Taxi taxi) {
locationSnapshot.put(taxi.getId(), taxi.getLocation()); } }
在上述代码中,看不到一个方法中有对多个资源直接加锁,但仔细分析却能发现在方法的调用之间是存在对多个资源“隐式”加锁的,比如Taxi中的setLocation(Coordinate location)与Fleet中的Image getImage()。
- setLocation(Coordinate location)方法需要获取当前出租车Taxi对象的锁以及出租车所属车队Fleet的锁
- getImage()方法需要获取当前车队Fleet的锁,以及在遍历出租车获取其地址信息时需要获取每个出租车Taxi对象的锁
3.3.2 协作对象之间的死锁解决
Taxi -> TaxiOptimize(优化出租车类):
package com.liziba.dl; import java.util.Objects; / * * 出租车类优化 *
* * @Author: Liziba */ public class TaxiOptimize {
// 省略相同的属性和函数 / * 优化内容 * setLocation(Coordinate location)方法不在加锁 * 将同步范围(锁住的代码)缩小 * this的锁与fleet顺序获取 ,锁内没有嵌套,不会死锁 * * @param location */ public void setLocation(Coordinate location) {
this.location = location; boolean release = false; synchronized (this) {
if (location.equals(destination)) {
release = true; } } if (release) {
fleet.free(this); } } }
Fleet -> FleetOptimize(优化出租车车队类):
package com.liziba.dl; import java.util.HashSet; import java.util.Set; / * * 出租车车队类优化 *
* * @Author: Liziba */ public class FleetOptimize {
// 省略相同的属性和函数 / * 优化内容 * getImage()不再加锁 * 将同步范围(锁住的代码)缩小 * this(出租车车队对象)与drawMarker()方法中获取taxi对象的锁不再嵌套不会死锁 * * @return */ public Image getImage() {
Set<TaxiOptimize> copy ; synchronized (this) {
copy = new HashSet<TaxiOptimize>(taxis); } Image image = new Image(); for (TaxiOptimize taxi : copy) {
image.drawMarker(taxi); } return image; } }
3.4 资源死锁
3.4.1 数据库连接池资源死锁
3.4.2 线程饥饿死锁
如下通过Executors.newSingleThreadExecutor()构建一个只有一个线程的线程池,提交的主任务会再次提交两个任务到这个线程池中去执行,在主任务中等待两个子任务的结果,而子任务又必须等到主任务执行结束后才能执行,这种情况就会产生线程饥饿死锁。
package com.lizba.currency.deadlock; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; / * * 单线程Executor中任务发送死锁 *
* * @Author: Liziba * @Date: 2021/7/1 21:25 */ public class ThreadDeadLock {
/ 单个线程的线程池 */ static ExecutorService executorService = Executors.newSingleThreadExecutor(); public static class Task1 implements Callable<String> {
@Override public String call() throws Exception {
Future<String> first = executorService.submit(new Task2()); Future<String> second = executorService.submit(new Task2()); // 当前任务等待子任务的结果,但是两个子任务在等待主任务完成,导致死锁 return first.get() + second.get(); } } public static class Task2 implements Callable<String> {
@Override public String call() throws Exception {
return "Hello Java"; } } / * 测试 * @param args */ public static void main(String[] args) {
executorService.submit(new Task1()); } }
4、死锁的避免和诊断
关于死锁的避免主要是这几个方面:
- 尽可能使用无锁编程,使用开放调用的编码设计
- 尽可能的缩小锁的范围,防止锁住的资源过多引发阻塞和饥饿
- 如果加锁的时候需要获取多个锁则应该正确的设计锁的顺序
- 使用定时锁,比如Lock中的tryLock()
关于死锁的诊断主要是这几个方面:
- 找出代码什么地方会使用多个锁,对这些代码实例进行全局分析
- 通过线程转储(Thread Dump)信息来分析死锁
5、死锁以外的其他活跃性危险
除了死锁以外,并发的程序中可能还会存在以下几种风险
5.1 饥饿
- 不随意改变线程的优先级,尽量使得线程的优先级一致(这个在大部分场景都是适用的)
- 任务的执行尽量保持随机性或者公平性(性能考虑优先)
5.2 响应时间长
- 异步执行
- 避免代码中锁住的资源过大或者是CPU密集型的资源(尽量优化)
- 提升硬件设备
- 合理的设计线程执行的优先级
5.3 活锁
- 增加重试的随机性
- 增大重试间隔时间
- 设置最大重试次数
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/229578.html原文链接:https://javaforall.net
