1.常⻅的锁策略
1.1乐观锁 vs 悲观锁---加锁时遇到的问题
不是针对某一种具体的锁,而是具体的某个锁具有" 悲观 " 和" 乐观 "的特性~~
. 悲观锁
默认认为并发冲突一定会发生 ,操作数据前先加锁,锁住资源,别人不能改,自己操作完再释放锁,强隔离、防冲突。
✅ 优点:
- 数据强一致性,杜绝并发脏写
- 逻辑简单,不用处理重试逻辑
❌ 缺点:
- 锁竞争严重,并发能力差
- 容易产生死锁、锁等待、超时问题
- 长事务会长期占用锁,影响业务
. 乐观锁
默认认为并发冲突很少发生 ,操作数据不加锁 ,只在提交更新时校验数据是否被别人修改过,没被改就更新,被改了就放弃 / 重试。
✅ 优点:
- 无阻塞、高并发友好
- 无死锁,资源利用率高
❌ 缺点:
- 高并发写冲突时,大量重试,CPU 飙升
1.2重量级锁 vs 轻量级锁---遇到问题之后的解决方法
.重量级锁
当悲观场景下,此时就要付出更大的代价--->更低效
.轻量级锁
当乐观场景下,此时就要付出相对较小的代价--->更高效
1.3⾃旋锁 vs 挂起等待锁
.挂起等待锁
是重量级锁的典型表现,也是操作系统内核级别的,加锁的时候如果发现有竞争,就会使该线程进入阻塞状态,后续需要时在进行唤醒。
.⾃旋锁
是轻量级锁的典型表现,也是应用程序级别的,加锁的时候如果发现有竞争,一般是不会进入阻塞,而是通过忙等的形式来进行等待

💡 悲观锁 ==> 重量级锁 ==> 挂起等待锁
🎯乐观锁 ==> 轻量级锁 ==> ⾃旋锁
1.4公平锁 vs ⾮公平锁
假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后 C也尝试获取锁, C 也获取失败, 也阻塞等待.当线程 A 释放锁的时候, 会发⽣啥呢?
.公平锁:
遵守 "先来后到". B ⽐ C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
.⾮公平锁:
不遵守 "先来后到". B 和 C 都有可能获取到锁.synchronized 是⾮公平锁.



注意:
•synchronized 是⾮公平锁.
• 操作系统内部的线程调度就可以视为是随机的 . 如果不做任何额外的限制, 锁就是⾮公平锁. 如果要
想实现公平锁, 就需要依赖额外的数据结构 , 来记录线程们的先后顺序.
• 公平锁和⾮公平锁没有好坏之分, 关键还是看适⽤场景.
1.5可重⼊锁 vs 不可重⼊锁
可重⼊锁的字⾯意思是"可以重新进⼊的锁",即允许同⼀个线程多次获取同⼀把锁。
synchronized 是可重⼊锁

