可以把 Unsafe 理解成:Java 里一个“非常底层、非常强大、也非常危险”的工具类。

它名字叫 Unsafe,不是因为它不好用,而是因为它绕过了很多 JVM 的安全保护机制,一旦用错,很容易导致内存泄漏、程序崩溃、数据错乱。


1. 为什么需要 Unsafe?

正常 Java 代码是运行在 JVM 里的,Java 程序不能像 C/C++ 一样直接操作内存地址,也不能直接调用 CPU 指令。

比如 Java 里你不能这样做:

int* p = 某个内存地址;
*p = 100;

因为 Java 为了安全,把这些底层操作都封装起来了。

但是 JDK 底层有些类,比如:

AtomicInteger
ConcurrentHashMap
ReentrantLock
ThreadPoolExecutor
LockSupport

它们为了实现高性能并发,就需要一些更底层的能力,例如:

CAS 操作
线程挂起和唤醒
内存屏障
直接操作对象字段
直接分配堆外内存

这些普通 Java 代码做不到,所以 JVM 提供了一个“后门”类:Unsafe


2. Unsafe 和 CAS 的关系

你前面学的原子类,例如:

AtomicInteger

它的核心操作本质上就是 CAS。

例如:

atomicInteger.compareAndSet(0, 1);

底层大概会走到类似这样的 Unsafe 方法:

unsafe.compareAndSwapInt(obj, offset, expect, update);

意思是:

如果 obj 对象中 offset 位置的值等于 expect,
就把它改成 update;
否则不修改。

这个操作不是普通 Java 代码完成的,而是 Unsafe 调用了 native 方法,最后依赖 CPU 的原子指令完成。

所以可以这样理解:

AtomicInteger
    ↓
Unsafe.compareAndSwapInt()
    ↓
native 方法
    ↓
CPU 原子指令,例如 cmpxchg

因此,Unsafe 是很多原子类实现 CAS 的底层入口


3. offset 是什么?

Unsafe 操作对象字段时,通常不是通过字段名操作,而是通过字段在对象内存中的偏移量操作。

例如:

class User {
    private int age;
}

正常 Java 代码访问:

user.age

但是 Unsafe 是这样理解的:

user 对象的起始地址 + age 字段的偏移量

比如:

age 字段在 User 对象中的偏移量是 12

那么 Unsafe 就可以直接定位到:

user 对象地址 + 12

然后修改这个位置上的值。

所以 Unsafe 可以做到:

修改对象字段
修改 private 字段
根据内存偏移量 CAS 修改字段

例如 AtomicInteger 里面会有类似逻辑:

private volatile int value;

private static final long valueOffset;

valueOffset 就是 value 字段在对象中的内存偏移量。

CAS 时不是说“修改 value 字段”,而是说:

修改当前 AtomicInteger 对象中 valueOffset 位置的那个 int 值

4. Unsafe 为什么能保证原子性?

以 CAS 为例:

compareAndSwapInt(obj, offset, expect, update)

这个操作包含三个步骤:

1. 读取内存中的当前值
2. 比较当前值是否等于 expect
3. 如果相等,就修改成 update

如果这三个步骤是普通 Java 代码执行的,那不是原子的。

但是 Unsafe 的 CAS 最终会调用 CPU 的原子指令。

例如 x86 CPU 里的:

cmpxchg

CPU 会保证这个比较并交换的过程不可被其他线程打断。

所以原子性不是 Java 自己保证的,而是:

Unsafe 调用 native 方法
native 方法调用 CPU 原子指令
CPU 保证该指令是原子的

5. Unsafe 能做哪些事?

你笔记里列的几类都可以这样理解。


1)内存操作

Unsafe 可以直接申请内存和释放内存。

类似 C 语言里的:

malloc()
free()

Java 里对应 Unsafe 的方法类似:

allocateMemory()
freeMemory()

这块内存不在 JVM 堆里,所以不受 GC 管理。

也就是说:

你申请了,就必须自己释放。

如果不释放,就会内存泄漏。

这就是为什么说 Unsafe 很危险。


2)对象字段操作

Unsafe 可以拿到对象某个字段的内存偏移量:

objectFieldOffset()

然后通过偏移量直接修改对象中的字段值。

即使字段是:

private

Unsafe 也可以绕过访问权限直接修改。

所以它可以破坏 Java 的封装性。


3)CAS 操作

这是并发中最常见的用途。

例如:

compareAndSwapInt()
compareAndSwapLong()
compareAndSwapObject()

这些方法是很多原子类的基础。

例如:

AtomicInteger
AtomicLong
AtomicReference
AtomicStampedReference

它们底层都依赖 CAS 思想。


4)线程挂起和恢复

Unsafe 里有:

park()
unpark()

这两个方法是 LockSupport 的底层基础。

例如:

LockSupport.park();
LockSupport.unpark(thread);

AQS 里面阻塞和唤醒线程,也会间接使用这套机制。

所以可以这么理解:

ReentrantLock 获取锁失败
    ↓
线程进入 AQS 队列
    ↓
LockSupport.park() 挂起线程
    ↓
Unsafe.park() 真正阻塞线程

当锁释放时:

LockSupport.unpark(thread)
    ↓
Unsafe.unpark(thread)
    ↓
唤醒等待线程

5)内存屏障 Fence

Unsafe 还提供内存屏障相关方法,例如:

loadFence()
storeFence()
fullFence()

它们用来限制 CPU 和编译器的指令重排序。

简单理解:

Fence 就像一道栅栏,防止栅栏前后的读写操作乱序执行。

