文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Java 锁相关详解 【一、锁的基础概念】

一、锁的基础概念(专业加强版)

目标:从 硬件 → JMM → 语言/库语义 三层逐步压实认识,厘清“为什么需要锁”“锁到底保障了什么”“不用锁如何正确”。


1.1 并发的硬件背景与风险来源

1.1.1 多级缓存与一致性

  • 现代 CPU 以 L1/L2/L3 多级缓存 + 写缓冲(write buffer) 提升吞吐,导致线程可能读到过期值
  • 缓存一致性协议(如 MESI) 只保证单一地址的读写可见性收敛不保证跨地址的顺序(这正是“有序性”问题产生的根源)。
  • Cache Line(常见 64B) 是一致性与伪共享的粒度:不同线程写入同一行内不同变量,也会相互抖动(false sharing)。

1.1.2 指令重排与内存次序

  • 编译器、JIT、CPU 都可进行 as-if-serial 层面的重排(在单线程下不改变结果的前提)。
  • 内存模型差异:x86 接近 TSO(较强),ARM/POWER 等更弱;这影响屏障需求。
  • 直观结论:不加同步,你无法要求“先写 A 再写 B,另一个线程一定按 A→B 的顺序看到”。

1.2 Java 内存模型(JMM)核心

JMM 通过一组“同步动作(synchronization actions)”和“happens-before(HB) 规则”定义“何为正确的并发可见性与顺序”。

1.2.1 同步动作(不完全列举)

  • monitorenter/monitorexitsynchronized
  • volatile 读/写
  • 线程生命周期:Thread.start()Thread.join()interrupt()
  • 对象构造的 final 字段发布语义
  • 显式栅栏(VarHandle.fullFence/acquireFence/releaseFence 等)

1.2.2 happens-before(HB)规则(关键子集)

  1. 程序次序规则:同一线程内,程序顺序上的前操作 HB 后操作。
  2. 监视器锁规则:对同一把锁的解锁 HB 其后对同一把锁的加锁。
  3. volatile 规则:对某个 volatile 变量的写 HB 随后对该变量的读。
  4. 线程启动规则Thread.start() HB 被启动线程中的第一个动作。
  5. 线程终止/等待规则:线程中的所有动作 HB 成功的 Thread.join() 返回。
  6. 中断规则:对线程 interrupt() HB 该线程检测到中断(如 isInterrupted() 返回 true)。
  7. 传递性:HB 具有传递性(A→B 且 B→C 则 A→C)。

SC-for-DRF:若程序无数据竞争(Data-Race-Free),JMM 保证其表现为顺序一致性。因此正确同步(建立 HB)是根本。

1.2.3 原子性、可见性、有序性在 JMM 下的落点

  • 原子性volatile 的单次读写是原子的(long/double 在 JDK5+ 也原子);复合操作需要锁或 CAS。
  • 可见性synchronized 解锁与 volatile 写会把变更刷新至主内存,后续加锁/读取方看见
  • 有序性volatile 写具有 release 语义,volatile 读具有 acquire 语义;锁解/加同理提供 释放/获取 语义。

1.3 Java 语言与库级同步语义对照