核心重点:
1.锁要记录当前是那个线程拿到的这把锁。
2.使用计数器,记录当前加锁了多少次,在合适的时候进行解锁唤醒。
1.6读写锁vs普通互斥锁
- 普通互斥锁(Mutex) :同一时间只允许一个线程访问,不管是读还是写。
- 读写锁(RWMutex) :读可以共享,写必须独占 。
- 读锁共享:多个线程可以同时加读锁(并发读)
- 写锁独占:加写锁时,其他读写都阻塞
⼀个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.
• 两个线程都只是读⼀个数据, 此时并没有线程安全问题. 直接并发的读取即可.
• 两个线程都要写⼀个数据, 有线程安全问题.
• ⼀个线程读另外⼀个线程写, 也有线程安全问题.
💡读写锁规则(非常重要):
- 读 + 读:可以同时进行(共享)
- 读 + 写:互斥阻塞
- 写 + 写:互斥阻塞
- 写的时候,所有读都要等
注意 ,Synchronized 不是读写锁.
只要是涉及到 "互斥", 就会产⽣线程的挂起等待. ⼀旦线程挂起, 再次被唤醒就不知道隔了多久
了.因此尽可能减少 "互斥" 的机会, 就是提⾼效率的重要途径.读写锁特别适合于 "频繁读, 不频繁写" 的场景中.
💡💡2. 重点面试题
2.1 是怎么理解乐观锁和悲观锁的,具体怎么实现呢?
悲观锁是 多个线程访问同⼀个变量冲突的概率较⼤, 会在每次访问变量之前都去真正加锁.
乐观锁认为多个线程访问同⼀个变量冲突的概率不⼤. 并不会真的加锁, ⽽是直接尝试访问数据.
在访问的同时识别当前的数据是否出现访问冲突
悲观锁的实现就是先加锁,获取到锁再操作数据. 获取不到锁就等待.
2.2 介绍下读写锁?
读写锁就是把读操作 和写操作 分别进⾏加锁. 读锁和读锁之间不互斥.
写锁和写锁之间互斥. 写锁和读锁之间互斥.
读写锁最主要⽤在 "频繁读, 不频繁写" 的场景中.
2.3什么是⾃旋锁,为什么要使⽤⾃旋锁策略呢,缺点是什么?
如果获取锁失败, ⽴即再尝试获取锁, ⽆限循环, 直到获取到锁为⽌. 第⼀次获取锁失败, 第⼆次的尝试
会在极短的时间内到来. ⼀旦锁被其他线程释放, 就能第⼀时间获取到锁.
相⽐于挂起等待锁,
优点 : 没有放弃 CPU 资源, ⼀旦锁被释放就能第⼀时间获取到锁, 更⾼效. 在锁持有时间⽐较短的场景下⾮常有⽤.
缺点: 如果锁的持有时间较⻓, 就会浪费 CPU 资源
2.4synchronized是什么?
synchronized 是:
悲观锁、可重入锁、非公平锁、普通互斥排他锁
无竞争轻量级 / 自旋,竞争激烈变为重量级 / 挂起等待
3.CAS
3.1什么是CAS?
全称 :Compare And Swap,比较并交换
是乐观锁 的核心实现,无锁、非阻塞、基于硬件指令。

3.2核心执行逻辑
-
拿到内存旧值 V
-
比较:当前工作内存值 预期值 A 是否等于 内存值 V
-
相等 → 交换:把新值 B 写入内存
不相等 → 失败,重试 / 放弃
CAS伪代码
if (内存值 == 预期值) {
内存值 = 新值;
}
整个过程是 CPU 硬件原子指令,不加锁。
3.3CAS 归类
- 悲观锁 / 乐观锁:乐观锁
- 重量级 / 轻量级锁:轻量级、无锁
- 挂起等待 / 自旋锁:自旋锁(失败就循环重试)
- 公平 / 非公平:无锁机制,无公平概念
- 可重入 / 不可重入:不可重入
- 读写锁 / 互斥锁:既不是读写锁,也不是互斥锁,无锁并发
3.4优缺点
优点
- 全程用户态,无内核切换,性能高
- 不死锁、不阻塞线程
- 适合竞争不激烈的并发场景
缺点
- ABA 问题(致命)
- 循环自旋,竞争激烈时CPU 空转消耗高
- 只能保证单个变量原子性,不能保证代码块
3.4三大经典问题
1. ABA 问题
- 现象:原值 A → 被改成 B → 又改回 A
CAS 只看最终值,误以为没被修改,导致数据错乱 - 解决:版本号 / 时间戳
2. 自旋消耗 CPU
- 解决:自适应自旋、限制重试次数、退化为锁
3. 只能原子操作单个变量
- 解决:
AtomicReference封装对象
3.5相关⾯试题
-
讲解下你⾃⼰理解的 CAS 机制
全称 Compare and swap, 即 "⽐较并交换". 相当于通过⼀个原⼦的操作, 同时完成 "读取内存, ⽐较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的⽀撑.
-
ABA问题怎么解决?
给要修改的数据引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期. 如果发现当前版本号和之前读到的版本号⼀致, 就真正执⾏修改操作, 并让版本号⾃增; 如果发现当前版本号⽐之前读到的版本号⼤, 就认为操作失败
4.加锁⼯作过程
JVM 将 synchronized 锁分为 ⽆锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进⾏依次升级。(锁升级)

