先来一个核心判断:VarHandle 对数组做原子操作时,**并不会主动帮你检查索引是否越界**。越界后会怎样?和普通数组访问一样,JVM 会在运行时直接抛出 ArrayIndexOutOfBoundsException。所以,在高并发场景下,如果多个线程同时用非法索引去调用 VarHandle 的 get、set 或 compareAndSet,异常该抛还是会抛,VarHandle 本身并不会替你兜底。

Why?为什么 VarHandle 不做这件事
说到底,VarHandle 的设计哲学就是追求极致性能,同时把底层控制权交给你。它绕过了 AtomicIntegerArray 这类封装类里内置的那些边界校验、null 数组判断和统一 volatile 语义包装。你可能会好奇:JDK 9+ 里 AtomicIntegerArray 不就是基于 VarHandle 实现的吗?没错,但它额外加了一层安全层。直接使用 VarHandle,等于你手动把这层防护给摘掉了。
- 省去边界检查 → 减少分支预测失败和指令开销 → 更适合高频、确定性索引的场景
- 代价也很明确:你必须自己确保索引合法,否则异常在运行时爆发,不会有任何提前预警
- 尤其在并发循环、分段处理或动态计算索引(比如 hash 映射、轮询偏移)时,逻辑上稍微出点偏差,就容易漏判
高并发下,越界防护应该怎么做
既然不能指望 VarHandle 自己来检查,那就得把检查前置,而且要做得轻量、线程安全,自然融入业务逻辑中。
- 索引预校验 + 快速失败:在调用 VarHandle 方法前,加一句
index >= 0 && index < array.length的判断。这个判断开销极小,而且可以被内联优化;代价比捕获异常要低两个数量级 - 避免在循环条件里隐式越界:比如
for (int i = start; i <= end; i++)这种写法,很容易多走一轮。统一用i < end或严格按预计算的合法区间来执行,会更安全 - 对动态索引做归一化:比如用模运算取余得到有效下标(
int safeIdx = Math.floorMod(hash, array.length)),比事后检查更高效稳定 - 结合 ThreadLocal 或分段锁缩小校验粒度:如果某段索引只被固定的线程访问,可以在初始化阶段一次性校验并缓存合法范围,避免每次重复判断
和 AtomicIntegerArray 的开销对比
直接手写 VarHandle 能省掉的,其实就是 AtomicIntegerArray 封装带来的那笔“安全税”:
AtomicIntegerArray每次操作都强制检查array != null和index边界——哪怕你 100% 确定不会越界- 它的
get/set是 public final 方法,无法被 JIT 内联成单条 CPU 指令;而静态获取的 VarHandle 可以内联成近乎原生的内存访问 - 如果你的索引来源可信(比如预分配固定任务槽、RingBuffer 固定大小),VarHandle 这种零开销跳过的特性,就是合理的选择
实际编码时的一点建议
不要为了“看起来更底层”就裸用 VarHandle。动手之前,先问自己三个问题:
- 索引是否全部来自可控输入(比如配置、预计算结果),还是可能混入用户输入、网络数据或文件内容?
- 是否已经有其他机制在兜底,比如限流、预校验层、断言?
- 压测中,VarHandle 带来的微秒级收益,是否真的比一次未捕获的越界异常导致线程中断更值得?
多数业务系统,用 AtomicIntegerArray 更稳妥。只有那些高性能中间件、底层框架或已经充分验证的热路径,才适合切换到 VarHandle,并自行承担起索引守门的责任。