承接的问题
9.1 和 9.2 已经把 VarHandle 的语义讲完,还剩一个现实问题:手上有一堆 sun.misc.Unsafe 旧代码,怎么迁?
先看 JDK 25 自己的态度。AtomicInteger 的源码里留着这样一段:
// JDK 25 源码摘录,已删减:AtomicInteger 的字段声明
public class AtomicInteger extends Number implements java.io.Serializable {
/*
* This class intended to be implemented using VarHandles, but there
* are unresolved cyclic startup dependencies.
*/
private static final Unsafe U = Unsafe.getUnsafe();
private static final long VALUE
= U.objectFieldOffset(AtomicInteger.class, "value");
/** @serial */
private volatile int value;
注释说得很清楚:本来打算用 VarHandle 实现,但启动期的循环依赖没解决。这不是「Unsafe 更好」,而是 JDK 自身在类初始化顺序上的妥协。同包的 Striped64 没有这个负担,Cell 和 base、cellsBusy 都已经切到 VarHandle:
// JDK 25 源码摘录,已删减:Striped64.Cell 的 VarHandle 路径
@jdk.internal.vm.annotation.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return VALUE.weakCompareAndSetRelease(this, cmp, val);
}
final void reset() {
VALUE.setVolatile(this, 0L);
}
final long getAndSet(long val) {
return (long)VALUE.getAndSet(this, val);
}
private static final VarHandle VALUE = MhUtil.findVarHandle(
MethodHandles.lookup(), "value", long.class);
}
第 8 章已经讲过 Cell 为什么要分片、为什么用 weakCompareAndSetRelease;本节只关心「同样的语义怎么用 VarHandle 写出来」。
还要注意一个常被忽略的事实:AtomicIntegerFieldUpdater 在 JDK 25 仍然用 Unsafe。javap -p 可以看到它的实现类里是 private static final Unsafe U 和 private final long offset,不是句柄。所以「JUC 全迁完了」是错的,迁移是按类、按启动依赖逐个做的。
四种句柄,各自一套坐标
VarHandle 不是只有字段一种。按访问目标分,常用的是四类:
| 目标 | 工厂 | 变量类型 | 坐标 |
|---|---|---|---|
| 实例字段 | Lookup.findVarHandle(Class, name, type) | 字段类型 | 接收者 |
| 静态字段 | Lookup.findStaticVarHandle(Class, name, type) | 字段类型 | 无 |
| 数组元素 | MethodHandles.arrayElementVarHandle(T[].class) | 元素类型 | (T[], int) |
| 字节数组视图 | MethodHandles.byteArrayViewVarHandle(T[].class, order) | 视图类型 | (byte[], int) |
一段代码把四种都跑一遍:
// demo.HandlesDemo(节选)
static final VarHandle VALUE;
static final VarHandle GLOBAL;
static final VarHandle INTS;
static final VarHandle INT_VIEW;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
VALUE = lookup.findVarHandle(HandlesDemo.class, "value", long.class);
GLOBAL = lookup.findStaticVarHandle(HandlesDemo.class, "global", long.class);
INTS = MethodHandles.arrayElementVarHandle(int[].class);
INT_VIEW = MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.nativeOrder());
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
long value;
static long global;
public static void main(String[] args) {
HandlesDemo demo = new HandlesDemo();
VALUE.set(demo, 7L);
System.out.println("instance field = " + VALUE.get(demo));
GLOBAL.setVolatile(41L);
System.out.println("static field = " + GLOBAL.getVolatile());
int[] numbers = {1, 2, 3};
INTS.set(numbers, 1, 20);
int old = (int) INTS.getAndAdd(numbers, 2, 5);
System.out.println("array element = " + Arrays.toString(numbers) + " (old at [2]=" + old + ")");
byte[] buffer = new byte[Integer.BYTES];
INT_VIEW.set(buffer, 0, 0x01020304);
System.out.println("byte view bytes = " + Arrays.toString(buffer));
System.out.printf("byte view read back = 0x%08x%n", (int) INT_VIEW.get(buffer, 0));
}
本机输出(只摘录上文调用对应的行;程序还额外打印了字节序与计数器结果):
instance field = 7
static field = 41
array element = [1, 20, 8] (old at [2]=3)
byte view bytes = [4, 3, 2, 1]
byte view read back = 0x01020304
byte view bytes = [4, 3, 2, 1] 是小端机器的结果:0x01020304 的最低字节 04 放在下标 0。字节数组视图的坐标是「字节偏移」,不是元素下标;在长度足够的 buffer 里,INT_VIEW.get(buffer, 4) 读的才是第二个 int。
这里有一个必须提前知道的限制:字节数组视图句柄只支持 plain 的 get/set。本机验证:
byte view GET = true
byte view GET_VOLATILE = false
byte view CAS = false
getVolatile -> java.lang.UnsupportedOperationException
原因在 MethodHandles.byteArrayViewVarHandle 的注释里:byte[] 不保证按视图类型对齐,非 plain 模式无法安全实现。需要原子或有序访问时,改用 ByteBuffer.allocateDirect 或 java.lang.foreign.MemorySegment 上的句柄。
迁移对照表
把常见 Unsafe 调用逐条对上:
| Unsafe 旧写法 | VarHandle 替代 | 说明 |
|---|---|---|
objectFieldOffset + getInt(obj, off) | get(obj) | plain,无跨线程顺序 |
getIntVolatile / putIntVolatile | getVolatile / setVolatile | 与 volatile 字段等价 |
getIntAcquire / putIntRelease | getAcquire / setRelease | 单向屏障 |
getIntOpaque / putIntOpaque | getOpaque / setOpaque | 单变量一致,无跨变量顺序 |
compareAndSwapInt | compareAndSet | 强 CAS,不允许伪失败 |
compareAndExchangeInt | compareAndExchange | 返回 witness value |
weakCompareAndSwapInt | weakCompareAndSet | 允许伪失败 |
getAndAddInt | getAndAdd | 默认 volatile 语义 |
getAndSetInt | getAndSet | 返回旧值 |
arrayBaseOffset + arrayIndexScale | arrayElementVarHandle | 不再手算偏移 |
copyMemory / setMemory | System.arraycopy / MemorySegment.copy | 批量复制不需要 Unsafe |
allocateMemory / freeMemory | Arena + MemorySegment | 堆外内存走 FFM |
monitorEnter / monitorExit | synchronized / ReentrantLock | 监视器语义 |
park / unpark | LockSupport | 线程挂起 |
getUnsafe | 无替代 | 只能通过受信模块获取 |
defineClass / defineAnonymousClass | 无等价替代 | 类定义不属于变量访问 |
旧代码大致长这样。下面这段只是对照,不提供 import,不能直接编译:
// 旧写法(反例,不可直接编译):Unsafe 版计数器
final class UnsafeCounter {
private static final Unsafe U = Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(UnsafeCounter.class, "value");
private volatile long value;
void increment() {
U.getAndAddLong(this, VALUE, 1L);
}
long sum() {
return U.getLongVolatile(this, VALUE);
}
}
迁到 VarHandle 之后,偏移量、类型转换和字段名检查都收进句柄:
// demo.VarHandleCounter
public class VarHandleCounter {
private static final VarHandle VALUE;
static {
try {
VALUE = MethodHandles.lookup()
.findVarHandle(VarHandleCounter.class, "value", long.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private long value;
public void increment() {
VALUE.getAndAdd(this, 1L);
}
public boolean casIncrement(long expected) {
return VALUE.compareAndSet(this, expected, expected + 1L);
}
public long sum() {
return (long) VALUE.getVolatile(this);
}
public static void main(String[] args) throws InterruptedException {
VarHandleCounter counter = new VarHandleCounter();
int threads = 8;
int perThread = 100_000;
Thread[] workers = new Thread[threads];
for (int i = 0; i < threads; i++) {
workers[i] = Thread.ofPlatform().start(() -> {
for (int j = 0; j < perThread; j++) {
counter.increment();
}
});
}
for (Thread worker : workers) {
worker.join();
}
System.out.println("after getAndAdd: " + counter.sum());
}
}
after getAndAdd: 800000
这个数只证明「8 线程 × 100 000 次没有丢更新」,不是吞吐结论。迁移时最有价值的不是性能,而是三件事:字段类型在查找时校验、偏移量不再暴露给业务代码、访问模式在调用点一眼可见。
访问权限与 final 字段
Lookup.findVarHandle 的权限规则和普通反射不同:它检查的是调用 lookup() 的那个类对目标成员的访问权。同一个类里拿自己的私有字段没问题;跨包拿包私有字段会抛 IllegalAccessException;跨模块还要目标模块 opens 对应的包。
MethodHandles.privateLookupIn(target, lookup) 可以提升到目标类的私有访问级别,但它不是绕过模块系统:目标模块必须把包开放给调用方。classpath 上的未命名模块默认开放,命名模块要显式配置。
final 字段是另一个边界。本机探测结果:
instance final SET supported = false
instance final CAS supported = false
static final SET supported = false
normal SET supported = true
plain read via handle = 1
set final -> java.lang.UnsupportedOperationException
findVarHandle 能拿到 final 字段的句柄,也能读,但所有写模式都不支持;调用 set 抛 UnsupportedOperationException。这意味着 VarHandle 不能用来改 final 字段,也不能像某些 Unsafe 技巧那样在构造完成后强改常量。需要可变状态就老老实实声明非 final 字段。
什么时候不要用 VarHandle
- 堆外内存管理:
allocateMemory、reallocateMemory、freeMemory、地址算术,VarHandle一概不做。用Arena+MemorySegment,或者ByteBuffer.allocateDirect。 - 任意偏移量访问:
objectFieldOffset拿到的裸偏移可以传给 JNI 或做指针运算,VarHandle把偏移封装在句柄里,不暴露。依赖偏移量的本地代码无法直接迁移。 - 类定义与反射式改写:
defineClass、defineAnonymousClass没有对应物。隐藏类用Lookup.defineHiddenClass,但语义不同。 - 监视器与线程挂起:
monitorEnter/monitorExit、park/unpark不在变量访问范畴,用synchronized、ReentrantLock、LockSupport。 getUnsafe()本身:VarHandle没有「获取一个万能后门」的入口。迁移的第一步是把「拿 Unsafe 实例」这段删掉,而不是找替代品。- 字节数组视图的原子访问:只支持 plain
get/set,需要原子性时换MemorySegment。 float/double的 CAS:VarHandle按位比较,NaN的不同位模式和-0.0与+0.0会让 CAS 出现「看起来相等却失败」的情况,迁移时要按位语义重新检查。
在 libraryd-conc 里落地
卷一第 2 章的 UnsafeLoanService 是故意写出竞态的反例。真正要迁移的是它背后的库存单元:旧实现用 Unsafe 的偏移量做 CAS,新实现把偏移量换成句柄。
package com.plume.conc.core;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
public final class StockLedger {
private final ConcurrentHashMap<String, StockCell> cells = new ConcurrentHashMap<>();
private final BookRepository books;
public StockLedger(BookRepository books) {
this.books = books;
}
public void seed(String isbn) {
cells.computeIfAbsent(isbn, key -> new StockCell(books.findByIsbn(key)
.map(Book::stock)
.orElseThrow(() -> new LoanException(ErrorCode.BOOK_NOT_FOUND, key))));
}
public void reserve(String isbn) {
StockCell cell = Optional.ofNullable(cells.get(isbn))
.orElseThrow(() -> new LoanException(ErrorCode.BOOK_NOT_FOUND, isbn));
if (!cell.tryReserveOne()) {
throw new LoanException(ErrorCode.OUT_OF_STOCK, isbn);
}
}
public int stockOf(String isbn) {
StockCell cell = cells.get(isbn);
return cell == null ? 0 : cell.current();
}
static final class StockCell {
private static final VarHandle STOCK;
static {
try {
STOCK = MethodHandles.lookup()
.findVarHandle(StockCell.class, "stock", int.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
private int stock;
StockCell(int stock) {
this.stock = stock;
}
boolean tryReserveOne() {
int current = (int) STOCK.getAcquire(this);
while (current > 0) {
int witness = (int) STOCK.compareAndExchangeRelease(this, current, current - 1);
if (witness == current) {
return true;
}
current = witness;
}
return false;
}
void restock(int copies) {
STOCK.getAndAdd(this, copies);
}
int current() {
return (int) STOCK.getVolatile(this);
}
}
}
迁移前后要对照的是「谁在算偏移」:旧代码把 objectFieldOffset 的返回值存成 long,每个方法都把它传给 Unsafe;新代码只在静态初始化里查一次句柄,业务方法只写 STOCK.compareAndExchangeRelease(this, ...)。偏移量从公开状态变成实现细节,字段名打错会在类初始化时以 NoSuchFieldException 为原因抛出 ExceptionInInitializerError,而不是在运行时读写到错误地址。
StockLedger 的粒度也要说清楚:它只保证「一个 ISBN 对应的一个 int 库存」原子扣减;跨 ISBN 的批量扣减、库存与会员配额联动仍然不是原子的。VarHandle 迁移的是访问方式,不是事务边界。
迁移检查清单
- 先删
Unsafe.getUnsafe(),确认没有遗留的offset字段;有就说明迁移没做完。 - 把
objectFieldOffset换成findVarHandle,字段类型必须逐字匹配;int写成long会抛NoSuchFieldException。 - 按语义选模式:只需要单变量一致用 opaque,发布用 release/acquire,需要全序才用 volatile。
- 检查
final字段:写模式不支持,必须改成非 final 或换设计方案。 - 检查数组访问:手算的
base + index * scale全部换成arrayElementVarHandle;字节数组视图需要原子性时不能留在byteArrayViewVarHandle。 - 检查
float/double的 CAS:按位语义重写比较逻辑。 - 检查权限:跨包、跨模块的句柄要么有
privateLookupIn加opens,要么把访问收敛到目标类内部。
小结
- JDK 25 的
AtomicInteger仍用 Unsafe 是启动期循环依赖的妥协;Striped64已经完整迁到VarHandle,AtomicIntegerFieldUpdater仍在用 Unsafe。 - 四种常用句柄:实例字段、静态字段、数组元素、字节数组视图;字节数组视图只支持 plain 读写。
- 迁移的核心是把「偏移量 + 类型转换」换成「一次查找、静态缓存、调用点带语义的句柄」。
final字段只有读模式;privateLookupIn受模块opens约束;getUnsafe、堆外内存、类定义、监视器不在VarHandle的能力范围内。- 库存单元迁移后仍然只保证单字段原子扣减;跨字段事务要另找边界。
阅读导航:上一节 9.2 内存屏障与 acquire/release | 下一节 10.1 无锁栈与 Michael-Scott 队列
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。