并发控制涵盖三种核心策略:悲观锁的“先锁后操作”独占模式、乐观锁的硬件原语无锁机制以及数据隔离的空间换时间方案。本文深入字节码与JDK源码层,全面解析无锁并发CAS、底层Unsafe类及线程级隔离ThreadLocal的底层原理。

i++ 不是线程安全的?许多开发者误以为 i++ 仅有一行代码因而属于原子操作。实际在 JVM 字节码层面,该操作被拆解为多步复合指令,构成典型的 “读-改-写” 流程:
getstatic i // 1. 【读】从主内存中读取静态变量 i 的值,压入当前线程的操作数栈
iconst_1 // 2. 【备】将常量 1 压入当前线程的操作数栈
iadd // 3. 【算】将栈顶的两个值相加(即执行 i + 1)
putstatic i // 4. 【写】将计算后的新结果写回主内存的静态变量 i
这 4 条指令缺乏原子性,在多线程高并发场景下极易引发 写丢失 (Lost Update) 。
经典面试高频题:两个线程并发各对同一个整型变量 i(初始为 0)进行 50 次 i++,最终结果可能是什么?
AtomicInteger 原子类。悲观锁:以 synchronized 或 ReentrantLock 为代表。假设冲突概率极高,强行让线程排队阻塞,涉及用户态与内核态的切换,开销较重。
乐观锁:以 CAS 机制为代表。假设冲突概率极低,平时不加锁,最后提交时对比数据。
避坑指南:为什么“写多读少”的高激烈竞争场景下绝对不能用乐观锁?
如果 100 个线程同时 Update 同一条数据,乐观锁下仅有 1 个能成功,其余 99 个全部失败。若代码内部使用 while 循环让其不断自旋重试,这 99 个线程将疯狂空转 CPU,短时间内会把 CPU 瞬间打满导致整个系统雪崩。此时宁愿使用悲观锁让线程在队列中安全阻塞休息,让出 CPU 资源。
CAS 是一种无锁并发(乐观锁)机制,其核心操作依赖三个关键值:
[线程工作内存 (E, N)] --------带着(E,N)回到主存-------> [主内存实际值 (V)]
↓
比对:V == E ?
/
(是:没人动过) (否:已被篡改)
/
[更新成功: V = N] [更新失败: 自旋重试]
“比较”和“交换”看似两步动作,但绝不会发生中间被切走篡改的情况。Java 自身并不执行此比对逻辑,而是通过 Unsafe 类调用硬件级别的原子指令(如 x86 架构下的 cmpxchg 指令)。由于是 CPU 原语级别的指令,它在硬件层面保证了执行过程不可被打断,天然具备原子性。
缺陷 1:ABA 问题
主内存的值经历了 A -> B -> A 的过程。由于最终值依然是 A,普通的 CAS 会误判为“期间没有人动过”从而放行。这在链表数据结构中可能导致节点错乱。
1A -> 2B -> 3A)。JUC 包提供了 AtomicStampedReference(带邮戳的原子引用)来彻底解决此陷阱。缺陷 2:极端高并发下自旋耗尽 CPU
竞争极其激烈时,大量线程在自旋死循环中重试,导致 CPU 飙升。
LongAdder,采用分段锁/Cell 数组化的思想。将单一变量的累加分散到多个独立的 Cell 中,最后求和,将高并发的单点竞争转化为多点并发,大幅降低自旋概率。缺陷 3:只能保证单个共享变量的原子操作
AtomicReference 类,将多个变量包装合成一个联合对象进行整体 CAS 替换。Unsafe 是位于 sun.misc 包下的底层工具类。Java 本身受限于虚拟机的沙箱机制,无法直接访问底层操作系统和硬件。Unsafe 类就像是 JVM 开辟的一个“后门”,其内部全都是 native 本地方法,允许 Java 代码直接调用 C/C++ 绕过虚拟机限制。
Unsafe 提供了极高性能的硬件级操作,主要涵盖以下四个特权维度:
| 核心特权 | 核心机制与原理解析 | 工业级实战落地场景 |
|---|---|---|
| 1. 内存操作 | 绕过 JVM,直接在操作系统中分配、修改和释放堆外内存(Off-Heap) 。 | 高性能 I/O(如 Java NIO 中的 DirectByteBuffer),实现“零拷贝”提升网络与磁盘吞吐量。 |
| 2. 对象与字段操作 | 精准获取字段在对象内存中的绝对偏移量,且能无视 private 修饰符强行修改字段值。 |
各种原子类初始化时,通过它提前获取 valueOffset 的内存物理地址。 |
| 3. CAS 操作 | 提供底层原语,直接向 CPU 发送 compareAndSwapInt 等高并发硬件级指令。 |
它是 AtomicInteger、AtomicLong 等原子工具类的绝对发动机。 |
| 4. 线程调度 | 提供 park() 和 unpark() 方法,精准地将特定某个线程挂起(阻塞休眠)或唤醒。 |
JUC重量级组件 AQS (AbstractQueuedSynchronizer) 和 LockSupport 的底层休眠与唤醒实现。 |
通过 Unsafe 分配的堆外内存完全脱离了 JVM 垃圾回收器 (GC) 的管辖。如果开发人员分配了内存但忘记手动调用 freeMemory() 释放,这部分内存将永远无法被回收,导致严重的系统级内存泄漏,直接压垮宿主机。因此,默认情况下普通 Java 代码是不被允许直接实例化使用它的。
面对并发冲突,悲观锁的逻辑是“大家排队抢”,乐观锁是“大家试着抢”。而 ThreadLocal 换了一个降维打击的思路:空间换时间,干脆不抢了,我给每个线程发一份专属的变量副本。每个线程独立安全操作自己的数据,互不干扰,天生免疫线程安全问题。
初学者极易误以为是 ThreadLocal 内部维护了一个 Map 来包含所有线程的数据。真实的架构恰恰相反:
Thread (学生对象) :每个 Thread 对象内部,都揣着一个专属的私有口袋,即 ThreadLocalMap 成员变量。
ThreadLocal (数学试卷) :通常声明为全局 private static final 共享对象。
Entry (专属答题卡盲盒) :口袋里放的是一个 Entry 数组。
ThreadLocal 本身(通过弱引用细棉线拴着)。set & get)set(T value) 源码逻辑public void set(T value) {
// 1. 获取当前正在执行的线程对象
Thread t = Thread.currentThread();
// 2. 掏出该线程自带的私有口袋
ThreadLocalMap map = getMap(t);
if (map != null)
// 3. 口袋已存在,则将当前 ThreadLocal 对象 (this) 作为 Key 存入 value
map.set(this, value);
else
// 4. 口袋还未初始化,则帮该线程创建专属的 ThreadLocalMap 并存入首个数据
createMap(t, value);
}
get() 源码逻辑public T get() {
Thread t = Thread.currentThread(); // 1. 获取当前线程
ThreadLocalMap map = getMap(t); // 2. 取出线程内部的 Map
if (map != null) {
// 3. 以当前 ThreadLocal 对象 (this) 作为 Key,快速计算哈希下标寻找 Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value; // 4. 找到盲盒,拆出强引用的业务数据并返回
return result;
}
}
// 5. 如果 map 为空或没找到,返回初始值 (通常是 null)
return setInitialValue();
}
哈希冲突解决:线性探测法 (Linear Probing)
与 HashMap 冲突时采用的链表法/红黑树不同,ThreadLocalMap 采用的是最原始的线性探测法。如果计算出的数组下标坑位已经被别的 ThreadLocal 占了,它不会悬挂链表,而是直接挨个往下寻找下一个空着的槽位。
父子线程传递:InheritableThreadLocal
传统的 ThreadLocal 属于绝对隔离,主线程开启的子线程是绝对拿不到主线程口袋里的数据的。为了解决链路上子线程继承上下文的问题,JDK 提供了 InheritableThreadLocal。它的实现原理是在 Thread 类初始化(init 方法)创建新线程时,如果发现父线程有 inheritableThreadLocals 拷贝,则在创建子线程时将父线程的口袋进行一次全量深拷贝。
在企业生产环境的大型工程中,绝大多数线程都是放在线程池中反复复用、长期不死的。这导致了以下严重的内存泄露链条:
外部强引用消失
↓
发生 GC 垃圾回收
↓
Key 是【弱引用】 ──→ 瞬间断裂 ──→ Key 变为了 null
↓
Value 是【强引用】 ──→ 依然存在坚不可摧的强引用链:
Thread (不死) -> ThreadLocalMap -> Entry -> Value
↓
最终结果:Map中积压大量 Key 为 null 但 Value 占用堆内存的“孤儿数据”。
由于Key已经变成 null,代码永远无法再访问到这些 Value,而 GC 也由于强引用链存在无法对其回收。
↓
日积月累,内存疯狂堆积,最终引发 OutOfMemoryError (OOM) 崩溃。
// Entry 源码:继承自弱引用 WeakReference
static class Entry extends WeakReference> {
Object value; // v 是无保护的强引用
Entry(ThreadLocal<?> k, Object v) {
super(k); // 把 Key 绑在弱引用线上,极其脆弱
value = v;
}
}
由于这个致命的盲盒设计,在 Web 开发(如拦截器、过滤器存储用户信息上下文)或线程池异步调用中,使用完毕后必须养成在 finally{} 代码块中显式手动调用 remove() 方法的习惯:
private static final ThreadLocal USER_HOLDER = new ThreadLocal<>();public void doBusiness(UserContext ctx) {
try {
USER_HOLDER.set(ctx); // 1. 存入专属口袋
// 2. 执行核心业务链路...
} finally {
// 3. 【致命关键】强制清除当前线程 Map 里的 Entry 盲盒,将 Key 和 Value 一起干掉!
USER_HOLDER.remove();
}
}
CAS借助CPU原语实现无锁并发但需防范ABA与自旋空转问题,Unsafe赋予底层硬件操作能力却伴随堆外内存泄漏风险,ThreadLocal以空间换时间实现线程隔离但务必在finally中手动调用remove()清除Entry。三者相辅相成,共同构成Java高并发编程的核心技术基石。