跟着AI学习Java,第二天

7 阅读9分钟

第 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. 线程首次获取锁 → 计数器 + 1,关联到 monitor 的 _owner
    2. 同一线程再次获取 → 计数器 + 1
    3. 线程释放锁 → 计数器 - 1
    4. 计数器 = 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 核心结构:

    1. 同步状态(state):0 = 无锁,>0 = 有锁(可重入次数)
    2. 等待队列:双向链表,存放等待获取锁的线程
    3. 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 对比(完整表格)
特性synchronizedReentrantLock
锁获取方式自动获取手动 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 优缺点
  • 优点:无锁,避免线程切换开销,并发性能高

  • 缺点:

    1. ABA 问题:变量从 A→B→A,CAS 认为未修改(解决:加版本号,如 AtomicStampedReference)
    2. 自旋开销:高并发下自旋次数多,消耗 CPU
    3. 只能修改单个变量,无法保证复合操作原子性
4.4 常用原子类(基于 CAS)
  • AtomicInteger/AtomicLong:基本类型原子操作
  • AtomicReference:引用类型原子操作
  • AtomicStampedReference:解决 ABA 问题
  • LongAdder:高并发下替代 AtomicLong,分段 CAS,性能更高

二、HashMap(JDK1.7 & JDK1.8)

  1. 整体结构JDK1.7:数组 + 链表JDK1.8:数组 + 链表 + 红黑树
  2. 核心参数默认初始容量:16(必须是 2 的 n 次方)加载因子:0.75扩容阈值:容量 × 加载因子树化条件:链表长度 ≥ 8且 数组长度 ≥ 64退化为链表:红黑树节点 ≤ 6
  3. 哈希计算原理先计算 key 的 hashCode ()再做 扰动函数:让高位参与运算,减少哈希冲突最终下标:hash & (数组长度 - 1) 等价于取模,但速度更快
  4. JDK1.7 完整细节底层数组:Entry<K,V>[] table哈希冲突解决:链地址法插入方式:头插法(新节点放在链表头部)扩容机制:新容量 = 旧容量 << 1(翻倍)所有元素 重新计算 hash 重新排布并发问题:多线程扩容时,链表可能 反转成环调用 get () 时出现 死循环,CPU 100%
  5. JDK1.8 完整细节底层数组:Node<K,V>[] table插入方式:尾插法(解决了扩容成环问题)扩容优化:不需要重新完整计算 hash新下标 = 原下标 或 原下标 + 旧容量判断依据:hash & 旧容量,结果为 0 不变,非 0 则加旧容量查询效率:链表:O (n)红黑树:O (logn)
  6. JDK1.8 put 流程(完整)如果数组为空,先执行 resize () 初始化根据 key 计算 hash,得到数组下标如果该下标位置 没有节点,直接新建 Node 放入如果有节点:key 完全相同 → 直接覆盖 value如果是红黑树节点 → 执行红黑树插入如果是链表 → 尾插,插入后判断长度是否 ≥8,满足则树化插入后判断元素数量是否 > 阈值,是则扩容
  7. HashMap 为什么线程不安全JDK1.7:头插法 → 扩容链表反转 → 形成环形链表 → get () 死循环JDK1.8:无锁结构 → 并发 put 可能导致:数据覆盖数据丢失扩容多次,数据错乱
  8. 高频面试题为什么容量是 2 的 n 次方?答:下标计算 hash & (len-1) 更高效,分布更均匀。为什么加载因子是 0.75?答:空间利用率与哈希冲突概率的平衡。为什么 HashMap 线程不安全?答:无锁,并发 put / 扩容会导致数据丢失、覆盖、死循环。

三、ConcurrentHashMap

  1. 作用线程安全的高效哈希表读操作几乎无锁,写操作低粒度加锁性能远优于 Hashtable、Collections.synchronizedMap
  2. JDK1.7 实现结构:Segment 分段 + HashEntry 数组 + 链表锁机制:分段锁(ReentrantLock)结构理解:一个 ConcurrentHashMap 包含多个 Segment每个 Segment 就是一个小 HashMap并发度:默认 16(最多 16 个线程同时写)锁粒度:锁整个 Segment优点:并发度高缺点:结构复杂,查询需要两次哈希
  3. 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 + 桶粒度锁 + 红黑树