在 Java 9 引入 java.lang.invoke.VarHandle 之后,字段级原子操作不再只有 AtomicLong 和同步块两条路。对于 long 类型字段,VarHandle 提供了一组专门的位操作访问模式,它们把读、改、写三个动作合并为一次原子执行,特别适合用单个 long 值维护多个布尔标志的场景。例如连接池中每个连接是否空闲、任务调度器中每个队列是否就绪,都可以压缩到一个 long 的 64 个 bit 中,再通过 VarHandle 完成无锁修改。

为什么 long 字段的位运算需要原子化
一个 long 类型字段天然拥有 64 个 bit,如果只是单线程环境,使用 value |= mask、value &= ~mask 这样的写法既简单又高效。但一旦多个线程同时修改不同 bit,问题就出现了:读 value、做位运算、再写回 value 并不是一个原子动作。线程 A 读取 value 为 0,准备设置 bit1;线程 B 同时读取 value 为 0,准备设置 bit2。两个线程各自计算完写回后,最后写入的线程会覆盖另一个线程的结果,bit1 或 bit2 至少有一个丢失。这种情况在高并发状态机里并不罕见,而且不是靠简单的 volatile 就能解决,因为 volatile 只保证可见性,不保证读改写原子性。
传统上要解决这类问题,要么使用 synchronized 块保护整个读改过程,要么使用 AtomicLong 的 compareAndSet 循环。synchronized 会引入监视器竞争,线程可能阻塞和唤醒;AtomicLong 虽然无锁,但它的 getAndUpdate 需要传入函数式接口,内部仍然通过 CAS 重试。对于只需要做一次按位或、按位与或按位异或的场景,这些方案都显得过于笨重。VarHandle 的位操作接口则提供了更贴近需求的能力,它让 JVM 在可能的情况下直接使用 CPU 的锁前缀指令,或者至少把 CAS 循环封装在底层,开发者无需手动重试。
获取 long 字段的 VarHandle 通常放在静态初始化块中完成。字段本身建议声明为 volatile long,这样即使某些路径直接读取字段,也能获得可见性保证。下面是一个基本定义:
public class LongFlags {
private static final VarHandle VALUE;
static {
try {
VALUE = MethodHandles.lookup().findVarHandle(LongFlags.class, "value", long.class);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
private volatile long value;
}
三个核心位操作方法的返回值与语义
VarHandle 为 long 字段提供的位操作访问模式主要包括 getAndBitwiseOr、getAndBitwiseAnd 和 getAndBitwiseXor。它们的共同点是都返回修改前的旧值,而新值可以通过旧值与掩码再做一次普通位运算得到。例如 getAndBitwiseOr(this, mask) 返回旧值 old,实际写入字段的是 old | mask。这意味着如果你只关心操作是否成功、不需要旧值,依然要能正确处理返回值;而如果你需要返回更新后的结果,需要在旧值基础上再按位或一次。
下面通过一个封装类展示如何利用这些方法完成置位、清位、翻转和查询。掩码参数 mask 由调用方传入,可以是单个 bit 的位移结果,也可以是多个 bit 组合。清位时需要注意,getAndBitwiseAnd 接受的是参与与运算的值,所以传入 ~mask 才能在目标位上做清零。
public class LongBitOps {
private static final VarHandle VALUE;
static {
try {
VALUE = MethodHandles.lookup().findVarHandle(LongBitOps.class, "value", long.class);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
private volatile long value;
public long setFlags(long mask) {
long old = (long) VALUE.getAndBitwiseOr(this, mask);
return old | mask;
}
public long clearFlags(long mask) {
long old = (long) VALUE.getAndBitwiseAnd(this, ~mask);
return old & ~mask;
}
public long toggleFlags(long mask) {
long old = (long) VALUE.getAndBitwiseXor(this, mask);
return old ^ mask;
}
public boolean isSet(long mask) {
return (value & mask) == mask;
}
}
如果使用传统的 compareAndSet 循环,同样的 setFlags 需要写成下面这样。这段代码虽然也能工作,但多线程竞争激烈时会反复自旋,而且每次循环都要显式读取和写回,容易引入边界错误。
public long setFlagsWithCas(long mask) {
long oldValue, newValue;
do {
oldValue = value;
newValue = oldValue | mask;
} while (!VALUE.compareAndSet(this, oldValue, newValue));
return newValue;
}
VarHandle 的位操作方法在返回旧值的同时,具有 volatile 读写的内存效果。也就是说,一个线程对字段执行 getAndBitwiseOr 后,另一个线程随后读取该字段,一定能看到更新后的 bit。这种语义让它可以直接用于发布状态变更,而不需要额外加锁或使用内存屏障。虽然 JVM 在不同硬件平台上的实现可能不同,但从 Java 内存模型角度看,这些方法和 getVolatile、setVolatile 处于同一访问级别。
性能对比:比 synchronized 和 AtomicLong.getAndUpdate 快在哪里
从执行路径看,synchronized 保护一个 long 字段的位运算,至少需要进入监视器、检查锁状态、可能发生线程挂起或唤醒,竞争激烈时还会出现线程上下文切换。即使 JIT 对不可竞争锁做了优化,它仍然比一条原子机器指令更重。对于只修改一个 bit 的高频路径,这种开销会被放大。VarHandle 的位操作则有机会被 JIT 编译为 x86 平台上的 LOCK OR、LOCK AND、LOCK XOR 指令。这些指令直接在缓存行上完成读改写,线程不需要进入内核态,也不需要显式加锁。
AtomicLong 虽然也是无锁实现,但它提供的 API 里并没有直接的按位或、按位与、按位异或方法。要实现同样的功能,通常会调用 getAndUpdate 或 updateAndGet,并传入类似 old -> old | mask 的 lambda。问题在于,这个 lambda 会被封装成一个函数对象,可能在每次调用时产生分配,而且 function 的 invoke 会阻止一部分内联优化。更重要的是,AtomicLong 内部的 CAS 循环依然需要处理返回旧值和新值之间的比较,而 VarHandle 的 getAndBitwiseOr 在支持硬件原子位操作的平台上可以省去循环,即使退化到 CAS 循环,也只是 JVM 内部的一条快速路径。
当然,性能优势并不是在所有场景下都绝对。如果多个线程同时修改同一个 bit,CAS 或硬件锁前缀都会产生缓存行竞争,吞吐会随并发度提高而下降。这时真正的瓶颈是单个 long 字段所在的缓存行,而不是使用哪种原子操作。换句话说,VarHandle 位操作解决的是避免锁和减少函数对象开销,它不能消除共享缓存行本身的竞争。针对这种情况,可以考虑把不同状态拆分到不同 long 字段,或者使用 LongAdder 这类分散竞争的数据结构。
下面从代码复杂度和典型吞吐表现做一个简单对比,可以直观看出三者的差异。
- synchronized 块:代码最简单,但阻塞式加锁,适合低竞争或保护复杂逻辑。
- AtomicLong.getAndUpdate:无锁,但需要函数对象,语义不如位操作直接,高竞争时自旋较明显。
- VarHandle 位操作:无锁、无函数对象、语义直指位修改,适合高频状态位更新。
实战:基于 long 位图构建无锁任务状态容器
任务调度器通常需要快速判断一个任务是否处于运行、暂停、取消、失败等状态。把这些状态定义成 long 的不同 bit,可以避免创建多个 boolean 字段,同时减少对象头大小。下面是一个简化实现:低 8 位表示任务运行阶段,其余 bit 作为扩展标志。通过 VarHandle 位操作可以在任意线程中安全地同时修改多个状态。
public class TaskState {
private static final long RUNNING = 1L << 0;
private static final long PAUSED = 1L << 1;
private static final long STOPPED = 1L << 2;
private static final long FAILED = 1L << 3;
private static final VarHandle STATE;
static {
try {
STATE = MethodHandles.lookup().findVarHandle(TaskState.class, "state", long.class);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
private volatile long state;
public long start() {
long old = (long) STATE.getAndBitwiseOr(this, RUNNING);
return old | RUNNING;
}
public long pause() {
long old = (long) STATE.getAndBitwiseOr(this, PAUSED);
return old | PAUSED;
}
public long finishSuccess() {
long old = (long) STATE.getAndBitwiseAnd(this, ~RUNNING);
return old & ~RUNNING;
}
public long markFailed() {
long combined = FAILED | STOPPED;
long old = (long) STATE.getAndBitwiseOr(this, combined);
return old | combined;
}
public boolean isRunning() {
return (state & RUNNING) != 0;
}
public boolean isPaused() {
return (state & PAUSED) != 0;
}
}
注意 markFailed 方法一次性设置了 FAILED 和 STOPPED 两个 bit。如果把它拆成两次 getAndBitwiseOr 调用,中间会暴露只有 FAILED 而没有 STOPPED 的状态,其他线程可能观察到不一致的任务快照。因此在设计无锁状态机时,应当把需要同时变更的 bit 合并到同一个掩码中,用一次原子位操作完成。这也是 VarHandle 位操作相比普通 AtomicLong 方法更贴合状态机模型的原因之一。
对于清位操作,代码里使用 ~RUNNING 作为掩码。在 Java 中,~RUNNING 是 long 类型的按位取反,除了 bit0 为 0 以外,其余 bit 全部是 1。将它传入 getAndBitwiseAnd 后,只有 bit0 会被清零,其他 bit 保持不变。这种写法比 value &= ~mask 更加不易出错,因为原子方法保证了读改写不会被其他线程穿插。
总的来说,如果你的组件把多个布尔标志压进一个 long 字段,并且需要频繁从不同线程更新不同 bit,那么 VarHandle 提供的 getAndBitwiseOr、getAndBitwiseAnd、getAndBitwiseXor 是非常合适的选择。它们比 synchronized 更轻量,比 AtomicLong 函数式更新更直接,在 JVM 底层可以走 CPU 原子位操作指令,从而获得更好的并发吞吐。使用时要记住两点:字段建议声明为 volatile long,VarHandle 在静态初始化块中通过 MethodHandles.lookup 获取;同时把需要同时变更的 bit 合并成一个掩码,交给一次原子调用完成。

