连续学习三个FieldUpdater以后,可以看到它们遵循同一种思路:先定位对象中的字段,再执行原子更新。VarHandle把这条思路扩展到了实例字段、静态字段和数组元素,还把读取、写入与更新时的内存语义放到方法名中。
这一篇围绕三个具体操作展开:多个线程累加同一个字段、原子更新数组中的一个位置,以及通过Release/Acquire发布一条消息。理解这三个例子,就能看清“变量在哪里”和“怎样访问变量”之间的关系。
一、一个句柄,两个问题
VarHandlevalue=MethodHandles.lookup().findVarHandle(Counter.class,"value",int.class);这行代码确定宿主类、字段名与字段类型,得到一个访问入口。后续真正操作哪个Counter实例,由调用时传入的对象决定:
value.getAndAdd(counter,1);intcurrent=(int)value.getVolatile(counter);可以把VarHandle拆成两个维度理解:坐标负责定位变量,访问模式负责规定这次读写的语义。
| 变量位置 | 定位所需坐标 | 操作示例 |
|---|---|---|
| 实例字段 | 宿主对象 | handle.getVolatile(object) |
| 静态字段 | 无宿主坐标 | handle.getVolatile() |
| 数组元素 | 数组对象、下标 | handle.getVolatile(array, index) |
句柄可以共享,变量值属于具体对象或具体数组位置。两个Counter对象使用同一个静态句柄,各自仍然拥有独立计数。Java官方API
二、访问模式放在方法名中
从FieldUpdater迁移过来时,最容易误解的一点是:VarHandle定位的字段不要求一定声明为volatile,访问时的语义由所调用的模式决定。
privateintvalue;这个普通字段可以通过getVolatile读取,也可以通过getAndAdd执行原子加法。反过来,即使字段声明为volatile,通过句柄调用普通get,也不能把它当成getVolatile使用。
| 模式 | 常用方法 | 主要用途 |
|---|---|---|
| Plain | get、set | 普通访问,不建立跨线程发布顺序 |
| Opaque | getOpaque、setOpaque | 同一变量访问的相干顺序,不负责发布其他数据 |
| Acquire/Release | getAcquire、setRelease | 建立有方向的数据发布与读取关系 |
| Volatile | getVolatile、setVolatile | 使用volatile读写语义 |
| 原子更新 | compareAndSet、getAndAdd | 把比较更新或读改写作为一个原子动作 |
这张表要联系具体协议使用。看到一条状态标志需要跨线程可见,不应直接把所有访问降到Opaque;先弄清标志是否还承担了发布其他字段的职责。
原子性与可见性是两个不同的问题。getVolatile加上setVolatile,仍然是两个独立动作。下面的代码会丢失并发增加:
intold=(int)VALUE.getVolatile(this);VALUE.setVolatile(this,old+1);线程A和B都可能读取到10,然后各自写入11。需要计数时,直接使用getAndAdd(this, 1),让读取旧值与写入新值组成一次原子更新。访问模式说明
三、从字段到数组,只是坐标发生变化
VarHandleelements=MethodHandles.arrayElementVarHandle(int[].class);int[]values=newint[3];booleanchanged=elements.compareAndSet(values,1,0,7);intprevious=(int)elements.getAndAdd(values,1,5);values与下标1共同定位第二个元素。CAS成功后,该元素变成7;接着原子加5,方法返回旧值7,当前值变成12。
下标不会保存在句柄中,同一句柄也可以访问另一个int数组。对应的原子性仍然只覆盖一个元素,不能把两个下标上的操作自动合并成一个事务。
图中两个槽位已经是1和1,读线程先前分次记录的结果仍可能是[0, 1]。对各元素分别原子读取,并不等于取得整组一致快照。VarHandle数组访问也需要遵守同样的单格边界。
这种坐标设计让字段与数组使用统一的一组访问方法,同时保留了各自准确的类型要求。数组越界会报错,宿主对象或参数类型不匹配也不会因为方法表面接收Object而被忽略。
四、完整程序:字段累加、数组CAS与消息发布
下面使用Java21运行。VarHandle从Java9开始提供;程序中的ExecutorService自动关闭写法需要较新的JDK,因此本例统一以Java21编译。
importjava.lang.invoke.MethodHandles;importjava.lang.invoke.VarHandle;importjava.util.concurrent.*;publicclassVarHandleDemo{staticfinalclassCounter{privateintvalue;privatestaticfinalVarHandleVALUE;static{try{VALUE=MethodHandles.lookup().findVarHandle(Counter.class,"value",int.class);}catch(ReflectiveOperationExceptione){thrownewExceptionInInitializerError(e);}}voidincrement(){VALUE.getAndAdd(this,1);}intvalue(){return(int)VALUE.getVolatile(this);}}staticfinalclassMessage{privateintpayload;privatebooleanready;privatestaticfinalVarHandleREADY;static{try{READY=MethodHandles.lookup().findVarHandle(Message.class,"ready",boolean.class);}catch(ReflectiveOperationExceptione){thrownewExceptionInInitializerError(e);}}voidpublish(intdata){payload=data;READY.setRelease(this,true);}intawait(){while(!(boolean)READY.getAcquire(this))Thread.onSpinWait();returnpayload;}}publicstaticvoidmain(String[]args)throwsException{Countercounter=newCounter();try(ExecutorServicepool=Executors.newFixedThreadPool(8)){Future<?>[]tasks=newFuture<?>[8];for(inti=0;i<tasks.length;i++){tasks[i]=pool.submit(()->{for(intj=0;j<1000;j++)counter.increment();});}for(Future<?>task:tasks)task.get();}System.out.println("count="+counter.value());VarHandleelements=MethodHandles.arrayElementVarHandle(int[].class);int[]values=newint[3];System.out.println("array CAS="+elements.compareAndSet(values,1,0,7));intold=(int)elements.getAndAdd(values,1,5);System.out.println("array old="+old);System.out.println("array current="+(int)elements.getVolatile(values,1));Messagemessage=newMessage();FutureTask<Integer>reader=newFutureTask<>(message::await);Threadthread=newThread(reader,"reader");thread.start();message.publish(42);System.out.println("message="+reader.get(5,TimeUnit.SECONDS));thread.join();}}执行命令:
javac--release21VarHandleDemo.javajavaVarHandleDemo本例实际运行输出:
count=8000 array CAS=true array old=7 array current=12 message=42第一段有8个任务,每个增加1000次,最终得到8000。主线程通过Future.get()等待所有任务完成,并接收任务异常,随后读取最终计数。
第二段只访问下标1。CAS返回true表示把0改为7;getAndAdd返回7表示操作前的值,之后读取到12。它返回的是旧值,不要把这个返回值直接当成累计后的结果。
第三段的关键不在打印42,而在payload的写入怎样被安全地交给读线程。
五、Release/Acquire怎样把payload一起发布
生产者先写普通字段,再发布标志:
payload=data;READY.setRelease(this,true);消费者反复读取标志,观察到发布的true以后再读取数据:
while(!(boolean)READY.getAcquire(this)){Thread.onSpinWait();}returnpayload;Release写与匹配的Acquire读建立顺序关系:消费者通过Acquire观察到这次Release发布的标志后,后续读取可以看到发布前写入的payload。这里的ready同时承担“状态标志”和“数据发布点”两个职责。
如果把读取改成普通get,就失去了这条发布协议需要的Acquire语义。Thread.onSpinWait()只是自旋提示,不能替代Acquire,也不会自己建立数据可见性。
示例是一条消息、一次发布,只有一个生产者。把它扩展成反复复用的消息槽,还需要确认消费完成、何时重置标志以及下一条消息是否覆盖上一条。业务线程可能等待很久时,使用阻塞队列等成熟同步结构更容易管理等待与唤醒。
六、类型、权限与访问范围要在入口确定
1. 创建句柄时完成访问检查
MethodHandles.lookup()的权限来自创建它的代码位置。示例把私有字段句柄的创建放在所属类内部,不需要公开字段,也不需要把Lookup对象交给外部。
若访问另一个类的私有字段,需要按模块与访问规则获取合适的Lookup;不能把查找失败当成普通业务失败无限重试。程序把查找异常转成初始化错误,使配置错误尽早暴露。
2. 返回类型与调用参数都要对齐
intcount=(int)VALUE.getVolatile(counter);调用点的坐标、值与返回类型共同参与类型匹配。VarHandle的方法具有签名多态性,编译通过不等于任意参数组合都有效,类型不匹配可能抛出WrongMethodTypeException。
3. 不是每个句柄都支持每种操作
final字段不能通过VarHandle写入,引用类型也不能使用数值加法。可以通过isAccessModeSupported检查特定模式是否支持。业务层应只暴露自己需要的方法,不必把完整句柄作为公共API开放。
4. 选定协议后,统一访问入口
Counter的value没有其他直接写入。若在其他方法中执行普通value++,它不会因为同一个字段曾被VarHandle访问过,就自动获得原子性。使用句柄的收益来自整套一致的读写协议。Java官方API
七、什么时候选择VarHandle
现成AtomicInteger已经能清楚表达一个独立计数器时,可以直接使用它。已有对象字段需要原子操作、需要访问数组元素,或者正在实现明确的内存访问协议时,VarHandle提供更统一的入口。
不要仅凭“模式更弱”就宣称更快。先保证语义正确,再围绕真实竞争强度、对象布局与运行环境测量。多个对象之间的复杂业务约束,仍然需要锁或其他完整协议。
八、🧠 思维导图
总结要点
VarHandle先用坐标定位变量,再用访问模式规定这次操作的语义。对象字段与数组元素可以共享一套操作方式,但具体坐标与类型必须匹配。
原子更新要使用完整的读改写操作。分别执行volatile读和写,无法让加法自动成为原子动作;数组操作的原子范围也只覆盖当前元素。
Release/Acquire适合有方向的数据发布。把标志与它保护的数据一起推导,统一访问入口,才能让内存模式服务于清楚的并发协议。
下一篇进一步拆解compareAndExchange:它返回的见证值怎样帮助我们组织CAS重试循环。
👉如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