-
偏向锁--第⼀个尝试加锁的线程, 优先进⼊偏向锁状态
-
轻量级锁 --随着其他线程进⼊竞争, 偏向锁状态被消除, 进⼊轻量级锁状态(⾃适应的⾃旋锁).
此处的轻量级锁就是通过 CAS 来实现
-
重量级锁 --如果竞争进⼀步激烈, ⾃旋不能快速获取到锁状态, 就会膨胀为重量级锁
注意:(当前JVM中,只有锁升级没有锁降级)
无锁 =>偏向锁:代码进入synchronized 的代码块
偏向锁 =>轻量级锁:拿到偏向锁的线程运行过程中,遇到其他线程尝试竞争这个锁
轻量级锁 => 重量级锁:JVM发现,当前竞争锁的情况非常激烈
5.其他的优化操作
5.1锁消除--是编译器优化的体现
编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
有些应⽤程序的代码中, 写了 synchronized, 但其实在多线程环境下没有用到,就会自动把synchronized给去掉。
5.2锁粗化--锁的粒度
⼀段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会⾃动进⾏锁的粗化
加锁与解锁之间,包含的代码越多,锁的粗粒度就越粗,包含的代码量越少,锁的粗粒度就越细 (不是代码行数,是实际执行的指令/时间)
一个代码中,反复对细粒度的代码进行加锁,就可能被优化成更粗粒度的加锁了
6.JUC 的常⻅类--java.util.concurrent--就是和线程相关的一下工具
6.1Callable 接⼝
Callable 是⼀个 interface . 相当于把线程封装了⼀个 "返回值"
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class demo8 {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
try {
// 等待任务执行完成并获取结果
int result = futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
// 线程被中断时的处理
System.err.println("线程被中断");
e.printStackTrace();
} catch (ExecutionException e) {
// 任务执行过程中发生异常
System.err.println("任务执行出错");
e.printStackTrace();
}
}
}
可以看到, 使⽤ Callable 和 FutureTask 之后, 代码简化了很多, 也不必⼿动写线程同步代码了.
理解 Callable
Callable 和 Runnable 相对, 都是描述⼀个 "任务". Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务
.
Callable 通常需要搭配 FutureTask 来使⽤. FutureTask ⽤来保存 Callable 的返回结果. 因为
Callable 往往是在另⼀个线程中执⾏的, 啥时候执⾏完并不确定.
FutureTask 就可以负责这个等待结果出来的⼯作
理解 FutureTask
想象去吃⿇辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你⼀张 "⼩票" . 这个⼩票就是
FutureTask. 后⾯我们可以随时凭这张⼩票去查看⾃⼰的这份⿇辣烫做出来了没.

6.2ReentrantLock
可重⼊互斥锁. 和 synchronized 定位类似, 都是⽤来实现互斥效果, 保证线程安全.
ReentrantLock 也是可重⼊锁. Reentrant 这个单词的原意就是 "可重⼊"
ReentrantLock 的⽤法:
• lock(): 加锁, 如果获取不到锁就死等.
• trylock(超时时间): 加锁, 如果获取不到锁, 等待⼀定的时间之后就放弃加锁.
• unlock(): 解锁
ReentrantLock lock = new ReentrantLock();
-----------------------------------------
lock.lock();
try {
// working
} finally {
lock.unlock()
}
6.3ReentrantLock 和 synchronized 的区别:
• synchronized 是⼀个关键字, 是 JVM 内部实现的(是基于 C++ 实现)
ReentrantLock 是标准库的⼀个类, 在 JVM 外实现的(基于 Java 实现).
• synchronized 使⽤时不需要⼿动释放锁.
ReentrantLock 使⽤时需要⼿动释放. 使⽤起来更灵活, 但是也容易遗漏 unlock.
•synchronized 在申请锁失败时, 会死等.
ReentrantLock 可以通过 trylock 的⽅式等待⼀段时间就放弃.
• synchronized 是⾮公平锁, ReentrantLock 默认是⾮公平锁. 可以通过构造⽅法传⼊⼀个 true 开启公平锁模式.
• synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程.
ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
6.4如何选择使⽤哪个锁?
• 锁竞争不激烈的时候, 使⽤ synchronized, 效率更⾼, ⾃动释放更⽅便.
• 锁竞争激烈的时候, 使⽤ ReentrantLock, 搭配 trylock 更灵活控制加锁的⾏为, ⽽不是死等.
• 如果需要使⽤公平锁, 使⽤ ReentrantLock
7.原子类
是 Java java.util.concurrent.atomic 包下的一组 无锁、线程安全工具类,核心用 CAS(Compare-And-Swap)+ volatile 实现,保证单个变量的 "读 - 改 - 写" 全程原子、不可分割 ,多线程并发无竞态,性能远超 synchronized。
| 类 | 作用 |
|---|---|
AtomicInteger |
原子更新 int(常用) |
AtomicLong |
原子更新 long |
AtomicBoolean |
原子更新 boolean |
AtomicReference<V> |
原子更新对象引用 |
AtomicStampedReference<V> |
带版本号更新引用 |
AtomicMarkableReference<V> |
带标记位更新引用 |
AtomicInteger cnt = new AtomicInteger(0);
cnt.incrementAndGet(); // ++i,返回新值
cnt.getAndIncrement(); // i++,返回旧值
cnt.addAndGet(5); // +=5,返回新值
cnt.compareAndSet(0,1);// CAS:预期0则设为1,成功返回true
原子类 vs synchronized
| 对比 | 原子类 | synchronized |
|---|---|---|
| 实现 | 无锁(CAS + 自旋) | 悲观锁(阻塞 + 唤醒) |
| 粒度 | 变量级(极细) | 代码块 / 方法级 |
| 性能 | 高并发下极高 | 竞争激烈时阻塞、上下文切换开销大 |
| 适用 | 单变量简单原子操作 | 多变量复合操作、复杂临界区 |
原子类是无锁并发 的基石,适合单变量、高并发、简单原子操作 场景,性能碾压 synchronized;复杂多变量同步仍用锁或 java.util.concurrent 高级工具。 |
8.线程池
虽然创建销毁线程⽐创建销毁进程更轻量, 但是在频繁创建销毁线程的时候还是会⽐较低效.线程池就是为了解决这个问题. 如果某个线程不再使⽤了, 并不是真正把线程释放, ⽽是放到⼀个 "池⼦" 中, 下次如果需要⽤到线程就直接从池⼦中取, 不必通过系统来创建了.
8.1ExecutorService 和 Executors
代码⽰例:
• ExecutorService 表⽰⼀个线程池实例.
• Executors 是⼀个⼯⼚类, 能够创建出⼏种不同⻛格的线程池.
• ExecutorService 的 submit ⽅法能够向线程池中提交若⼲个任务.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
8.2Executors 创建线程池的⼏种⽅式
newFixedThreadPool(n):固定核心线程,队列无界 → OOM 风险newSingleThreadExecutor():单线程池,串行执行newCachedThreadPool():无核心线程、最大线程无限 → 线程爆炸newScheduledThreadPool():定时任务线程池
8.3线程池的⼯作流程

