关于死锁的详谈

死锁是什么

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

举个栗子理解死锁:

光头强和女神一起去饺子馆吃饺子. 吃饺子需要酱油和醋,光头强抄起了酱油瓶, 女神抄起了醋瓶.

光头强: 你先把醋瓶给我, 我用完了就把酱油瓶给你.

光头强: 你先把酱油瓶给我, 我用完了就把醋瓶给你.

如果这俩人彼此之间互不相让, 就构成了死锁.酱油和醋相当于是两把锁, 这两个人就是两个线程.

为了进一步阐述死锁的形成, 很多资料上也会谈论到 "哲学家就餐问题".

        有个桌子, 围着一圈 哲 ♂ 家, 桌子中间放着一盘意大利面. 每个哲学家两两之间, 放着一根筷子. 每个 哲 ♂ 家 只做两件事: 思考人生 或者 吃面条. 思考人生的时候就会放下筷子. 吃面条就会拿起左 右两边的筷子(先拿起左边, 再拿起右边). 如果 哲 ♂ 家 发现筷子拿不起来了(被别人占用了), 就会阻塞等待. [关键点在这] 假设同一时刻, 五个 哲 ♂ 家 同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会 发现右手的筷子都被占用了. 由于 哲 ♂ 家 们互不相让, 这个时候就形成了 死锁。

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

如何避免死锁

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

        互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用

        不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

        请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

        循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

        当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

其中最容易破坏的就是 "循环等待".

        两个线程对于加锁的顺序没有约定,就可能会产生环路等待:

public class Main {
    public static void main(String[] args) {
        // 假设 apologize1 是 1 号, apologize2 是 2 号, 约定 1号先道歉, 后 2号道歉.
        Object apologize1 = new Object();
        Object apologize2 = new Object();

        Thread student1 = new Thread(() -> {
            synchronized (apologize1) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (apologize2) {
                    System.out.println("student1接受了别人的道歉并且自己也道歉了");
                }
            }
        });
        Thread student2 = new Thread(() -> {
            synchronized (apologize2) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (apologize1) {
                    System.out.println("student2接受了别人的道歉并且自己也道歉了");
                }
            }
        });

        student1.start();
        student2.start();
    }
}

         约定好先获取 lock1,再获取 lock2,就不会环路等待:

Thread student2 = new Thread(() -> {
            synchronized (apologize1) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (apologize2) {
                    System.out.println("student2接受了别人的道歉并且自己也道歉了");
                }
            }
        });

破坏循环等待

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

面试题

1) 谈谈 volatile关键字的用法?

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

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

JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器.

        其中堆区这个内存区域是多个线程之间共享的. 只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.

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

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

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

        通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.

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

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

        NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.

        RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.

        BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.

        WAITING: 调用 wait 方法会进入该状态.

        TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.

        TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.

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

        使用 synchronized / ReentrantLock 加锁

        使用 AtomInteger 原子操作.

6) Servlet是否是线程安全的?

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

7) Thread和Runnable的区别和联系?

        Thread 类描述了一个线程. Runnable 描述了一个任务.

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

8) 多次start一个线程会怎么样

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

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

synchronized 加在非静态方法上, 相当于针对当前对象加锁.

如果这两个方法属于同一个实例:

线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容.

如果这两个方法属于不同实例:

两者能并发执行, 互不干扰.

10) 进程和线程的区别?

        进程是包含线程的. 每个进程至少有一个线程存在,即主线程。

        进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.

        进程是系统分配资源的最小单位,线程是系统调度的最小单位。

  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
是指两个或多个进程在互斥地请求资源的时候,由于资源被占用而无法继续执行,导致所有进程都被阻塞的情况。在Spring Boot中,可能发生在多线程并发访问共享资源时,例如数据库连接池、缓存、等。 在给出解决方案之前,我们需要先排查问题。一种常用的排查问题的方法是使用jstack命令输出线程的堆栈信息。通过查看堆栈信息,我们可以定位到可能引起的代码行,并进行解决修复。 下面是一个示例代码,模拟了一个可能导致的场景: ```java package com.xz.springboottest.day1; public class DeadLock { private static final String ACTION_ONE = "拿起碗"; private static final String ACTION_TWO = "拿起筷子"; public static void main(String[] args) { // 哲学家小明 new Thread(() -> { synchronized (ACTION_ONE) { try { Thread.sleep(1000); synchronized (ACTION_TWO) { System.out.println("小明开始吃饭了。。。。。。"); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 哲学家小张 new Thread(() -> { synchronized (ACTION_TWO) { try { Thread.sleep(1000); synchronized (ACTION_ONE) { System.out.println("小张开始吃饭了。。。。。。"); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } } ``` 以上代码模拟了哲学家就餐的场景,当只剩下一只碗和一双筷子时,可能出现的问题。 对于解决的方法,可以考虑以下几种方案: 1. 避免循环等待:为了避免,可以规定所有线程在请求资源时按照固定的顺序获取,从而避免循环等待。 2. 加顺序:在多个线程同时请求多个资源的情况下,为了避免,可以约定线程必须按照相同的顺序请求资源。 3. 设置超时时间:在获取的时候设置超时时间,如果超过一定时间还未获取到,可以放弃或者重试。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值