死锁出现的原因
多线程安全出现的问题:
1.操作系统是随机调度的,抢占式执行
2.代码结构内,多个线程同时修改同一个变量
禁止变量修改是以一种解决线程安全问题的思路
Java中这种思路的普适性不是很高
3.修改的操作并不是"原子性"
通过加锁把非原子性的修改操作打包成一个整体,变成原子操作
4.内存可见性
5.指令重排序
对于问题三引入了synchronized关键字进行加锁
()指定锁对象
{}放进去的就是要打包成一个整体的代码
进入代码开机会针对锁对象加锁
除了代码块(),return,break针对锁对象解锁
具体synchronized使用方法见上一篇文章
死锁出现的场景
1)一个线程一把锁,这个线程针对这把锁连续加锁两次
这种情况下,代码示例没有出现真正的死锁,因为synchronized针对这种情况做出了特殊的处理
"synchronized"是"可重入锁"
Java可重入锁的内部包括"线程持有者"和"计数器"两个信息
线程持有者:如果某个线程加锁的时候发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并且让计数器自增
解锁的时候计数递减为0,才真正的释放锁(才能被被别的线程获取)
2.两个线程两把锁
死锁代码如下:
private static Object Locker1=new Object();private static Object Locker2=new Object();public static void main(String[] args) {Thread thread1=new Thread(()-> {synchronized(Locker1){for(int i=0;i<50000;i++){}synchronized(Locker2){}}});Thread thread2=new Thread(()->{synchronized(Locker2){for(int i=0;i<50000;i++){//需要有东西在第一把锁里}synchronized(Locker1){}}});thread1.start();thread2.start();}
线程1针对locker1加锁,线程针对locker2加锁
线程1不释放locker1的情况下再针对locker2加锁
同时线程2在不释放locker2的情况下再对locker1加锁
两个内部锁都没执行到,说明这两个现场第二次synchronized的时候阻塞了
死锁的场景3
3.N个线程M个锁
多个线程形成环形依赖,每个线程持有部分资源并等待下一个线程的资源。
public class CircularDependencyDeadlock {private static final Object lockA = new Object();private static final Object lockB = new Object();private static final Object lockC = new Object();public static void main(String[] args) {// 线程1:持有 lockA,需要 lockBThread thread1 = new Thread(() -> {synchronized (lockA) {System.out.println("Thread1 持有 lockA");try {Thread.sleep(100); // 确保其他线程能获取到各自的锁} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println("Thread1 获取到 lockB");}}});// 线程2:持有 lockB,需要 lockCThread thread2 = new Thread(() -> {synchronized (lockB) {System.out.println("Thread2 持有 lockB");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockC) {System.out.println("Thread2 获取到 lockC");}}});// 线程3:持有 lockC,需要 lockAThread thread3 = new Thread(() -> {synchronized (lockC) {System.out.println("Thread3 持有 lockC");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockA) {System.out.println("Thread3 获取到 lockA");}}});// 启动所有线程thread1.start();thread2.start();thread3.start();}
}
线程1:持有锁A,等待锁B
线程2:持有锁B,等待锁C
线程3:持有锁C,等待锁A
三者形成环形依赖链,彼此无法继续执行。
死锁形成的四个必要条件(缺一不可)
1.锁是互斥[基本特性]:资源(锁,文件,设备)一次只能被一个线程独占使用
2.锁是不可被抢占的[基本特性]:线程1拿到了锁A,如果线程1不释放A,线程2无法拿到锁A
3.请求和保持
Thread thread1=new Thread(()-> {synchronized(Locker1){for(int i=0;i<50000;i++){}synchronized(Locker2){}}});Thread thread2=new Thread(()->{synchronized(Locker2){for(int i=0;i<50000;i++){//需要有东西在第一把锁里}synchronized(Locker1){}}});thread1.start();thread2.start();
保持:没有释放锁的情况下
请求:获取其他的锁
解决方法:获取其他锁前,先释放自己的锁(避免锁嵌套)
4.循环等待/循环依赖/环路等待
多个线程形成环形依赖,每个线程持有部分资源并等待下一个线程的资源。造成阻塞
解决办法:给锁编号(1,2,3,4......n)约定所有线程在加锁的时候,按照一定的顺序进行加锁.(约定加锁顺序)
死锁话题下还有一个解决方案,银行家算法
银行家算法过于复杂,日常开发中实现一套银行家算法费时费力,哪怕实现也许银行家算法本身就有bug,所以日常开发中不会使用这种方法来解决死锁问题