1. 什么是死锁?
发生在并发中
互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进导致程序陷入无尽的阻塞,这就是死锁。
如果多个线程之间的依赖关系是环形,存在环路的锁的依赖关系,那么也可能会发生死锁
2. 死锁的影响
死锁的影响在不同系统中是不一样的,这取决于系统对死锁的处理能力
数据库中:检测并放弃事务
JVM中:无法自动处理
几率不高但是危害大
不一定发生,但是遵守“墨菲定律
一旦发生,多是高并发场景,影响用户多
整个系统崩溃、子系统崩溃、性能降低
压力测试无法找出所有潜在的死锁
3. 发生死锁的例子
3.1 最简单的情况
两个线程持有对方的锁
/**
* 最简单的情况
* 线程一获取锁一去拿锁二,但是锁二也在等锁一
*/
public class Deadlock {
private static Object object1 = new Object();
private static Object object2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获取了锁");
synchronized (object2) {
System.out.println(Thread.currentThread().getName() + "获取了锁");
}
}
}
},"线程一");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "获取了锁");
synchronized (object1) {
System.out.println(Thread.currentThread().getName() + "获取了锁");
}
}
}
},"线程二");
t1.start();
t2.start();
}
}
3.2 银行转账
**
* 描述: 转账时候遇到死锁,一旦打开注释,便会发生死锁
*/
public class TransferMoney implements Runnable {
int flag = 1;
static Account a = new Account(500);
static Account b = new Account(500);
public static void main(String[] args) throws InterruptedException {
TransferMoney t1 = new TransferMoney();
TransferMoney t2 = new TransferMoney();
t1.flag = 1;
t2.flag = 0;
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 200);
}
if (flag == 0) {
transferMoney(b, a, 200);
}
}
public void transferMoney(Account from, Account to, int amount) {
synchronized (from) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println("余额不足转账失败!");
}
from.balance -= amount;
from.balance += amount;
System.out.println("成功转账" + amount + "元");
}
}
}
static class Account {
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
3.3 模拟多人随机转账
/**
* 描述: 多人同时转账,依然很危险
*/
public class MultiTransferMoney {
private static final int NUM_ACCOUNTS = 500;
private static final int NUM_MONEY = 1000;
private static final int NUM_ITERATIONS = 1000000;
private static final int NUM_THREADS = 20;
public static void main(String[] args) {
Random rnd = new Random();
TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new TransferMoney.Account(NUM_MONEY);
}
class TransferThread extends Thread {
@Override
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
int toAcct = rnd.nextInt(NUM_ACCOUNTS);
int amount = rnd.nextInt(NUM_MONEY);
TransferMoney.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
}
System.out.println("运行结束");
}
}
for (int i = 0; i < NUM_THREADS; i++) {
new TransferThread().start();
}
}
}
4. 死锁的四个必要条件
- 互斥条件
就是说一个资源每一次只能被同一个进程或者线程使用,比如说我这里是一把锁,那么你拿到锁之后,别的线程的就不能再用这个锁了叫互斥条件,如果说这个资源可以无限共享,那么这个东西就不是互斥的,那么这种情况下,是不会发生死锁的。
- 请求与保持条件
第一个线程去请求第二把锁,我又保持我的第一把锁,请求的时候阻塞了对于我已经获取的那个资源我就保持不变,我也不释放
- 不剥夺条件
是不能有一个外界来干扰,不能有一个外界来剥夺的。所以这就叫做不剥夺条件
- 循环等待条件
两个两个的循环等待,其实就是你等我,我等你
而多个线程的循环等待,就是从头开始一个一个一个最终呢,是头尾相接这么一个等待的关系,这种呢就叫做循环
等待,如果你不是循环等待,也就是说你们之间不构成环路的话,那么其实呢,这并不是死锁,他们是可以解开的
以上的,这四个条件啊,我们看到都是必要条件,它不是说充分条件,它是必要条件,必要条件就是说你这四个必须同时满足缺一不可。
5. 如何定位死锁
5.1 命令行
进入bin目录
查看pid :jstack -lm
运行:jstack [pid]
5.2ThreadMXBean代码
ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
long[] threads = mxBean.findDeadlockedThreads();
if (threads != null && threads.length > 0) {
for (long thread : threads) {
ThreadInfo info = mxBean.getThreadInfo(thread);
System.out.println("发现了死锁:" + info.getThreadName());
}
}
6. 修复死锁
6.1 线上发生死锁应该怎么办?
线上问题都需要防患于未然,不造成损失地扑灭几乎已经是不可能
保存案发现场然后立刻重启服务器
暂时保证线上服务的安全,然后在利用刚才保存的信息,排查死锁,修改代码,重新发版
6.2 死锁避免策略
避免策略:哲学家就餐
的换手方案、转账换序方案
思路:避免相反的获取锁的顺序
比如转账的时候,我获取我的锁,你获取你的锁,这样就发生了死锁。
实际上不在乎获取锁的顺序
代码演示:
通过hashcode来决定获取锁的顺序、冲突时需要“加时赛“
有主键更方便
import 死锁.TransferMoney;
public class TransferMoney1 implements Runnable {
int flag = 1;
static Account a = new Account(500);
static Account b = new Account(500);
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
TransferMoney1 r1 = new TransferMoney1();
TransferMoney1 r2 = new TransferMoney1();
r1.flag = 1;
r2.flag = 0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 200);
}
if (flag == 0) {
transferMoney(b, a, 200);
}
}
public static void transferMoney(Account from, Account to, int amount) {
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功转账" + amount + "元");
}
}
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
new Helper().transfer();
}
}
}
else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}else {
synchronized (lock) {
synchronized (to) {
synchronized (from) {
new Helper().transfer();
}
}
}
}
}
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
}
哲学家就餐–流程
先拿起左手的筷子
然后拿起右手的筷子
如果筷子被人使用了,那就等别人用完
吃完后,把筷子放回原位
/**
* 描述: 演示哲学家就餐问题导致的死锁
*
* 输出:
* 哲学家2号 思考
* 哲学家4号 思考
* 哲学家1号 思考
* 哲学家3号 思考
* 哲学家5号 思考
* 哲学家5号 捡起左边的筷子
* 哲学家2号 捡起左边的筷子
* 哲学家3号 捡起左边的筷子
* 哲学家4号 捡起左边的筷子
* 哲学家1号 捡起左边的筷子
*/
public class DiningPhilosophers {
public static class Philosopher implements Runnable {
private Object leftChopstick;
private Object rightChopstick;
public Philosopher(Object leftChopstick, Object rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
try {
while (true) {
doAction("思考");
synchronized (leftChopstick) {
doAction("捡起左边的筷子");
synchronized (rightChopstick) {
doAction("拿起正确的筷子-吃");
doAction("放下右边的筷子");
}
doAction("放下左边的筷子");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doAction(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
Thread.sleep((long) (Math.random() * 10));
}
}
public static void main(String[] args) {
//初始化数组
Philosopher[] philosophers = new Philosopher[5];
//筷子数组
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopstick = chopsticks[i];
//初始化右边的筷子
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
}
}
}
服务员检查(避免策略)
改变一个哲学家拿叉子的顺序(避免策略)
餐票(避免策略)
//如果是最后一个叫他先那右边的
if (i==chopsticks.length-1){
philosophers[i] = new Philosopher(rightChopstick,leftChopstick);
}else {
philosophers[i] = new Philosopher(leftChopstick, rightChopstick);
}
6.3 死锁检测与恢复策略
检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
领导调节(检测与恢复策略)
死锁检测算法
允许发生死锁
每次调用锁都记录
定期检查锁的调用链路图”中是否存在环路
一旦发生死锁,就用死锁恢复机制进行恢复
恢复方法1:进程终止
逐个终止线程,直到死锁消除。
终止顺序:
优先级(是前台交互还是后台处理)
已占用资源、还需要的资源
已经运行时间
恢复方法2:资源抢占
把已经分发出去的锁给收回来
让线程回退几步,这样就不用结束整个线程,成本比较低
缺点:可能同一个线程一直被抢占,那就造成饥饿
6.4 蛇鸟策略(不推荐)
蛇鸟策略:蛇鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而蛇鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。
7. 实际工程中避免死锁的8个tips
7.1 设置超时时间
使用:Lock的tryLock(long timeout, TimeUnit unit)
因为synchronized不具备尝试锁的能力
造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢
获取锁失败打日志、发报警邮件、重启等
代码演示:
线程有获取了锁1,休眠,然后在休眠的过程中,线程2也获取了锁2,此时线程1获取不到锁2,然后线程1就会释放此时线程2就获取了两把锁,此时线程2释放了,线程1也就可以获取两把锁了。
/**
* 描述:用tryLock来避免死锁
*
*/
public class TryLockDeadlock implements Runnable {
int flag = 1;
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
TryLockDeadlock r1 = new TryLockDeadlock();
TryLockDeadlock r2 = new TryLockDeadlock();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (flag == 1) {
try {
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程1获取到了锁1");
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
System.out.println("线程1获取到了锁2");
System.out.println("线程1成功获取到了两把锁");
lock2.unlock();
lock1.unlock();
break;
} else {
System.out.println("线程1尝试获取锁2失败,已重试");
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程1获取锁1失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (flag == 0) {
try {
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程2获取到了锁2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(3000, TimeUnit.MILLISECONDS)) {
System.out.println("线程2获取到了锁1");
System.out.println("线程2成功获取到了两把锁");
lock1.unlock();
lock2.unlock();
break;
} else {
System.out.println("线程2尝试获取锁1失败,已重试");
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println("线程2获取锁2失败,已重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
7.2 多使用并发类而不是自己设计锁
ConcurrentHashMap, ConcurrentLinkedQueue,AtomicBoolean等
实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高
多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好
并发场景需要用到map,首先想到用ConcurrentHashMap
7.3 尽量降低锁的使用粒度:用不同的锁而不是一个锁
7.4 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
7.5 给你的线程起个有意义的名字: debug和排查时事半功倍,框架和JDK都遵守这个最佳实践
7.6 避免锁的嵌套MustDeadLock类
7.7 分配资源前先看能不能收回来:银行家算法
7.8 尽量不要几个功能用同一把锁:专锁专用
8. 活锁
活锁哲学家问题:在完全相同的时刻进入餐厅,并同时拿起左边的餐叉那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉
虽然线程并没有阻塞,也始终在运行(所以叫做“活”锁,线程是“活”的),但是程序却得不到进展,因为线程始终重复做同样的事
如果这里死锁,那么就是这里两个人都始终一动不动,直到对方先抬头,他们之间不再说话了,只是等待
8.1 活锁演示
/**
* 描述: 演示活锁问题
*/
public class LiveLock {
static class Spoon {
private Diner owner;
public Spoon(Diner owner) {
this.owner = owner;
}
public Diner getOwner() {
return owner;
}
public void setOwner(Diner owner) {
this.owner = owner;
}
public synchronized void use() {
System.out.printf("%s吃完了!", owner.name);
}
}
static class Diner {
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
isHungry = true;
}
public void eatWith(Spoon spoon, Diner spouse) {
while (isHungry) {
if (spoon.owner != this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
// Random random = new Random();
if (spouse.isHungry /*&& random.nextInt(10) < 9*/) {
System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");
spoon.setOwner(spouse);
continue;
}
spoon.use();
isHungry = false;
System.out.println(name + ": 我吃完了");
spoon.setOwner(spouse);
}
}
}
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("织女");
Spoon spoon = new Spoon(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.eatWith(spoon, wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon, husband);
}
}).start();
}
}
8.2 解决活锁问题
原因:重试机制不变,消息队列始终重试,吃饭始终谦让
以太网的指数退避算法
加入随机因素解决
8.3 工程中的活锁实例:消息队列
策略:消息如果处理失败,就放在队列开头重试
由于依赖服务出了问题处理该消息一直失败
没阻塞,但程序无法继续
解决:放到队列尾部、重试限制
9. 饥饿
当线程需要某些资源(例如CPU),但是却始终得不到
线程的优先级设置得过于低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁
饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算宣染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差
10. 常见面试问题
1.写一个必然死锁的例子(我面百度的时候考过),生产中什么场景下会发生死锁?
在一个方法中获取多个锁,这种是比较容易发生死锁的,但是也有不明显的情况,比如说我们不是在一个方法中获取两个锁,而是在一个方法中先获取一个锁,再去调用其他的方法,但是在其他的方法中确实又获取到其他的锁了,这样一来呢相当于一个循环调用,也形成了锁的链路,也可能会造成死锁,而这些情况呢,在我们实际生产中,比如说库存的增减,比如说金钱的转移都问题。
2.发生死锁必须满足哪些条件?
一定要满足这四个条件,第一个互斥条件,我们这个资源同时只能被一个线程或者进程使用
请求与保持条件,我们这个线程请求另外的一个锁,并且手中持有一个锁,而且它还不放弃。
不剥夺条件,就是不能被剥夺我们这些线程之间,自己持有资源,但是你又不能把我的直接抢走,这样一来才有可能会陷入死锁
循环等待条件,是在多个线程中体现为只有构成环路才有可能会发生死索,而两个现场之间,它的循环等待,意味着就是你等我我等你
3.如何定位死锁?
jstack
ThreadMXBean
4.有哪些解决死锁问题的策略?
避免策略:哲学家就餐的换手方案、转账换序方案
检测与恢复策略:一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
蛇鸟策略:蛇鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而蛇鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。
5.讲一讲经典的哲学家就餐问题
服务员检查(避免策略)
改变一个哲学家拿叉子的顺序(避免策略)
餐票(避免策略)
领导调节(检测与恢复策略)
6.实际工程中如何避免死锁?
8点
7.什么是活跃性问题?活锁、饥饿和死锁有什么区别?
原文地址:http://www.cnblogs.com/mrwyk/p/16889126.html