8.4信号量 Semaphore--能够协调多个线程之间的资源分配
信号量, ⽤来表⽰ "可⽤资源的个数". 本质上就是⼀个计数器
8.5CountDownLatch--同时等待 N 个任务执⾏结束
8.6相关⾯试题
1. 线程同步的⽅式有哪些?
synchronized, ReentrantLock, Semaphore 等都可以⽤于线程同步.
2. 为什么有了 synchronized 还需要 juc 下的 lock?
以 juc 的 ReentrantLock 为例,
• synchronized 使⽤时不需要⼿动释放锁. ReentrantLock 使⽤时需要⼿动释放. 使⽤起来更灵活,
• synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的⽅式等待⼀段时间就放弃.
• synchronized 是⾮公平锁, ReentrantLock 默认是⾮公平锁. 可以通过构造⽅法传⼊⼀个 true 开启公平锁模式.
• synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程.
ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
9.线程安全的集合类
9.1多线程环境使⽤ ArrayList
- ⾃⼰使⽤同步机制 (synchronized 或者 ReentrantLock)
- Collections.synchronizedList(new ArrayList)
- 使⽤ CopyOnWriteArrayList
9.2多线程环境使⽤队列
- ArrayBlockingQueue---基于数组实现的阻塞队列
- LinkedBlockingQueue---基于链表实现的阻塞队列
- PriorityBlockingQueue---基于堆实现的带优先级的阻塞队列
- TransferQueue---最多只包含⼀个元素的阻塞队列
9.3多线程环境使⽤哈希表
HashMap 本⾝不是线程安全的.
在多线程环境下使⽤哈希表可以使⽤:
• Hashtable
• ConcurrentHashMap
10.死锁
死锁是什么
死锁是这样⼀种情形:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌。
死锁是⼀种严重的 BUG!! 导致⼀个程序的线程 "卡死", ⽆法正常⼯作!
如何避免死锁
死锁产⽣的四个必要条件:
•互斥使⽤ ,即当资源被⼀个线程使⽤(占有)时,别的线程不能使⽤
• 不可抢占 ,资源请求者不能强制从资源占有者⼿中夺取资源,资源只能由资源占有者主动释放。
• 请求和保持 ,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
• 循环等待,即存在⼀个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了⼀个等待环路。当上述四个条件都成⽴的时候,便形成死锁。当然,死锁的情况下如果打破上述任何⼀个条件,便可让死锁消失。