【JUC】Unsafe
可以把 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 唤醒等待线程。
更多推荐




所有评论(0)