java多线程避免死锁_多线程之死锁就是这么简单

前言

只有光头才能变强

回顾前面:

本篇主要是讲解死锁,这是我在多线程的最后一篇了。主要将多线程的基础过一遍,以后有机会再继续深入!

死锁是在多线程中也是比较重要的知识点了!

那么接下来就开始吧,如果文章有错误的地方请大家多多包涵,不吝在评论区指正哦~

声明:本文使用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锁也没有释放。

所以他们都只能等待,而这种等待是无期限的-->永久等待-->死锁

68c0fef7b63e

image

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 taxis;

@GuardedBy("this") private final Set availableTaxis;

public Dispatcher() {

taxis = new HashSet();

availableTaxis = new HashSet();

}

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 taxis;

@GuardedBy("this") private final Set availableTaxis;

public Dispatcher() {

taxis = new HashSet();

availableTaxis = new HashSet();

}

public synchronized void notifyAvailable(Taxi taxi) {

availableTaxis.add(taxi);

}

public Image getImage() {

Set copy;

// Dispatcher内置锁

synchronized (this) {

copy = new HashSet(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分析。

具体可参考:

三、总结

发生死锁的原因主要由于:

线程之间交错执行

解决:以固定的顺序加锁

执行某方法时就需要持有锁,且不释放

解决:缩减同步代码块范围,最好仅操作共享变量时才加锁

永久等待

解决:使用tryLock()定时锁,超过时限则返回错误信息

在操作系统层面上看待死锁问题(这是我之前做的笔记、很浅显):

参考资料:

《Java核心技术卷一》

《Java并发编程实战》

《计算机操作系统 汤小丹》

如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y。为了大家方便,刚新建了一下qq群:742919422,大家也可以去交流交流。谢谢支持了!希望能多介绍给其他有需要的朋友

文章的目录导航:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值