• 死锁和多线程常见问题


    一、死锁

    1.1 死锁是什么

    死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
    通俗讲就是,尝试加锁的时候发现上次锁没有及时释放(因为一些原因,bug),导致加锁加不上
    【死锁的常见场景】

    • 一个线程一把锁
      线程A针对锁1连续加锁两次,如果是不可重入锁,就死锁了
    • 两个线程两把锁
      无论锁是不是可重入锁都有可能导致死锁
      举例:
      滑稽老哥和女神一起去饺子馆吃饺子. 吃饺子需要酱油和醋. 滑稽老哥抄起了酱油瓶,女神抄起了醋瓶.
      滑稽: 你先把醋瓶给我, 我用完了就把酱油瓶给你.
      女神: 你先把酱油瓶给我,我用完了就把醋瓶给你.
      如果这俩人彼此之间互不相让, 就构成了死锁. 酱油和醋相当于是两把锁,这两个人就是两个线程.
    • N个线程M把锁 经典案例:哲学家进餐问题

    1.2 哲学家进餐问题

    【哲学家进餐问题】

    • 有个桌子, 围着一圈 哲 ♂ 家, 桌子中间放着一盘意大利面. 每个哲学家两两之间, 放着一根筷子 在这里插入图片描述
    • 每个 哲 ♂ 家 只做两件事: 思考人生 或者 吃面条. 思考人生的时候就会放下筷子. 吃面条就会拿起左右两边的筷子(先拿起左边, 再拿起右边).
    • 如果 哲 ♂ 家 发现筷子拿不起来了(被别人占用了), 就会阻塞等待. 在这里插入图片描述
    • 假设同一时刻, 五个 哲 ♂ 家 同时拿起左手边的筷子, 然后再尝试拿右手的筷子, 就会发现右手的筷子都被占用了. 由于 哲 ♂ 家 们互不相让, 这个时候就形成了 死锁
      +死锁是一种严重的
      BUG!! 导致一个程序的线程 “卡死”, 无法正常工作!

    1.3 产生死锁的必要条件

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

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

    当上述四个条件都成立的时候,便形成死锁。

    1.4 如何避免死锁

    死锁的情况下如果打破上述任何一个条件,便可让死锁消失。其中最容易破坏的就是 “循环等待”.

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

    • 对锁进行编号 在这里插入图片描述

    • 每个哲学家都先拿起序号小的那只筷子,只有拿起了小序号的筷子才能继续拿大序号的筷子 在这里插入图片描述

    • 接下来,4号哲学家就能顺利拿起5号筷子,吃完后放下4和5两只筷子,接下来3、2、1和5号哲学家都能顺利进餐,不会产生死锁

    锁排序,是避免死锁问题的一种方法,还有"银行家算法"也能避免死锁(但这种方法很复杂,不建议在面试中回答)

    1.5 相关面试题

    谈谈死锁?

    1. 一句话概括
    2. 产生死锁的三个典型场景
    3. 死锁的必要条件
    4. 解决死锁,锁排序

    二、多线程常见问题

    (1)谈谈volatile关键字的用法
    volatile能保证内存可见性,强制从主存中读取数据,此时如果有其他线程修改被volatile修饰的变量,可以第一时间读取到最新值
    (2)Java多线程是如何实现数据共享的? Java
    把内存分成了这几个区域:方法区、堆区、本地方法栈、虚拟机栈、程序计数器,其中堆区这个内存区是多个线程之间共享的。只要把某个数据放在堆内存中,就可以让多个线程都能访问到
    (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 获取到之后才能执行方法内容.
    如果这两个方法属于不同实例: 线程2继续执行两者能并发执行, 互不干扰 synchronized都加在两个静态方法上,相当于对类对象加锁
    线程1能拿到锁,并执行方法,线程2阻塞等待,直到线程1执行完毕,释放锁,

    (10) 进程和线程的区别?

    • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
    • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
    • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
  • 相关阅读:
    市场上低代码产品纷繁复杂,企业该如何选择?
    使用VBA创建数字金字塔
    谐振波导光栅的严格分析
    聊聊零拷贝技术原理和应用
    MINA架构DEMO
    html前端跨域问题的解决方案
    DBCO-PA-PA-Mal
    Linux必会100个命令(五十六)tcpdump命令
    达梦SQL语法兼容笔记
    辅助驾驶功能开发-功能规范篇(22)-3-L2级辅助驾驶方案功能规范
  • 原文地址:https://blog.csdn.net/m0_60631323/article/details/126585936