【多线程 - 11、死锁】
死锁
1、介绍
在 Java 中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,程序不再往下执行。只能通过中止并重启的方式来让程序重新执行。要尽可能避免死锁的情况发生
2、造成死锁的原因
互斥条件:
同一资源同时只能由一个线程读取
不可抢占条件:
不能强行剥夺线程占有的资源
请求和保持条件:
请求其他资源的同时对自己手中的资源保持不放
循环等待条件:
在相互等待资源的过程中,形成一个闭环
预防死锁:
只需要破坏其中一个条件即可,比如使用定时锁、尽量让线程用相同的加锁顺序,加锁超时或自动释放,死锁检测算法等等
3、避免死锁的方法
- 固定加锁的顺序(针对锁顺序死锁)
- 开放调用(针对对象之间协作造成的死锁)
- 使用定时锁–>
tryLock()
;如果等待获取锁时间超时,则抛出异常而不是一直等待
二、顺序死锁
例子
public class ThreadTest6 {
public static void main(String[] args) {
Demo demo = new Demo();
new Thread(demo,"1").start();
new Thread(demo,"2").start();
new Thread(demo,"3").start();
new Thread(demo,"4").start();
}
}
class Demo implements Runnable{
Account a = new Account("A",1000);
Account b = new Account("B",1000);
@Override
public void run() {
transferMoney(a,b,100);
transferMoney(b,a,100);
}
public void transferMoney(Account fromAccount, Account toAccount,double money) {
synchronized (fromAccount) {
System.out.println("线程" + Thread.currentThread().getName() + "得到锁" + fromAccount.getName());
synchronized (toAccount) {
System.out.println("线程" + Thread.currentThread().getName() + "得到锁" + toAccount.getName());
if(fromAccount.getMoney() < money) {
System.out.println("余额不足");
} else {
fromAccount.setMoney(fromAccount.getMoney()-money);
toAccount.setMoney(toAccount.getMoney() + money);
System.out.println("转账后:" + fromAccount.getName() + "有:" + fromAccount.getMoney());
System.out.println("转账后:" + toAccount.getName() + "有:" + toAccount.getMoney());
}
}
}
}
}
class Account{
public Account(String name, double money) {
this.name = name;
this.money = money;
}
private String name;
private double money;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
}
代码有可能会发生死锁:如果两个线程同时调用
transferMoney()
,线程A 从 X账户 向 Y账户 转账,线程B 从 账户Y 向 账户X 转账,那么就会发生死锁
避免死锁
上面
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值 来固定加锁的顺序,这样就不会发生死锁的问题
三、协作对象之间发生死锁
发生条件
A对象 的一个同步方法调用了 B对象 的同步方法。且有,B对象 的一个同步方法调用了 A对象 的一个同步方法。则可能发生:A线程 得到了 A对象 锁,B对象 同时得到了 B对象 锁,都不会释放,且都无法继续执行,就发生了死锁
开放调用避免死锁
在协作对象之间发生死锁中,主要是因为在调用某个方法时就需要持有锁,并且在方法内部也调用了其他带锁的方法;如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用;可以在方法中使用同步代码块来实现同步,调用另一对象的同步方法不放在同步代码块中,就解决了问题
四、定时锁避免死锁
tryLock 方法
使用显式 Lock 锁,在获取锁时使用
tryLock()
方法。当等待超过时限的时候,tryLock()
不会一直等待,而是返回错误信息
tryLock 是防止自锁的一个重要方式
tryLock()
方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待
例子
public class tryLock {
public static void main(String[] args) {
System.out.println("开始");
final Lock lock = new ReentrantLock();
new Thread() {
@Override
public void run() {
String tName = Thread.currentThread().getName();
if (lock.tryLock()) {
System.out.println(tName + "获取到锁!");
} else {
System.out.println(tName + "获取不到锁!");
return;
}
try {
for (int i = 0; i < 5; i++) {
System.out.println(tName + ":" + i);
}
Thread.sleep(5000);
} catch (Exception e) {
System.out.println(tName + "出错了!!!");
} finally {
System.out.println(tName + "释放锁!!");
lock.unlock();
}
}
}.start();
new Thread() {
@Override
public void run() {
String tName = Thread.currentThread().getName();
if (lock.tryLock()) {
System.out.println(tName + "获取到锁!");
} else {
System.out.println(tName + "获取不到锁!");
return;
}
try {
for (int i = 0; i < 5; i++) {
System.out.println(tName + ":" + i);
}
} catch (Exception e) {
System.out.println(tName + "出错了!!!");
} finally {
System.out.println(tName + "释放锁!!");
lock.unlock();
}
}
}.start();
System.out.println("结束");
}
}
五、总结
发生死锁的原因主要由于:
线程之间交错执行
解决: 以固定的顺序加锁
执行某方法时就需要持有锁,且不释放
解决: 缩减同步代码块范围,最好仅操作共享变量时才加锁
永久等待
**解决:**n使用tryLock()
定时锁,超过时限则返回错误信息