ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

并发安全内容

并发安全内容

JUC包下并发安全使用的类

线程池

  • ThreadPoolExecutor
  • Executors

并发集合类

  • ConcurrentHashMap
  • CopyOnWriteArrayList

同步工具类

  • CountDownLatch
  • CyclicBarrier
  • Semaphore

原子类

  • AtomicInteger
  • AtomicReference

Java中常用的锁和使用场景

  • 内置锁(synchronized):Java中的synchronized关键字是内置锁机制的基础,可以用于方法或代码块。当一个线程进入synchronized代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。其中,syncronized加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁则涉及操作系统级的互斥锁
  • ReentrantLock:java.util.concurrent.locks.ReentrantLock是一个显式的锁类,提供了比synchronized更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock使用lock()和unlock()方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿
  • 读写锁(ReadWriteLock):java.util.concurrent.locks.ReadWriteLock接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性
  • 乐观锁和悲观锁:悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。synchronized和ReentrantLock都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现
  • 自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源

synchronized锁的升级

  • 为什么要升级:在 JDK1.5 及之前,synchronized 完全依赖操作系统内核的互斥量实现,是典型的重量级锁,每次加锁解锁都需要完成用户态到内核态的切换,性能开销极大
  • 锁升级的哲学逻辑
能不加锁就不加锁(无锁) 只有一个线程来?那就记住你(偏向锁) 偶尔有第二个线程来?你们自旋抢一下就行(轻量级锁) 很多人来抢?老实排队,让操作系统管(重量级锁)

Mark Word

  • Mark Word存在对象头中
  • 对象头是 JVM 存储对象元信息的地方,和你写的业务字段无关。主要包含 Mark Word(存锁信息、hashcode、GC年龄)和 类型指针(指向类的元数据)
┌──────────────────────────────────────────────────────┐ │ │ │ 一个 Java 对象在内存中的布局 │ │ │ ├──────────────────────────────────────────────────────┤ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ 对象头 (Header) │ │ │ │ ┌─────────────────────┬────────────────┐ │ │ │ │ │ Mark Word │ Klass Pointer │ │ │ │ │ │ (64 bit) │ (32/64 bit) │ │ │ │ │ └─────────────────────┴────────────────┘ │ │ │ │ ┌────────────────────────────────────┐ │ │ │ │ │ Array Length (仅数组对象有) │ │ │ │ │ │ (32 bit) │ │ │ │ │ └────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ 实例数据 (Instance Data) │ │ │ │ 对象的字段值(int age, String name ...) │ │ │ └────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ 对齐填充 (Padding) │ │ │ │ 使对象总大小为 8 字节的整数倍 │ │ │ └────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────┘
  • Mark Word里面存的东西
  • 锁信息 当前是偏向锁/轻量级锁/重量级锁,以及谁持有锁
  • hashcode 调用 hashCode() 后存入,懒加载的
  • GC分代年龄 4 bit,所以最大值 15,这就是为什么最大分代年龄是 15
  • 类型指针相关 指向对象属于哪个类
┌─────────────┬──────────────────────────────────────┬────────┬─────────────┐ │ 状态 │ 内容 (64 bit) │ 标志位 │ 是否存锁信息│ ├─────────────┼──────────────────────────────────────┼────────┼─────────────┤ │ │ unused:25 │ │ │ │ 无锁 │ hashcode:31 │ 001 │ 否 │ │ │ age:4 │ │ │ │ │ biased_lock:0 │ │ │ ├─────────────┼──────────────────────────────────────┼────────┼─────────────┤ │ │ thread:54 │ │ │ │ 偏向锁 │ epoch:2 │ 101 │ 是(偏向) │ │ │ age:4 │ │ │ │ │ biased_lock:1 │ │ │ ├─────────────┼──────────────────────────────────────┼────────┼─────────────┤ │ │ ptr_to_lock_record:62 │ 00 │ 是(轻量) │ │ 轻量级锁 │ │ │ │ ├─────────────┼──────────────────────────────────────┼────────┼─────────────┤ │ │ ptr_to_heavyweight_monitor:62 │ 10 │ 是(重量) │ │ 重量级锁 │ │ │ │ ├─────────────┼──────────────────────────────────────┼────────┼─────────────┤ │ │ 空 │ 11 │ 否 │ │ GC标记 │ │ │ │ └─────────────┴──────────────────────────────────────┴────────┴─────────────┘

从低级到高级锁