这和 volatile、JMM 的有序性有关。


6. Unsafe 为什么危险?

因为它可以绕过 JVM 的保护。

比如:

第一,直接操作内存

申请的内存不受 GC 管理
忘记释放就内存泄漏
释放错了可能程序崩溃

第二,绕过访问权限

它可以修改 private 字段,破坏封装性。


第三,绕过类型安全

普通 Java 会检查类型,比如不能随便把一个对象当成另一个对象。

但 Unsafe 操作内存时,如果偏移量写错,可能把不该改的地方改了。


第四,可能导致 JVM 崩溃

普通 Java 程序最多抛异常。

但 Unsafe 操作底层内存出错时,可能不是抛异常,而是直接导致 JVM 崩溃。


7. 面试中怎么说 Unsafe?

可以这样回答:

Unsafe 是 Java 中一个非常底层的工具类,它提供了绕过 JVM 限制的能力,比如直接操作内存、直接修改对象字段、执行 CAS、线程 park/unpark、内存屏障等。很多 JUC 并发工具类底层都会用到 Unsafe,比如 AtomicInteger 通过 Unsafe 的 CAS 方法实现无锁原子更新,AQS 通过 LockSupport 间接使用 Unsafe 的 park/unpark 实现线程阻塞和唤醒。

但是 Unsafe 也很危险,因为它可以直接操作内存,且分配的堆外内存不受 GC 管理,如果使用不当会造成内存泄漏、破坏对象封装,甚至导致 JVM 崩溃。


8. 一句话总结

Unsafe 是 Java 提供给 JDK 底层使用的“后门工具类”,
它可以直接操作内存、对象字段、线程和 CPU 原子指令。
Atomic 原子类的 CAS、AQS 的线程阻塞唤醒等,底层都和 Unsafe 有关系。
但它绕过了 JVM 的安全机制,所以功能强大但非常危险。

AQS 全称是 AbstractQueuedSynchronizer,可以理解成:

Java JUC 包里用来实现锁和同步器的底层框架。

比如这些工具底层都和 AQS 有关:

ReentrantLock
CountDownLatch
Semaphore
ReentrantReadWriteLock

补充:AQS 主要干什么?

AQS 解决的是这个问题:

多个线程来抢一个资源,抢到的继续执行,抢不到的排队阻塞,资源释放后再唤醒等待线程。

所以它负责的事情是:

抢资源
排队
阻塞线程
唤醒线程

AQS 的两个核心

AQS 主要由两部分组成:

state 状态变量 + FIFO 等待队列

1. state:表示资源状态

AQS 里面有一个 volatile int state

不同工具里,state 的含义不同。

比如:

ReentrantLock:
state = 0    表示锁空闲
state = 1    表示锁被占用
state > 1    表示锁被同一个线程重入多次
CountDownLatch:
state 表示剩余计数
state = 3    表示还剩 3 个任务
state = 0    表示可以放行

AQS 通过 CAS 修改 state,保证多个线程同时修改时是安全的。


2. FIFO 等待队列:存放抢不到资源的线程

如果线程抢不到锁,就会被封装成一个 Node 节点,放进 AQS 的等待队列里。

大概是这样:

head <-> Node(t1) <-> Node(t2) <-> Node(t3) <-> tail

然后这些线程会被阻塞。

等锁释放后,AQS 会唤醒队列里的线程,让它重新尝试获取锁。


AQS 和子类怎么配合?

一句话:

AQS 负责排队、阻塞、唤醒;子类负责定义 state 的含义和获取/释放规则。

比如 ReentrantLock

AQS:我负责获取失败后排队、阻塞,释放后唤醒。
ReentrantLock:我告诉你 state=0 才能加锁,state>0 表示锁被占用。

流程是:

线程调用 lock()
    ↓
ReentrantLock 调用 AQS 的 acquire()
    ↓
AQS 调用 ReentrantLock 自己实现的 tryAcquire()
    ↓
如果 tryAcquire 返回 true,说明加锁成功
如果返回 false,AQS 就把线程放入队列并阻塞

释放锁时:

线程调用 unlock()
    ↓
ReentrantLock 调用 AQS 的 release()
    ↓
AQS 调用 ReentrantLock 自己实现的 tryRelease()
    ↓
如果 tryRelease 返回 true,说明锁彻底释放
    ↓
AQS 唤醒等待队列中的线程

独占模式和共享模式

AQS 支持两种模式:

独占模式

同一时刻只能有一个线程获取资源。

典型例子:

ReentrantLock

比如一把锁只能被一个线程持有。


共享模式

同一时刻可以有多个线程获取资源。

典型例子:

CountDownLatch
Semaphore
读写锁中的读锁

比如 CountDownLatch 中,计数归零后,多个等待线程都可以被放行。


最好记的版本

你可以直接记这句话:

AQS 是 JUC 里实现锁和同步器的基础框架,核心是 state 状态变量和 FIFO 等待队列。state 表示资源状态,CAS 修改 state;抢不到资源的线程进入等待队列并阻塞,资源释放后再唤醒队列中的线程。

再短一点:

AQS = state + FIFO 队列 + CAS + park/unpark

面试里可以这样说:

AQS 是 Java 并发包中的同步器框架,像 ReentrantLock、CountDownLatch、Semaphore 等都基于它实现。它内部通过一个 volatile int 类型的 state 表示同步状态,通过 CAS 修改 state;获取资源失败的线程会被封装成 Node 节点加入 FIFO 等待队列,并通过 LockSupport.park() 阻塞,释放资源时再通过 unpark 唤醒等待线程。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