一.什么是死锁
在多线程并发中两个或多个线程互相持有对方所需要的资源,不主动释放,在没有外力干预的情况下,所有人都无法继续前进,导致程序陷入无尽的阻塞,就是死锁
比如下图,线程A 线程B都尝试获取对方持有的锁,但是又不释放自己所持有的锁就会陷入死锁
如果多个线程间的依赖关系是环形,存在环路的锁依赖关系,那么也有可能会发生死锁
死锁一旦发生可能会造成系统崩溃,在高并发场景中,影响大量用户,压力测试无法找出所有潜在的死锁.
二.发生死锁的例子
例子1 必定会发生死锁的例子
/**
* @author: xuxu
* @date 2020/2/2 20:01
* @Description: 必定会发生死锁的情况
*/
public class MustDeadLock implements Runnable{
int flag = 1;
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) {
MustDeadLock r1 = new MustDeadLock();
MustDeadLock r2 = new MustDeadLock();
r1.flag=1;
r2.flag=0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
@Override
public void run() {
System.out.println("flag:"+flag);
if(flag==1){
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("获取锁");
}
}
}else if (flag==0){
synchronized (lock2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1){
System.out.println("获取锁");
}
}
}
}
}
此时线程1获取lock1并没有立刻释放,线程2获取lock2后也没有立即释放,他们在等待1秒后,分别想获取对方未释放的锁,发生死锁.
例子2 两个账户之间转账
/**
* @author: xuxu
* @date 2020/2/2 21:28
* @Description: 模拟转账遇到死锁 解除屏蔽的休眠代码会发生死锁现象
*/
public class TransferMoney implements Runnable{
int flag = 0;
public static Account a = new Account(500);
public static Account b = new Account(500);
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
r1.flag=0;
TransferMoney r2 = new TransferMoney();
r2.flag=1;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
System.out.println("转账开始");
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==0){
transferMoney(a,b,200);
}else if(flag==1){
transferMoney(b,a,200);
}
}
/**
* 转账方法
* @param from
* @param to
* @param amount
*/
public void transferMoney(Account from,Account to,int amount){
synchronized (from){
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
synchronized (to){
if(from.balance<amount){
System.out.println("余额不足");
}
from.balance = from.balance - amount;
to.balance = to.balance + amount;
System.out.println("转账成功");
}
}
}
/**
* 账号类
*/
static class Account{
//余额
private int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
转账时,一个线程控制A账户给B账户转账,我们先通过synchronized锁A再请求B的锁,,同时另一个线程控制B账户向A账户也转账,通过synchronized锁B再请求A的锁,这样如果在两个线程获取第二个锁之间加入休眠,会发生死锁的情况
例子3 多人转账死锁
/**
* @author: xuxu
* @date 2020/2/2 22:17
* @Description: 多人多次转账遇到死锁问题 账户数量越多越不容易死锁,但是依然会死锁
*/
public class MultiTransferMoney {
//账户数量
private static final int ACCOUNT_COUNT = 500;
//转账次数
private static final int NUMBEROFTRANSFER = 1000000;
//初始账户金额
private static final int ACCOUNT_INIT_BALANCE = 10000;
//线程数量
private static final int THREAD_NUM = 20;
//
private static AtomicInteger atomicInteger = new AtomicInteger();
/**
* 账户类
*/
static class Account{
int balance;
public Account(int balance) {
this.balance = balance;
}
}
/**
* 转账方法
* @param from
* @param to
* @param amount
*/
public static void transfer(Account from,Account to,int amount){
synchronized(from){
synchronized(to){
if(from.balance<amount){
System.out.println("余额不足");
}
from.balance = from.balance-amount;
to.balance = to.balance + amount;
System.out.println("转账成功");
}
}
}
public static void main(String[] args) {
Random r = new Random();
Account[] accounts = new Account[ACCOUNT_COUNT];
//初始化每个账户
for (int i=0;i<accounts.length;i++){
accounts[i]=new Account(ACCOUNT_INIT_BALANCE);
}
class TransferThread extends Thread{
@Override
public void run() {
// synchronized (object){
for(int i=0;i<NUMBEROFTRANSFER;i++){
System.out.println(atomicInteger.incrementAndGet());
int fromIndex =r.nextInt(ACCOUNT_COUNT);
int toIndex = r.nextInt(ACCOUNT_COUNT);
int amount = r.nextInt(ACCOUNT_INIT_BALANCE);
MultiTransferMoney.transfer(accounts[fromIndex],accounts[toIndex] ,amount );
}
// }
}
}
for(int i=0;i<THREAD_NUM;i++){
TransferThread t = new TransferThread();
t.start();
}
}
}
例子4 哲学家就餐问题 这是个经典案例
如图
假设有5个哲学家除了思考就是吃饭,但是桌上只有5根筷子.但是他们就餐的流程是
先拿起左手的筷子,再拿起右手的筷子,吃饭,如果筷子被人用了就需要等待别人用完,这里我们转换成代码
/**
* @author: xuxu
* @date 2020/2/3 11:42
* @Description: 哲学家就餐产生死锁问题
* 哲学家和筷子数量一致,每人一根筷子,但是吃饭需要两根筷子,产生互相等待筷子的现象
*/
public class DiningForPhilosophers {
/**
* 哲学家类
*/
public static class Philosophers implements Runnable{
private Object leftChopsticks;
private Object rightChopsticks;
public Philosophers(Object leftChopsticks, Object rightChopsticks) {
this.leftChopsticks = leftChopsticks;
this.rightChopsticks = rightChopsticks;
}
@Override
public void run() {
try {
while(true){
action("思考......");
synchronized (leftChopsticks){
action("拿起左边的筷子");
synchronized (rightChopsticks){
action("拿起右边的筷子---吃饭");
action("放下右边的筷子");
}
action("放下左边的筷子");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void action(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"-->"+action);
//随机休眠,代表这个行为执行的耗时
Thread.sleep((long) (Math.random()*10));
}
};
public static void main(String[] args) {
Philosophers[] philosophers = new Philosophers[5];
Object[] chopsticks = new Object[philosophers.length];
//初始化筷子对象
for (int i=0;i<chopsticks.length;i++){
chopsticks[i] = new Object();
}
//初始化哲学家对象,并启动线程.
for (int i=0;i<chopsticks.length;i++){
Object leftChopsticks = chopsticks[i];
Object rightChopsticks = chopsticks[(i+1)%chopsticks.length];
philosophers[i] = new Philosophers(leftChopsticks, rightChopsticks);
new Thread(philosophers[i],"哲学家"+i+"号").start();
}
}
}
同样发生死锁,这里大家陷入了一种循环等待的状态,0号获取了他左边的0号筷子,请求他右边的1号筷子,1号获取了左边的1号筷子,请求等待他右边的2号筷子...........5号获取了他左边的5号筷子等待他右手边的0号筷子........这样就形成了一个环.
三.出现死锁后,如何定位死锁(3种方式)
1)使用jstack工具
首先使用jps定位该进程的pid
然后进入jdk bin目录所在的路径 运行 jstack.exe 加 PID
结果如图
2)使用idea编译器Dump Threads功能打印出上述信息
3)使用ThreadMXBean工具 代码中定位
**
* @author: xuxu
* @date 2020/2/2 20:01
* @Description: 使用ThreadMxBean发现死锁
*/
public class UseThreadMxBeanFindDeadLock implements Runnable{
int flag = 1;
public static Object lock1 = new Object();
public static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
UseThreadMxBeanFindDeadLock r1 = new UseThreadMxBeanFindDeadLock();
UseThreadMxBeanFindDeadLock r2 = new UseThreadMxBeanFindDeadLock();
r1.flag=1;
r2.flag=0;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
//休眠一秒 让死锁出现,这里不能用join 因为陷入死锁后两个线程都被阻塞 永远不会执行后面的代码
Thread.sleep(1000);
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if(deadlockedThreads!=null && deadlockedThreads.length>0){
for (int i=0;i<deadlockedThreads.length;i++){
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.println("发现死锁"+threadInfo.getThreadName());
}
}
}
@Override
public void run() {
System.out.println("flag:"+flag);
if(flag==1){
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println("获取锁");
}
}
}else if (flag==0){
synchronized (lock2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1){
System.out.println("获取锁");
}
}
}
}
}
四.死锁产生的四大必要条件
互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
比如下图中同步锁,同一时间只能有一个线程获取锁,其他的线程无法获取该锁
不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
lock1在使用完毕前,不能被其他的进程夺取该锁,只能由获取lock1的线程自己释放.
请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
线程在请求lock2的时候并没有释放lock1 依然保持着lock1的锁
循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。
比如上述例子中 线程1 -->线程2 ---->线程1 循环等待
以上给出了导致死锁的四个必要条件,只要系统发生死锁则以上四个条件至少有一个成立。事实上循环等待的成立蕴含了前三个条件的成立,似乎没有必要列出然而考虑这些条件对死锁的预防是有利的,因为可以通过破坏四个条件中的任何一个来预防死锁的发生。
五.线上发生死锁怎么办
一旦发生死锁,不造成损失已经是不可能的了,我们应该保存案发现场,如日志后,立刻重启服务器,暂时保证线上服务的安全,然后利用刚刚保存的信息定位死锁,修改代码,重新发布
六.常见的死锁修复策略
(1)避免策略 在程序中就避免死锁 推荐
我们发现上面几个例子都是因为多个线程间获取锁的顺序相反导致这个问题.但是实际上业务上并不在乎获取锁的顺序问题.
例子1:修改两人转账时获取锁的顺序
/**
* @author: xuxu
* @date 2020/2/2 21:28
* @Description: 模拟转账遇到死锁 修改获取锁的顺序避免死锁
*/
public class TransferMoneyFix implements Runnable {
int flag = 0;
public static Account a = new Account(500);
public static Account b = new Account(500);
public static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
TransferMoneyFix r1 = new TransferMoneyFix();
r1.flag = 0;
TransferMoneyFix r2 = new TransferMoneyFix();
r2.flag = 1;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
System.out.println("转账开始");
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 == 0) {
transferMoney(a, b, 200);
} else if (flag == 1) {
transferMoney(b, a, 200);
}
}
/**
* 转账方法
*
* @param from
* @param to
* @param amount
*/
public void transferMoney(Account from, Account to, int amount) {
int fromHash = from.hashCode();
int toHash = to.hashCode();
//通过hash值判断 始终小的锁先获取 再获取大的锁.
// 这样不管是A转B 还是B转A 都会先获取小的锁,再获取大的锁,保证了顺序性
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
if (from.balance < amount) {
System.out.println("余额不足");
}
from.balance = from.balance - amount;
to.balance = to.balance + amount;
System.out.println("转账成功");
}
}
} else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
if (from.balance < amount) {
System.out.println("余额不足");
}
from.balance = from.balance - amount;
to.balance = to.balance + amount;
System.out.println("转账成功");
}
}
}else{
//避免hash值一样的情况,实际生产中可以用主键来代替hash值就不会hash碰撞
//通过在外层再加一把锁,谁先抢到锁谁就先执行.
synchronized (lock){
synchronized (to) {
synchronized (from) {
if (from.balance < amount) {
System.out.println("余额不足");
}
from.balance = from.balance - amount;
to.balance = to.balance + amount;
System.out.println("转账成功");
}
}
}
}
}
/**
* 账号类
*/
static class Account {
//余额
private int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
处理思路是通过保证两个线程获取锁的顺序一致来解决,比如通过hash值去比较A,B的大小(不考虑有主键的情况下),然后保证始终先获取hash值小的锁 再获取大的.来保证顺序不会相反.
但是如果hash值碰撞的情况下就需要再加一层锁 让他们随机去抢如下
这个时候我们思考一下
如果直接不判断,全部都在外面多加一层锁,不是也可以避免死锁问题么.是的 但是这样也会导致性能问题.具体见下图.
例子2:哲学家换手解决.改变一个哲学家拿筷子的顺序.
哲学家就餐的时候之所以会发生死锁是因为大家都拿起了左边的筷子,等着右边的筷子,所以都一直等待.
这个时候假设第5个哲学家 先拿右手的0号筷子,等待左手的5号筷子,由于0号筷子被1号哲学家已经获取,5号哲学家获取不到0号筷子,也不会去拿左手的5号筷子,这个时候4号哲学家就可以获取右手的5号筷子,执行完后让出左右手的4,5号筷子,以此类推,大家都可以拿到筷子,就如同转账一样我们打破了循环等待的条件也就是这个顺序.就避免了死锁 改动代码如下
public class DiningForPhilosophersFix {
/**
* 哲学家类
*/
public static class Philosophers implements Runnable{
private Object leftChopsticks;
private Object rightChopsticks;
public Philosophers(Object leftChopsticks, Object rightChopsticks) {
this.leftChopsticks = leftChopsticks;
this.rightChopsticks = rightChopsticks;
}
@Override
public void run() {
try {
while(true){
action("思考......");
synchronized (leftChopsticks){
action("拿起左边的筷子");
synchronized (rightChopsticks){
action("拿起右边的筷子---吃饭");
action("放下右边的筷子");
}
action("放下左边的筷子");
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void action(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"-->"+action);
//随机休眠,代表这个行为执行的耗时
Thread.sleep((long) (Math.random()*10));
}
};
public static void main(String[] args) {
Philosophers[] philosophers = new Philosophers[5];
Object[] chopsticks = new Object[philosophers.length];
//初始化筷子对象
for (int i=0;i<chopsticks.length;i++){
chopsticks[i] = new Object();
}
//初始化哲学家对象,并启动线程.
for (int i=0;i<chopsticks.length;i++){
Object leftChopsticks = chopsticks[i];
Object rightChopsticks = chopsticks[(i+1)%chopsticks.length];
//最后一个哲学家
if(i==(chopsticks.length-1)){
//先拿右手再拿左手
philosophers[i] = new Philosophers(rightChopsticks,leftChopsticks);
}else{
philosophers[i] = new Philosophers(leftChopsticks, rightChopsticks);
}
new Thread(philosophers[i],"哲学家"+i+"号").start();
}
}
}
我们改动了
这样就成功的避免了死锁
(2)检测和恢复策略 允许死锁的发生,但是发生死锁后要记录下来并通过停止线程或其他方式停止死锁
一段时间检测是否有死锁,如果有就剥夺某个资源,来解除死锁
(3)鸵鸟策略 默认 死锁发生的几率特别小,等死锁发生了,再去处理修改
七.生产环境中避免死锁的经验
1.设置超时时间
使用lock锁替换synchronized锁
造成超时的可能性很多,发生了死锁,线程陷入了死循环,线程执行很慢.
当获取锁失败的时候:我们可以记录日志,发报警邮件,执行重启脚本等操作避免线上事故.
/**
* @author: xuxu
* @date 2020/2/3 15:59
* @Description: 利用trylock 超时时间解决死锁问题
*/
public class TryLockResolveDeadLock implements Runnable{
int flag=0;
//两把锁
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
@Override
public void run() {
while (true){
if(flag==0){
try {
if(lock1.tryLock(1000, TimeUnit.MILLISECONDS)){
System.out.println("线程1获取了lock1");
if(lock2.tryLock(1000, TimeUnit.MILLISECONDS)){
System.out.println("线程1获取两把锁成功");
lock2.unlock();
lock1.unlock();
break;
}else{
System.out.println("线程1获取lock2失败,重试");
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
}else{
System.out.println("线程1获取lock1失败,正在重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}else if(flag==1){
try {
if(lock2.tryLock(1000, TimeUnit.MILLISECONDS)){
System.out.println("线程2获取了lock2");
if(lock1.tryLock(1000, TimeUnit.MILLISECONDS)){
System.out.println("线程2获取两把锁成功");
lock1.unlock();
lock2.unlock();
break;
}else{
lock2.unlock();
System.out.println("线程2获取lock1失败,重试");
Thread.sleep(new Random().nextInt(1000));
}
}else{
System.out.println("线程2获取lock2失败,正在重试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TryLockResolveDeadLock r1 = new TryLockResolveDeadLock();
TryLockResolveDeadLock r2 = new TryLockResolveDeadLock();
r1.flag=0;
r2.flag=1;
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
2.多使用并发类而不是自己设计锁
如ConcurrentHashMap java.util.concurrent.atomic.
3.尽量降低锁的使用粒度:用不同的锁而不是一个锁
4.如果能使用同步代码块,就不使用同步方法:自己指定锁对象
缩小了同步范围,可以自己指定锁对象
5.给线程指定有意义的名字,方便后期debug和排查
6.避免锁的嵌套,锁的嵌套尤其容易造成死锁
7.不要几个功能用同一把锁:专锁专用