【JavaEE】死锁是什么?如何避免死锁(保姆级讲解)

  • 博主简介:想进大厂的打工人
  • 博主主页:@xyk:
  • 所属专栏: JavaEE初阶

本篇文章将介绍什么是死锁,死锁的四大必要条件,如何去避免死锁~~~ 


目录

一、死锁是什么?

二、关于死锁的情况

2.1 一个线程的情况

2.2 两个线程的情况

2.2.1 形如这样的代码:

2.2.2 吃饺子

2.3 N个线程的情况

2.4 加锁代码

 三、如何避免死锁

3.1 死锁产生的四个必要条件

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。其中最容易破坏的就是 "循环等待".

3.2 破坏循环等待:

四、相关面试题

4.1 谈谈 volatile关键字的用法?

4.2 Java多线程是如何实现数据共享的?

4.3 Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?

4.4 Java线程共有几种状态?状态之间怎么切换的?

4.5 在多线程下,如果对一个数进行叠加,该怎么做?

4.6 Servlet是否是线程安全的?

4.7 Thread和Runnable的区别和联系?

4.8 多次start一个线程会怎么样

4.9 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么? 

4.10 进程和线程的区别?


一、死锁是什么?

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
简单来说,死锁,就是没人能解锁了~~

举例:

我有个女神,我向女神表白了:女神I love you,她欣然同意,我就加锁成功了~~接下来,我换个微信小号,再偷偷加她一把,再在小号上也表白~~此时第二次尝试加锁,就不会成功了,我就只能阻塞等待~~

形如这样的代码:

 就是加锁俩次的情况,第二次尝试加锁,需要等待第一个锁释放;第一个锁释放,需要等待第二个锁加锁成功;

逻辑上矛盾了!!! ==>这就是死锁~~

二、关于死锁的情况

2.1 一个线程的情况

一个线程,一把锁(上面的情况),可重入锁没事,不可重入锁死锁;

 

2.2 两个线程的情况

两个线程,两把锁,即使是可重入锁,也会死锁~~

2.2.1 形如这样的代码:

 t1和t2线程在互相抢占锁,t1拥有了locker1再去竞争locker2,t2拥有了locker2再去竞争locker1,这样就形成了死锁~~

2.2.2 吃饺子

滑稽老哥和女神一起去饺子馆吃饺子,吃饺子需要酱油和醋.
滑稽老哥抄起了酱油瓶,女神抄起了醋瓶.
滑稽: 你先把醋瓶给我,我用完了就把酱油瓶给你.
女神: 你先把酱油瓶给我,我用完了就把醋瓶给你.
如果这俩人彼此之间互不相让,就构成了死锁.
酱油和醋相当于是两把锁,这两个人就是两个线程.

2.3 N个线程的情况

此时有N个线程,M把锁,线程数量和锁数量更多了,就更容易死锁了!!

比如哲学家,就餐问题:

 这五个哲学家:

1.随机的进行 吃面条(拿起筷子)和思考人生(放下筷子)

2.固执的,如果他想拿筷子,被别人占用了,就会等待;等的过程中不会放下手里已经拿到的筷子~~

假设这五个哲学家,同时拿起左手边的筷子!!!就死锁了!!

 

死锁是一种严重的 BUG!! 导致一个程序的线程 "卡死", 无法正常工作!!

那么实践中,如何去避免出现死锁呢??一个简单有效的方法就是,破解循环等待这个条件:

针对锁进行编号,如果需要同时获取多把锁,约定加锁顺序,必须是先对小的编号加锁,后对大的编号加锁~~~

约定,获取多把锁,先获取小的,后获取大的!!

只要约定了加锁顺序,循环等待自然破除,死锁就不会形成了!!

此时,5号老铁就可以先拿起两支筷子进行吃面,吃完了放下筷子,下一位老铁继续进行拿起筷子吃面,依次进行到最后一位老铁,就不会形成死锁~~

2.4 加锁代码

还是上面的代码,进行加锁顺序调整,就可以避免死锁了

 两个线程t1,t2都先对locker1进行加锁,就可以避免死锁了~~

 三、如何避免死锁

3.1 死锁产生的四个必要条件

死锁产生的四个必要条件:

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。其中最容易破坏的就是 "循环等待".


3.2 破坏循环等待:

最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3...M)
N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待
可能产生环路等待的代码:

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (lock2) {
synchronized (lock1) {
// do something...
}
}
}
};
t2.start();

不会产生环路等待的代码:
约定好先获取 lock1, 再获取 lock2 , 就不会环路等待

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t2.start();

四、相关面试题

4.1 谈谈 volatile关键字的用法?

volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰
的变量, 可以第一时间读取到最新的值.

4.2 Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:
方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到


4.3 Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?

创建线程池主要有两种方式:

  • 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
  • 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.
     

LinkedBlockingQueue 表示线程池的任务队列. 用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务

4.4 Java线程共有几种状态?状态之间怎么切换的?

  • NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.
  • BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
  • WAITING: 调用 wait 方法会进入该状态.
  • TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
  • TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态
     

4.5 在多线程下,如果对一个数进行叠加,该怎么做?

使用 synchronized / ReentrantLock 加锁
使用 AtomInteger 原子操作.


4.6 Servlet是否是线程安全的?

Servlet 本身是工作在多线程环境下.
如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行
操作, 是可能出现线程不安全的情况的.


4.7 Thread和Runnable的区别和联系?

Thread 类描述了一个线程.
Runnable 描述了一个任务.
在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用
Runnable 来描述这个任务


4.8 多次start一个线程会怎么样

第一次调用 start 可以成功调用
后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常


4.9 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么? 

synchronized 加在非静态方法上, 相当于针对当前对象加锁.
如果这两个方法属于同一个实例:
线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到
锁之后才能执行方法内容.
如果这两个方法属于不同实例:
两者能并发执行, 互不干扰.

4.10 进程和线程的区别?


进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值