产生死锁需要同时满足四个必要条件:
- 互斥条件(Mutual Exclusion):资源不能被多个进程共享,即资源一次只能被一个进程使用。如果一个资源已经被分配给了一个进程,其他进程必须等待,直到该资源被释放。
- 持有并等待条件(Hold and Wait):一个进程已经持有了至少一个资源,同时还在等待获取其他被占用的资源。在此期间,该进程不会释放已经持有的资源。
- 不可剥夺条件(No Preemption):已分配给进程的资源不能被强制剥夺,只有持有该资源的进程可以主动释放资源。
- 循环等待条件(Circular Wait):存在一个进程集合 ,其中 等待 持有的资源, 等待 持有的资源,依此类推,直到 等待 持有的资源,形成一个进程等待环。
假设有两个进程 和 ,以及两个资源 和 ,一个简单的死锁场景是这样的:
- 持有资源 ,并请求资源 。
- 持有资源 ,并请求资源 。
在这种情况下,发生死锁的步骤如下:
- 互斥条件: 和 都只能被一个进程占用。
- 持有并等待条件: 持有 并等待 ,同时 持有 并等待 。
- 不可剥夺条件: 和 都不能被强制从 和 中剥夺。
- 循环等待条件: 等待 持有的 ,而 等待 持有的 ,形成一个循环。
19、如何避免死锁呢
产⽣死锁的有四个必要条件:互斥条件、持有并等待条件、不可剥夺条件、环路等待条件。
避免死锁,破坏其中的一个就可以。
消除互斥条件
这个是没法实现,因为很多资源就是只能被一个线程占用,例如锁。
消除请求并持有条件
消除这个条件的办法很简单,就是一个线程一次请求其所需要的所有资源。
消除不可剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可剥夺这个条件就破坏掉了。
消除环路等待条件
可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。
1. 进程和线程的区别
这是最基础也最重要的问题。你可以从定义、开销、稳定性、通信这四个维度来回答。
| 维度 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 基本定义 | 资源分配的基本单位。拥有独立的内存空间(代码段、数据段、堆等)。 | CPU 调度和执行的基本单位。不拥有系统资源,只拥有少量运行时资源(栈、寄存器)。 |
| 开销成本 | 重。创建/销毁需分配独立内存,切换涉及页表刷新,开销大。 | 轻。共享进程资源,创建/切换仅需维护少量上下文,开销小。 |
| 稳定性 | 高。一个进程崩溃(如段错误),通常不会影响其他进程(隔离性好)。 | 低。一个线程崩溃(如非法指针访问),会导致整个进程(包括该进程下所有线程)挂掉。 |
| 通信方式 | 复杂。需通过 IPC 机制(管道、消息队列、共享内存等)。 | 简单。直接读写同一进程内的全局变量或堆内存(但需考虑同步)。 |
2. 线程为什么比进程更轻量
线程之所以被称为“轻量级进程”,主要源于资源的共享和上下文切换的成本:
-
内存共享:同一进程内的线程共享代码段、数据段和堆内存。创建线程时,不需要像创建进程那样复制大量的内存页或建立独立的页表。
-
切换成本低:
- 进程切换:涉及用户态到内核态的切换,需要保存当前进程的 CPU 环境(寄存器、程序计数器),刷新内存管理单元(MMU)的快表(TLB),加载新进程的页表等,开销很大。
- 线程切换:同一进程内的线程切换,只需保存和恢复少量的寄存器内容和栈指针,不需要刷新 TLB,也不需要切换内存空间,因此速度极快。
3. 进程通信方式有哪些
由于进程间内存是隔离的,它们必须通过操作系统提供的内核机制来交换数据。常见方式包括:
- 管道 (Pipe) :半双工,数据只能单向流动。通常用于父子进程或兄弟进程间。
- 命名管道 (FIFO) :去除了亲缘关系限制,任何进程可以通过路径名访问。
- 消息队列 (Message Queue) :存放在内核中的消息链表,可以随机读写,克服了管道字节流限制。
- 共享内存 (Shared Memory) :最快的 IPC 方式。映射一段能被多个进程访问的内存区域,进程直接读写该内存(需配合信号量同步)。
- 信号量 (Semaphore) :主要用于同步,控制多个进程对共享资源的访问(计数器)。
- 套接字 (Socket) :不仅可用于不同机器间的通信,也可用于本机进程通信。
4. 线程同步方式有哪些
当多个线程访问共享资源时,为了防止数据不一致(竞态条件),需要同步机制:
- 互斥锁 (Mutex) :保证同一时刻只有一个线程能访问共享资源。
- 自旋锁 (Spinlock) :线程不进入睡眠,而是“自旋”(循环检查)等待锁释放。适用于锁持有时间极短的场景。
- 读写锁 (Read-Write Lock) :允许多个读线程同时访问,但写线程独占。适合“读多写少”场景。
- 条件变量 (Condition Variable) :允许线程在某个条件不满足时挂起(睡眠),直到其他线程改变条件并唤醒它。
- 信号量 (Semaphore) :允许多个线程同时访问共享资源(限制最大并发数)。
5. 什么是死锁
死锁是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力干涉,这些进程都将无法推进。
简单比喻:你拿着钥匙 A 等我的钥匙 B 开门,我拿着钥匙 B 等你的钥匙 A 开门,咱俩就这么僵持着。
6. 死锁的四个必要条件
产生死锁必须同时满足以下四个条件(Coffman 条件):
- 互斥条件:资源是独占的,一次只能被一个进程使用。
- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能被其他进程强行剥夺,只能由自己释放。
- 环路等待条件:存在一种进程资源的循环等待链(P1 等 P2,P2 等 P1)。
7. 怎么解决死锁问题
解决死锁通常有三种策略:
-
死锁预防(破坏四个必要条件之一):
- 破坏请求与保持:规定进程在运行前一次性申请完所有资源。
- 破坏不剥夺条件:允许抢占资源(当一个进程请求的资源得不到满足时,必须释放已占有的资源)。
- 破坏环路等待:对资源进行编号,规定进程必须按编号递增的顺序请求资源。
-
死锁避免(银行家算法):
- 在分配资源前,先计算此次分配是否会导致系统进入“不安全状态”。如果会,就不分配。
-
死锁检测与恢复:
- 允许死锁发生,系统定期运行检测算法(如资源分配图化简)。一旦发现死锁,通过剥夺资源、回滚或杀死进程来恢复。
8. 什么是内存泄漏和内存溢出
这两个概念常被混淆,但本质不同:
-
内存泄漏:
- 定义:指程序在申请内存后,无法释放已申请的内存空间。
- 本质:是逻辑错误。内存虽然不再被程序使用,但因为还持有引用,垃圾回收器(GC)无法回收它。
- 后果:长期运行后,泄漏累积,最终导致内存溢出。
- 例子:静态集合类(如
static List)不断添加对象却不清理。
-
内存溢出:
- 定义:指程序在申请内存时,系统无法提供足够的内存空间。
- 本质:是资源不足。
- 原因:可能是内存泄漏导致的,也可能是申请的数据量确实超过了物理内存限制(如加载一个 10GB 的文件到 4GB 内存中)。
- 例子:
OutOfMemoryError。
9. 什么是用户态和内核态
现代操作系统为了保护系统安全,将 CPU 的执行权限分为两个级别:
-
用户态:
- 权限低:应用程序运行在此模式。只能执行非特权指令(如算术运算),不能直接访问硬件设备或核心内存区域。
- 受限:如果需要读写文件、分配内存或访问网络,必须通过系统调用陷入内核。
-
内核态:
- 权限高:操作系统内核运行在此模式。可以执行特权指令(如开关中断、操作 MMU),访问所有内存和硬件。
-
切换代价:
- 从用户态切换到内核态(如发生系统调用、中断、异常)需要消耗资源。这涉及到保存当前上下文(寄存器、程序计数器)、刷新流水线等操作,这就是上下文切换的开销。