第 2 天学习任务
目标:Java 并发核心 + HashMap 核心
一、Java 并发核心(锁专题超详细版)
1. synchronized 详解
1.1 三大特性(底层保障逻辑)
- 原子性:通过
monitorenter/monitorexit保证代码块执行期间,其他线程无法获取锁,操作不可中断 - 可见性:解锁时自动将工作内存数据刷新到主内存,加锁时清空工作内存,从主内存重新读取
- 有序性:遵循 Happens-Before 原则,synchronized 块内的指令不会和块外指令重排序
1.2 锁的使用位置(附代码示例)
-
修饰代码块:
// 锁任意对象 Object lock = new Object(); synchronized (lock) { /* 业务逻辑 */ } // 锁this synchronized (this) { /* 业务逻辑 */ } // 锁Class对象 synchronized (Demo.class) { /* 业务逻辑 */ } -
修饰普通方法:
public synchronized void method() { /* 业务逻辑 */ } // 锁this -
修饰静态方法:
public static synchronized void staticMethod() { /* 业务逻辑 */ } // 锁Demo.class
1.3 底层原理(JVM 层面)
-
对象头结构:
长度 内容 说明 32/64 位 Mark Word 存储锁信息、HashCode、GC 年龄 32/64 位 类型指针 指向类元数据的指针 可选 数组长度 数组对象特有 -
Mark Word 状态切换:无锁 → 偏向锁 → 轻量级锁 → 重量级锁(状态记录在 Mark Word 的标志位)
-
monitor 对象:每个对象关联一个 monitor(管程),持有锁的线程会关联 monitor 的 _owner 字段,其他线程进入 _WaitSet 等待
1.4 锁升级流程(JDK1.8 ,附触发条件)
| 锁类型 | 适用场景 | 触发条件 | 底层实现 | 性能 |
|---|---|---|---|---|
| 偏向锁 | 单线程重复获取同一锁 | 无竞争,首次获取锁时 | Mark Word 记录线程 ID,无 CAS 操作 | 几乎无开销 |
| 轻量级锁(自旋锁) | 多线程交替获取锁,竞争弱 | 有线程竞争,但未超过自旋次数(默认 10 次) | CAS + 自旋,不阻塞线程 | 低开销 |
| 重量级锁 | 多线程竞争激烈 | 自旋次数耗尽,或有线程阻塞 | 操作系统互斥量(mutex),线程挂起 | 高开销 |
- 核心规则:只能升级,不能降级(偏向锁不会回退为无锁,轻量级锁不会回退为偏向锁)
- 自旋优化:JDK1.6 引入自适应自旋,自旋次数根据前一次同锁的自旋结果动态调整
1.5 可重入性(底层实现)
-
底层通过 锁计数器 实现:
- 线程首次获取锁 → 计数器 + 1,关联到 monitor 的 _owner
- 同一线程再次获取 → 计数器 + 1
- 线程释放锁 → 计数器 - 1
- 计数器 = 0 → 释放 monitor,其他线程可竞争
-
示例:
public synchronized void A() { B(); } public synchronized void B() { /* 可正常执行,计数器从1→2 */ }
1.6 公平性
- 默认 非公平锁:线程获取锁时,先尝试直接抢占,而非排队,减少线程切换开销
- 无法手动设置为公平锁
2. volatile 详解(补充锁相关关联)
2.1 与锁的区别
- volatile 仅保证可见性、有序性,不保证原子性;锁(synchronized/Lock)三者都保证
- volatile 是轻量级同步,无锁竞争;锁涉及锁竞争,开销更高
- volatile 修饰变量;锁修饰代码块 / 方法
2.2 与 synchronized 结合使用(DCL 单例完整代码)
public class Singleton {
// volatile 禁止指令重排,避免半初始化对象
private static volatile Singleton instance;
private Singleton() {} // 私有构造,防止实例化
public static Singleton getInstance() {
// 第一次检查,避免每次加锁
if (instance == null) {
synchronized (Singleton.class) {
// 第二次检查,防止多线程同时进入外层if
if (instance == null) {
instance = new Singleton(); // 3步操作:分配内存→初始化→引用赋值
}
}
}
return instance;
}
}
3. ReentrantLock 详解(超详细)
3.1 核心底层:AQS(抽象队列同步器)
-
AQS 核心结构:
- 同步状态(state):0 = 无锁,>0 = 有锁(可重入次数)
- 等待队列:双向链表,存放等待获取锁的线程
- Condition 队列:单向链表,存放 await () 的线程
-
ReentrantLock 基于 AQS 实现,state 记录重入次数
3.2 公平锁 vs 非公平锁(附实现差异)
-
非公平锁(默认) :
ReentrantLock lock = new ReentrantLock(); // 默认非公平逻辑:线程尝试获取锁时,先 CAS 修改 state=1,成功则获取锁;失败则进入等待队列优点:吞吐量高,减少线程切换缺点:可能出现线程饥饿(某些线程长期拿不到锁)
-
公平锁:
ReentrantLock lock = new ReentrantLock(true); // 公平锁逻辑:线程必须排队,只有队列头节点能获取锁,不允许抢占优点:无饥饿,公平性高缺点:吞吐量低,线程切换频繁
3.3 核心方法(附使用场景)
| 方法 | 作用 | 使用场景 |
|---|---|---|
| lock() | 获取锁,阻塞直到成功 | 必须获取锁才能执行的核心业务 |
| lockInterruptibly() | 可中断获取锁,线程中断时抛异常 | 可取消的任务(如超时任务) |
| tryLock() | 尝试获取锁,立即返回 true/false | 非核心业务,拿不到锁可走降级逻辑 |
| tryLock(long timeout, TimeUnit unit) | 超时获取锁,超时返回 false | 有时间限制的任务 |
| unlock() | 释放锁,必须在 finally 中执行 | 所有加锁场景,防止死锁 |
| newCondition() | 创建 Condition 对象,支持等待 / 唤醒 | 精准唤醒指定线程(如生产者消费者) |
3.4 Condition 精准唤醒(代码示例)
ReentrantLock lock = new ReentrantLock();
Condition producer = lock.newCondition(); // 生产者条件
Condition consumer = lock.newCondition(); // 消费者条件
// 生产者等待
lock.lock();
try {
while (队列满) {
producer.await(); // 生产者进入等待队列
}
// 生产数据
consumer.signal(); // 唤醒消费者
} finally {
lock.unlock();
}
// 消费者等待
lock.lock();
try {
while (队列空) {
consumer.await(); // 消费者进入等待队列
}
// 消费数据
producer.signal(); // 唤醒生产者
} finally {
lock.unlock();
}
3.5 与 synchronized 对比(完整表格)
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 锁获取方式 | 自动获取 | 手动 lock ()/unlock () |
| 公平性 | 仅非公平 | 公平 / 非公平可选 |
| 可中断 | 不可中断 | 支持 lockInterruptibly () |
| 超时获取 | 不支持 | 支持 tryLock (timeout) |
| 条件唤醒 | 随机唤醒 | Condition 精准唤醒 |
| 底层实现 | JVM 层面 | JDK 层面(AQS) |
| 性能 | 竞争小时接近 Lock | 竞争大时更优 |
| 死锁风险 | 低(自动释放) | 高(忘记 unlock ()) |
4. 补充:CAS 详解(并发核心底层)
4.1 定义
Compare And Swap(比较并交换),无锁编程核心,底层通过 CPU 指令 cmpxchg 实现(原子操作)
4.2 核心参数
- V:要修改的变量(内存值)
- A:预期值(工作内存值)
- B:新值
- 逻辑:如果 V == A,则将 V 改为 B;否则不修改,返回 V
4.3 优缺点
-
优点:无锁,避免线程切换开销,并发性能高
-
缺点:
- ABA 问题:变量从 A→B→A,CAS 认为未修改(解决:加版本号,如 AtomicStampedReference)
- 自旋开销:高并发下自旋次数多,消耗 CPU
- 只能修改单个变量,无法保证复合操作原子性
4.4 常用原子类(基于 CAS)
- AtomicInteger/AtomicLong:基本类型原子操作
- AtomicReference:引用类型原子操作
- AtomicStampedReference:解决 ABA 问题
- LongAdder:高并发下替代 AtomicLong,分段 CAS,性能更高
二、HashMap(JDK1.7 & JDK1.8)
- 整体结构JDK1.7:数组 + 链表JDK1.8:数组 + 链表 + 红黑树
- 核心参数默认初始容量:16(必须是 2 的 n 次方)加载因子:0.75扩容阈值:容量 × 加载因子树化条件:链表长度 ≥ 8且 数组长度 ≥ 64退化为链表:红黑树节点 ≤ 6
- 哈希计算原理先计算 key 的 hashCode ()再做 扰动函数:让高位参与运算,减少哈希冲突最终下标:hash & (数组长度 - 1) 等价于取模,但速度更快
- JDK1.7 完整细节底层数组:Entry<K,V>[] table哈希冲突解决:链地址法插入方式:头插法(新节点放在链表头部)扩容机制:新容量 = 旧容量 << 1(翻倍)所有元素 重新计算 hash 重新排布并发问题:多线程扩容时,链表可能 反转成环调用 get () 时出现 死循环,CPU 100%
- JDK1.8 完整细节底层数组:Node<K,V>[] table插入方式:尾插法(解决了扩容成环问题)扩容优化:不需要重新完整计算 hash新下标 = 原下标 或 原下标 + 旧容量判断依据:hash & 旧容量,结果为 0 不变,非 0 则加旧容量查询效率:链表:O (n)红黑树:O (logn)
- JDK1.8 put 流程(完整)如果数组为空,先执行 resize () 初始化根据 key 计算 hash,得到数组下标如果该下标位置 没有节点,直接新建 Node 放入如果有节点:key 完全相同 → 直接覆盖 value如果是红黑树节点 → 执行红黑树插入如果是链表 → 尾插,插入后判断长度是否 ≥8,满足则树化插入后判断元素数量是否 > 阈值,是则扩容
- HashMap 为什么线程不安全JDK1.7:头插法 → 扩容链表反转 → 形成环形链表 → get () 死循环JDK1.8:无锁结构 → 并发 put 可能导致:数据覆盖数据丢失扩容多次,数据错乱
- 高频面试题为什么容量是 2 的 n 次方?答:下标计算 hash & (len-1) 更高效,分布更均匀。为什么加载因子是 0.75?答:空间利用率与哈希冲突概率的平衡。为什么 HashMap 线程不安全?答:无锁,并发 put / 扩容会导致数据丢失、覆盖、死循环。
三、ConcurrentHashMap
- 作用线程安全的高效哈希表读操作几乎无锁,写操作低粒度加锁性能远优于 Hashtable、Collections.synchronizedMap
- JDK1.7 实现结构:Segment 分段 + HashEntry 数组 + 链表锁机制:分段锁(ReentrantLock)结构理解:一个 ConcurrentHashMap 包含多个 Segment每个 Segment 就是一个小 HashMap并发度:默认 16(最多 16 个线程同时写)锁粒度:锁整个 Segment优点:并发度高缺点:结构复杂,查询需要两次哈希
- JDK1.8 实现(重点)结构:Node 数组 + 链表 + 红黑树(结构同 HashMap1.8)锁机制:CAS + synchronized锁粒度:数组中的头节点(桶级别)读操作:无锁,只需 volatile 保证可见性
3.1 put 流程(JDK1.8)数组为空 → 初始化根据 hash 找到下标位置如果位置为空 → CAS 写入,不加锁如果位置有节点,且正在扩容 → 帮助扩容否则:synchronized 锁住头节点链表:遍历插入 / 覆盖红黑树:树插入判断是否需要树化 / 扩容
3.2 扩容机制支持 多线程协同扩容每个线程负责迁移一部分桶扩容时仍可并发访问,性能极高
3.3 关键特性size () 是弱一致性(不加锁统计)迭代器是 弱一致性,不会抛并发修改异常不允许 key/value 为 null
3.4 为什么 JDK1.8 用 synchronized 弃用分段锁锁升级优化后,轻量级锁 / 偏向锁成本极低锁粒度更细:从 Segment → 桶头节点结构更简单,查询更快并发性能更高
四、HashMap vs ConcurrentHashMap 总结
HashMap:线程不安全,高并发下会丢数据、死循环ConcurrentHashMap:线程安全,高并发场景首选JDK1.7:分段锁JDK1.8:CAS + synchronized + 桶粒度锁 + 红黑树