目录
[1. 锁策略](#1. 锁策略)
[1.1 悲观锁 vs 乐观锁](#1.1 悲观锁 vs 乐观锁)
[1. 悲观锁](#1. 悲观锁)
[1.2 重量级锁 vs 轻量级锁](#1.2 重量级锁 vs 轻量级锁)
[1.3 挂起等待锁 vs 自旋锁](#1.3 挂起等待锁 vs 自旋锁)
[1.4 非公平锁 vs 公平锁](#1.4 非公平锁 vs 公平锁)
[1.5 可重入锁 vs 不可重入锁](#1.5 可重入锁 vs 不可重入锁)
[1.6 只读锁 vs 读写锁](#1.6 只读锁 vs 读写锁)
[2. 关注synchronized 同步锁](#2. 关注synchronized 同步锁)
1. 锁策略
我们知道,在日常工作中,面对多线程并发执行遇到的线程安全问题我们最常用的解决办法就是加锁,但是面对不同情况下的线程安全问题,我们要选择合适的加锁策略。
1.1 悲观锁 vs 乐观锁
1. 悲观锁
| 特性 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思想 | 认为并发操作一定会产生冲突,提前加锁 | 认为并发冲突很少发生,不上锁,通过最终校验保证数据一致性 |
| 特点 | 独占、阻塞、排他 | 无阻塞、无独占、失败重试 |
| 执行流程 | 加锁 → 阻塞其他线程 → 释放锁后执行 | 读取数据 → 修改前校验版本/状态 → 一致则提交,不一致重试 |
| 优点 | 数据绝对安全,无并发异常 | 并发效率高,无阻塞开销 |
| 缺点 | 性能差,阻塞导致高延迟 | 频繁冲突时重试消耗资源 |
| Java实现 | synchronized、ReentrantLock |
CAS自旋(如**AtomicInteger**)、原子类 |
✅总结:多写少读 用悲观锁;少写多读 用乐观锁;
1.2 重量级锁 vs 轻量级锁
| 特性 | 重量级锁 | 轻量级锁 |
|---|---|---|
| 实现层级 | 内核态 | 用户态 |
| 阻塞方式 | 线程挂起,进入等待队列 | 自旋等待,不阻塞 |
| 切换开销 | 用户态 ↔ 内核态,开销大 | 无切换,开销小 |
| 触发场景 | 高竞争、轻量级锁膨胀 | 低竞争、线程交替执行 |
| 典型例子 | synchronized 最终形态 |
CAS、偏向锁升级前的状态 |
**✅总结:**重量级锁在线程冲突且没有拿到锁时进行阻塞等待;轻量级锁自旋,占用cpu直到等到拿到锁。
1.3 挂起等待锁 vs 自旋锁
| 特性 | 挂起等待锁(阻塞锁) | 自旋锁 | |
|---|---|---|---|
| 等待方式 | 线程休眠阻塞,进入等待队列 | 循环空转,持续尝试获取锁 | |
| CPU 占用 | 低(让出 CPU,不占用计算资源) | 高(持续占用 CPU 资源) | |
| 线程切换 | 有(线程被挂起,需唤醒) | 无(线程保持运行状态) | |
| 响应速度 | 较慢(需唤醒线程,存在调度延迟) | 极快(无需唤醒,直接获取锁) | |
| 适用场景 | 锁持有时间长(如重量级锁) | 锁持有时间极短(如 CAS、轻量级锁) | |
| 优缺点 | 优点:节省 CPU; 缺点:唤醒开销大 | 优点:无阻塞,获取锁快; 缺点:浪费 CPU 资源 |
**✅总结:**重量级锁 = 挂起等待锁;轻量级锁 = 自旋锁;
1.4 非公平锁 vs 公平锁
| 特性 | 公平锁 | 非公平锁 |
|---|---|---|
| 规则 | 先来先服务,线程严格按请求顺序排队获取锁 | 谁抢到谁用,新线程可直接插队抢占锁 |
| 新线程行为 | 主动进入队列尾部排队,不插队 | 不排队,直接尝试抢占锁 |
| 优点 | 线程执行均衡,避免饥饿 | 并发效率高,减少线程唤醒开销 |
| 缺点 | 排队、唤醒严格,性能略低 | 老线程可能长期抢不到锁,导致饥饿 |
| 典型实现 | new ReentrantLock(true) | synchronized、new ReentrantLock() 默认 |
**✅总结:**业务优先性能 → 一律用非公平锁;强有序排队需求 → 才用公平锁
1.5 可重入锁 vs 不可重入锁
| 特性 | 可重入锁 | 不可重入锁 |
|---|---|---|
| 同一线程重复获取 | 允许(计数器递增) | 阻塞(死锁) |
| 实现复杂度 | 较高(需维护计数器) | 简单(二元状态) |
| 典型应用场景 | 递归、嵌套调用 | 极少数特殊场景 |
| Java实现 | ReentrantLock, synchronized | 需自定义或无直接对应实现 |
**✅总结:**一般开发中使用的是可重入锁,因为并发效率高,很少使用不可重入锁,因为容易造成死锁。
1.6 只读锁 vs 读写锁
| 特性 | 读锁(共享锁) | 写锁(排他锁) |
|---|---|---|
| 规则 | 多个线程可同时加锁,互不阻塞 | 同一时刻仅一个线程持有锁 |
| 特点 | 共享、并发性高、无排他性 | 独占、排他、阻塞其他所有读写操作 |
| 限制 | * 加锁后禁止修改数据,仅允许读取 | 加锁期间禁止其他线程读写数据 |
| 适用场景 | 高频查询、数据浏览、读取操作 | 数据新增、修改、删除等写操作 |
✅总结:①:读与读不互斥;
②:读与写互斥;
③:写与写互斥;
2. 关注synchronized 同步锁
- 属于自适应 、非公平锁 、可重入锁;
- 内置 JVM 自适应锁机制,存在锁升级:无锁→偏向锁→轻量级锁→重量级锁;
- 竞争小时自旋等待 不阻塞,竞争激烈转为阻塞挂起,底层依托对象头 MarkWord 实现。
3.常见面试题
- 你是怎么理解悲观锁和乐观锁的,具体怎么实现呢?
回答:
①:悲观锁认为多个线程访问同一个共享变量时产生冲突的概率较大,所以会在每次访问数据时真正地加锁。具体实现就是先加锁,获取到锁之后进行访问数据,如果没有获取到锁就阻塞等待。
②:乐观锁认为多个线程访问同一个共享变量时产生冲突的概率较小,所以不会真正地加锁,尝试进行数据访问的同时识别该数据是否存在访问冲突。具体实现就是不真正加锁,引入版本号,通过版本号识别是否存在数据访问冲突。
- 介绍下读写锁?
**回答:**读写锁就是把读操作和写操作分别进行加锁,读与读之间不互斥、读与写之间互斥、写与写之间互斥;读写锁通常运用于"频繁读,不频繁写 "的场景中。
- 什么是自旋锁,为什么要使用自旋锁策略呢,优缺点是什么?
回答:
①:自旋锁是指当没有竞争到锁时,不断再次尝试获取锁,直到获取到锁为止。
②:使用自选锁的原因是当第一次获取锁失败,第二次获取锁会在极短的时间内到来,一旦有线程释放锁,就能第一时间拿到锁;
③:优点:没有放弃cou,能在锁释放的第一时间拿到锁。当持有锁时间较短时较高效。
缺点:当持有锁时间较长时,会浪费cpu资源。
- synchronized 是可重入锁么?
**回答:**是的。可重入锁是指连续两次加锁不会导致死锁。实现方式就是在锁中记录持有锁的线程的身份,以及一个计数器,当发现要加锁的线程是已经持有锁的线程,计数器直接自增。
以上就是关于锁策略面试应对的相关介绍,感兴趣的铁铁们不妨收藏后再走叭~