多线程之死锁就是这么简单
前言
只有光头才能变强
回顾前面:
- ThreadLocal就是这么简单
- 多线程三分钟就可以入个门了!
- 多线程基础必要知识点!看了学习多线程事半功倍
- Java锁机制了解一下
- AQS简简单单过一遍
- Lock锁子类了解一下
- 线程池你真不来了解一下吗?
本篇主要是讲解死锁,这是我在多线程的最后一篇了。主要将多线程的基础过一遍,以后有机会再继续深入!
死锁是在多线程中也是比较重要的知识点了!
那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~
声明:本文使用JDK1.8
一、死锁讲解
在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,不再程序往下执行。我们只能通过中止并重启的方式来让程序重新执行。
- 这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!
造成死锁的原因可以概括成三句话:
- 当前线程拥有其他线程需要的资源
- 当前线程等待其他线程已拥有的资源
- 都不放弃自己拥有的资源
1.1锁顺序死锁
首先我们来看一下最简单的死锁(锁顺序死锁)是怎么样发生的:
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
// 得到left锁
synchronized (left) {
// 得到right锁
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
// 得到right锁
synchronized (right) {
// 得到left锁
synchronized (left) {
doSomethingElse();
}
}
}
}
我们的线程是交错执行的,那么就很有可能出现以下的情况:
- 线程A调用
leftRight()
方法,得到left锁 -
同时线程B调用
rightLeft()
方法,得到right锁 - 线程A和线程B都继续执行,此时线程A需要right锁才能继续往下执行。此时线程B需要left锁才能继续往下执行。
- 但是:线程A的left锁并没有释放,线程B的right锁也没有释放。
- 所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁
1.2动态锁顺序死锁
我们看一下下面的例子,你认为会发生死锁吗?
// 转账
public static void transferMoney(Account fromAccount,
Account toAccount,
DollarAmount amount)
throws InsufficientFundsException {
// 锁定汇账账户
synchronized (fromAccount) {
// 锁定来账账户
synchronized (toAccount) {
// 判余额是否大于0
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
// 汇账账户减钱
fromAccount.debit(amount);
// 来账账户增钱
toAccount.credit(amount);
}
}
}
}
上面的代码看起来是没有问题的:锁定两个账户来判断余额是否充足才进行转账!
但是,同样有可能会发生死锁:
- 如果两个线程同时调用
transferMoney()
- 线程A从X账户向Y账户转账
- 线程B从账户Y向账户X转账
- 那么就会发生死锁。
A:transferMoney(myAccount,yourAccount,10);
B:transferMoney(yourAccount,myAccount,20);
1.3协作对象之间发生死锁
我们来看一下下面的例子:
public class CooperatingDeadlock {
// Warning: deadlock-prone!
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
// setLocation 需要Taxi内置锁
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination))
// 调用notifyAvailable()需要Dispatcher内置锁
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
// 调用getImage()需要Dispatcher内置锁
public synchronized Image getImage() {
Image image = new Image();
for (Taxi t : taxis)
// 调用getLocation()需要Taxi内置锁
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
上面的getImage()
和setLocation(Point location)
都需要获取两个锁的
- 并且在操作途中是没有释放锁的
这就是隐式获取两个锁(对象之间协作)..
这种方式也很容易就造成死锁…..
二、避免死锁的方法
避免死锁可以概括成三种方法:
- 固定加锁的顺序(针对锁顺序死锁)
- 开放调用(针对对象之间协作造成的死锁)
-
使用定时锁-->
tryLock()
- 如果等待获取锁时间超时,则抛出异常而不是一直等待!
2.1固定锁顺序避免死锁
上面transferMoney()
发生死锁的原因是因为加锁顺序不一致而出现的~
- 正如书上所说的:如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题!
那么上面的例子我们就可以改造成这样子:
public class InduceLockOrder {
// 额外的锁、避免两个对象hash值相等的情况(即使很少)
private static final Object tieLock = new Object();
public void transferMoney(final Account fromAcct,
final Account toAcct,
final DollarAmount amount)
throws InsufficientFundsException {
class Helper {
public void transfer() throws InsufficientFundsException {
if (fromAcct.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
}
// 得到锁的hash值
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
// 根据hash值来上锁
if (fromHash < toHash) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {// 根据hash值来上锁
synchronized (toAcct) {
synchronized (fromAcct) {
new Helper().transfer();
}
}
} else {// 额外的锁、避免两个对象hash值相等的情况(即使很少)
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
}
得到对应的hash值来固定加锁的顺序,这样我们就不会发生死锁的问题了!
2.2开放调用避免死锁
在协作对象之间发生死锁的例子中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法!
- 如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用!
我们可以这样来改造:
- 同步代码块最好仅被用于保护那些涉及共享状态的操作!
class CooperatingNoDeadlock {
@ThreadSafe
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
boolean reachedDestination;
// 加Taxi内置锁
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
// 执行同步代码块后完毕,释放锁
if (reachedDestination)
// 加Dispatcher内置锁
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
@ThreadSafe
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
public Image getImage() {
Set<Taxi> copy;
// Dispatcher内置锁
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
// 执行同步代码块后完毕,释放锁
Image image = new Image();
for (Taxi t : copy)
// 加Taix内置锁
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
使用开放调用是非常好的一种方式,应该尽量使用它~
2.3使用定时锁
使用显式Lock锁,在获取锁时使用tryLock()
方法。当等待超过时限的时候,tryLock()
不会一直等待,而是返回错误信息。
使用tryLock()
能够有效避免死锁问题~~
2.4死锁检测
虽然造成死锁的原因是因为我们设计得不够好,但是可能写代码的时候不知道哪里发生了死锁。
JDK提供了两种方式来给我们检测:
- JconsoleJDK自带的图形化界面工具,使用JDK给我们的的工具JConsole
- Jstack是JDK自带的命令行工具,主要用于线程Dump分析。
具体可参考:
- https://www.cnblogs.com/flyingeagle/articles/6853167.html
三、总结
发生死锁的原因主要由于:
- 线程之间交错执行
- 解决:以固定的顺序加锁
- 执行某方法时就需要持有锁,且不释放
- 解决:缩减同步代码块范围,最好仅操作共享变量时才加锁
- 永久等待
- 解决:使用
tryLock()
定时锁,超过时限则返回错误信息
- 解决:使用
在操作系统层面上看待死锁问题(这是我之前做的笔记、很浅显):
参考资料:
- 《Java核心技术卷一》
- 《Java并发编程实战》
- 《计算机操作系统 汤小丹》
- “单播”、“组播”和“多播”
- flash player10.1 + FMS4中的p2p功能
- fms4 p2p:图片分享
- 老域名新用的优缺点分析
- 不用临时变量,交换二个整型变量的值
- monoTouch开发(1):win7 + vmware下安装mac os
- 数据结构C#版笔记--队列(Quene)
- 数据结构C#版笔记--堆栈(Stack)
- MySQL基础入门-第一课 新建数据库(linux版本)
- 2017年度最不安全密码报告,看看你的密码安全吗?
- 数据结构C#版笔记--顺序表(SeqList)
- 数据结构C#版笔记--单链表(LinkList)
- 操作系统 页式存储 页与块之间的关系详解
- 数据结构C#版笔记--双向链表(DbLinkList)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 近期问题: jq循环中异步请求问题
- 启用HSTS并加入HSTS Preload List-附删除HSTS方法
- 聊聊UI标准化
- puppeteer学习----登录人才网并截图
- MySQL 案例:用户鉴权与 Host 优先级
- Http Post 快速使用
- 有赞Flutter插件开发与发布
- 国标GB28181协议客户端EasyGBS国标视频平台级联EasyNVR:EasyGBS如何实现调阅EasyNVR的视频通道?
- gitlab CI/CD 相关问题
- 微信jssdk分享接口
- 有赞 Flutter 混编方案
- 最近开发问题
- echarts相关问题总结
- 视频上云EasyNTS组网硬件设备登录后自动下线并清除设备信息是什么原因?
- 震惊! 再也不怕蹭网被发现了!