无锁(0 01)
所有线程都可以自由访问,没有任何锁信息。 对象刚 new 出来就是这个状态。 hashcode、GC分代年龄都存在 Mark Word 里。
偏向锁(1 01)
Thread A 第一次进入 synchronized 块 │ ├─ 用 CAS 把自己的线程ID 写入 Mark Word │ └─ 写入成功 → 以后 Thread A 再来,直接比较线程ID 匹配就直接进入,0 开销(连CAS都不用) Thread A 再次进入: 只需要检查 Mark Word 中的 threadID == 自己的 ID? ├─ 是 → 直接进入(无任何同步操作,极致优化) └─ 否 → 说明有另一个线程来了,触发撤销 Thread B 也来申请这把锁 │ ├─ Thread B 检查发现 threadID ≠ 自己 │ └─ 触发偏向锁撤销: 1. 暂停拥有偏向锁的线程 Thread A(需要等安全点 safepoint) 2. 检查 Thread A 是否还活着 │ ├─ Thread A 已退出同步块 → 锁对象恢复为无锁状态 │ 然后 Thread B 重新尝试获取 │ └─ Thread A 还在同步块中 → 升级为轻量级锁
轻量级锁(0 0)
  • 一般锁都是synchronized(对象)
  • 线程要用这个锁的时候,会将这个对象的Mark Word复制到这个线程的Lock Record中,然后把对象的Mark Word指向线程的Lock Record中
  • 不用的时候写回去
  • 拿锁的过程
Thread A 要执行 synchronized(obj): 第1步:在 Thread A 的栈帧中创建一个 Lock Record ┌─────────────────┐ │ Lock Record │ │ [ 空 ] │ ← 还没填东西 └─────────────────┘ 第2步:把 obj 的 Mark Word 复制到 Lock Record 里 ┌─────────────────┐ obj 对象头 │ Lock Record │ ┌──────────────┐ │ [ hashcode, │ ←── │ Mark Word │ │ age, 001 ] │ 复制 │ hashcode, │ └─────────────────┘ │ age, 001 │ └──────────────┘ 第3步:CAS 修改 obj 的 Mark Word,让它指向 Lock Record obj 对象头 ┌──────────────────────┐ │ ptr → Lock Record │00│ ← Mark Word 变成了指针 + 00标志 └──────────────────────┘ CAS 的含义: 比较 obj 的 Mark Word 是否还是第2步复制时的那个值 ├─ 是 → 说明没人抢,成功,把 Mark Word 改成指针 └─ 不是 → 说明有人抢了,CAS 失败 第4步:CAS 成功 → Thread A 获得轻量级锁,进入同步块
  • 释放锁的过程
Thread A 退出 synchronized(obj): 第1步:把 Lock Record 里备份的 Displaced Mark Word 通过 CAS 写回 obj 的 Mark Word obj 对象头 ┌──────────────────────┐ ┌─────────────────┐ │ hashcode, age, 001 │ ←──CAS──│ Lock Record │ └──────────────────────┘ │ [ hashcode, │ │ age, 001 ] │ └─────────────────┘ 第2步:CAS 成功 → 释放完毕,对象恢复无锁状态 CAS 失败 → 说明锁已经膨胀成重量级锁了,需要唤醒等待的线程
  • 竞争过程
Thread A 正持有轻量级锁 Thread B 这时也想获取这把锁 第1步:Thread B 执行 CAS 尝试修改 Mark Word → 失败(已被 A 改了) 第2步:Thread B 不会立即放弃,而是进入 "自旋" 状态 自旋 = 一个 while 循环,反复尝试 CAS while (true) { if (CAS 成功) { 拿到锁,退出循环; break; } // 失败了就继续循环,再试 } 第3步:自旋结果有两种: ├─ 自旋过程中 Thread A 释放了锁 → Thread B CAS 成功,拿到锁 │ (皆大欢喜,全程用户态,没有系统调用) │ └─ 自旋次数超过阈值还没拿到 → 膨胀为重量级锁(下面讲)
重量级锁(0 1)
  • Monitor
每个 Java 对象都可以关联一个 Monitor 对象(重量级锁激活时才创建) ┌─────────────────────────────────────────┐ │ ObjectMonitor │ │ │ │ _owner ──────→ 当前持有锁的线程 │ │ (只有一个) │ │ │ │ _EntryList ──→ 排队等锁的线程队列 │ │ (都睡着,等被唤醒) │ │ │ │ _WaitSet ────→ 调用了 wait() 的线程 │ │ (主动等待的线程) │ │ │ │ _count ──────→ 重入计数 │ │ (同一线程进入几次) │ └─────────────────────────────────────────┘
  • 加锁过程
Thread C 想获取 obj 的锁: 第1步:JVM 检查 obj 的 Mark Word,发现是重量级锁(标志位 10) Mark Word → 指向一个 ObjectMonitor 第2步:检查 Monitor 的 _owner ├─ null → CAS 设置自己为 _owner,拿到锁 ✓ ├─ 就是自己 → 重入,_count++,拿到锁 ✓ └─ 其他线程 → Thread C 进入阻塞流程 ↓ 第3步:阻塞流程(开销大就在这里) ① Thread C 被放入 _EntryList 队列 ② Thread C 调用操作系统的 mutex_lock() 系统调用 ③ OS 把 Thread C 挂起(从运行态 → 阻塞态) ④ 这涉及 用户态 ↔ 内核态 切换,成本极高 ⑤ Thread C 就睡着了,不消耗 CPU,但上下文切换代价大
  • 释放锁过程
Thread A 退出 synchronized: 第1步:设置 _owner = null 第2步:从 _EntryList 中唤醒一个等待的线程 第3步:被唤醒的线程重新竞争锁 (不是直接给它,而是让它和其他线程公平竞争)
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进