原语/机制可见性有序性原子性进阶特性典型用途
synchronized✅(锁释放/获取的内存语义)✅(临界区互斥)可重入、JIT 优化(消除/粗化)严格互斥、简单正确
volatile✅(W:release / R:acquire)❌(仅单次读写)低开销标志位、DCL 指针可见性状态标志、安全发布引用
J.U.C Lock(如 ReentrantLock✅(与 synchronized 等价)可中断、公平性、tryLock可控性强的互斥
ReadWriteLock / StampedLock部分(读共享/写独占)乐观读/降级、读多写少优化高读场景
原子类(Atomic*✅(CAS 有 acquire/release 语义)复合操作借助 CAS无锁化、性能好计数器、指针交换
VarHandle / Fences✅(显式 fence 精细控制)依实现近底层、可移植构建并发原语

说明:实际的“屏障”是编译器/JIT/CPU 的实现细节。JMM 只承诺语义,JIT 在不同 CPU 上插入合适屏障(如 x86 MFENCE 仅用于 Store→Load)。


1.4 可见性与有序性陷阱:双重检查锁(DCL)

1.4.1 错误示例(缺 volatile

class Singleton {
    private static Singleton INSTANCE; // 缺 volatile!

    private Singleton() {}

    static Singleton getInstance() {
        if (INSTANCE == null) {                 // 1. 读
            synchronized (Singleton.class) {
                if (INSTANCE == null) {         // 2. 再读
                    INSTANCE = new Singleton(); // 3. 赋值
                }
            }
        }
        return INSTANCE;
    }
}

问题:对象创建可被重排为:分配内存 → 将引用赋给 INSTANCE → 调用构造函数。
其他线程可能在构造未完成前读到非空引用(半初始化)。

1.4.2 正确示例(加 volatile

class Singleton {
    private static volatile Singleton INSTANCE;

    private Singleton() {}

    static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

volatile 保障写入具有 release 语义,读具有 acquire 语义,禁止关键重排,确保安全发布


1.5 安全发布(Safe Publication)与对象不变性

1.5.1 四种可靠发布方式

  1. 静态初始化:类初始化阶段天然同步。
  2. 经由 volatile 引用发布volatile 写建立发布 HB。
  3. 经由 final 字段:构造函数内不泄漏 thisfinal 字段对读取方有额外可见性保障(初始化安全性)。
  4. 经由恰当同步的容器:比如放入 ConcurrentHashMapBlockingQueue 之后再被其他线程获取。

1.5.2 反例:构造期间泄漏 this

class Bad {
    static List<Bad> holder = new ArrayList<>();
    final int x;

    Bad() {
        holder.add(this);  // 泄漏 this
        x = 42;            // final 字段,可能对读者不可见为 42
    }
}

在构造过程完成前把 this 暴露给其他线程,会破坏 final 的初始化保障。


1.6 “锁”与“无锁”的进度保证(Liveness/Progress)

1.6.1 进度属性

  • 安全性(Safety):永不发生坏事(如不同时进入临界区)。

  • 活性(Liveness):好事最终发生(不会永远卡住)。

  • 无锁进度级别

    • Wait-freedom:任何线程在有限步内完成。
    • Lock-freedom:系统整体持续前进(个别线程可能饿死)。
    • Obstruction-freedom:无竞争时有限步内完成。

1.6.2 CAS 与 ABA

  • CAS 是无锁算法基石,存在 ABA 问题:值从 A→B→A,CAS 误判为未变。
  • 方案:版本戳(AtomicStampedReference)、指针分离、/重用避免、或更高层协议。

1.7 屏障与硬件模型的直觉映射

JMM 层语义常见实现(x86/ARM 粗略直觉)说明
释放(Release)写写有序;必要时 StoreStore/StoreLoad使先前写对他线程可见
获取(Acquire)读后不越过;必要时 LoadLoad/LoadStore禁止后续读写越过该读
全栅栏(Full Fence)MFENCE/DMB SY阻断一切重排(保守)

注:具体由 HotSpot C2/JIT 选择最小必要指令;我们在应用层只需依赖语义而非具体指令。


1.8 性能度量与基准测试方法论

1.8.1 微基准常见坑

  • 死代码消除/DCE逃逸分析导致测试代码被优化掉。
  • JIT 暖身(warmup)不足导致结果抖动。
  • 缓存副作用、伪共享干扰吞吐。

1.8.2 建议:使用 JMH

@State(Scope.Group)
public class CounterBench {
    private final AtomicLong a = new AtomicLong();
    private long b;

    @Group("atomic")
    @Benchmark
    @GroupThreads(4)
    public void atomicInc() {
        a.incrementAndGet();
    }

    @Group("plain")
    @Benchmark
    @GroupThreads(4)
    public void plainInc() { // 非线程安全,仅作对照
        b++;
    }
}
  • Warmup/MeasurementFork 合理设置;观察 p99 延迟吞吐,而非单一平均值。

1.9 以图析理:JMM 的 HB 关系与发布路径

在这里插入图片描述


1.10 工程实践清单(Checklist)

  • 是否存在数据竞争:所有共享可变状态要么在锁保护下访问,要么经由 volatile/安全发布。
  • 是否正确发布:对象构造完成后再暴露;必要时使用 volatile 引用或并发容器传递。
  • 是否需要互斥:复合读改写选择 synchronized/Lock 或使用原子类封装逻辑。
  • 是否读多写少:考虑 ReadWriteLock / StampedLock(注意写偏与饥饿)。
  • 是否有伪共享:高写热点结构考虑填充/分离字段(如 @Contended,需 -XX:-RestrictContended)。
  • 是否可替代为无锁结构:队列/计数器等优先尝试原子类与现成并发容器。
  • 压测与实证:用 JMH / 生产压测验证假设,结合火焰图与事件记录(JFR)。

1.11 关键结论(TL;DR)

  1. HB 是一切并发正确性的根:不用 HB(锁/volatile/线程边界等)就无权要求他线程“看见”或“按顺序”。
  2. volatile ≠ 锁:它提供可见性与(受限的)有序性,但不提供互斥,无法保障复合操作安全。
  3. 安全发布优先:发布路径决定对象可见性的上限——错误发布会让所有后续同步失效。
  4. 度量先于优化:不要依靠直觉优化并发,先用工具实证,再选择锁/无锁/读写锁等策略。
posted @ 2025-09-04 16:40  NeoLshu  阅读(3)  评论(0)    收藏  举报  来源