news 2026/11/1 7:32:08

Java进阶篇之VarHandle:定位变量,再选择访问模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java进阶篇之VarHandle:定位变量,再选择访问模式

连续学习三个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使用。

模式常用方法主要用途
Plainget、set普通访问,不建立跨线程发布顺序
OpaquegetOpaque、setOpaque同一变量访问的相干顺序,不负责发布其他数据
Acquire/ReleasegetAcquire、setRelease建立有方向的数据发布与读取关系
VolatilegetVolatile、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

定位变量

实例字段使用对象坐标

数组使用对象与下标

选择访问模式

Acquire与Release发布数据

Volatile与原子更新

使用纪律

权限与类型匹配

统一读写入口

总结要点

VarHandle先用坐标定位变量,再用访问模式规定这次操作的语义。对象字段与数组元素可以共享一套操作方式,但具体坐标与类型必须匹配。

原子更新要使用完整的读改写操作。分别执行volatile读和写,无法让加法自动成为原子动作;数组操作的原子范围也只覆盖当前元素。

Release/Acquire适合有方向的数据发布。把标志与它保护的数据一起推导,统一访问入口,才能让内存模式服务于清楚的并发协议。

下一篇进一步拆解compareAndExchange:它返回的见证值怎样帮助我们组织CAS重试循环。

👉如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 10:33:34

视频号视频找不到下载按钮?res-downloader 快速搞定

视频号视频找不到下载按钮&#xff1f;res-downloader 快速搞定 【免费下载链接】res-downloader 视频号、小程序、抖音、快手、小红书、直播流、m3u8、酷狗、QQ音乐等常见网络资源下载! 项目地址: https://gitcode.com/GitHub_Trending/re/res-downloader 想把视频号里…

作者头像 李华
网站建设 2026/10/21 23:24:44

单片机毕设项目:基于单片机的4G老人跌倒检测健康监护与定位装置设计 基于STM32的老人跌倒报警与4G远程监测系统实现(032604)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/10/23 10:14:24

ROS2环境激活原理与实操:source命令背后的环境变量机制

1. 从安装完成到真正能用&#xff1a;ROS2环境激活到底在激活什么很多人在ROS2安装教程里走到最后一步&#xff0c;看到终端里出现一两行source命令&#xff0c;复制粘贴执行完就以为大功告成了。然后第二天打开新终端&#xff0c;输入ros2节点列表&#xff0c;直接报错命令找不…

作者头像 李华
网站建设 2026/11/1 7:31:28

PS5远程游玩完整方案:串流部署、画质与延迟调优实战

各位玩家、DIY爱好者&#xff0c;还有那些和我一样“电视总被家人霸占”的朋友&#xff0c;这篇内容应该能帮到你。我把这套围绕主机远程游玩、多设备接入折腾出来的完整方案&#xff0c;命名为“AnyPS5”。简单说&#xff0c;它解决的就是一个很具体的痛点&#xff1a;PS5摆在…

作者头像 李华
网站建设 2026/10/23 7:00:49

基于.NET的实验室管理信息系统开发实战:从需求分析到部署上线全解析

又到一年课设季&#xff0c;后台每天都能收到一堆“求选题”“求带”“有没有现成项目”的私信。说实话&#xff0c;闭着眼睛抄一个管理系统不如踏踏实实做一个自己真正能讲明白的系统。我一直觉得&#xff0c;实验室管理信息系统这套题目&#xff0c;在课设和毕设里属于性价比…

作者头像 李华
网站建设 2026/10/23 7:33:39

内联函数、auto、范围for、nullptr

知识点6内联函数&#xff1a;就是将常用的函数直接展开&#xff0c;没有建立栈帧的开销&#xff0c;提升运行效率。用 inline 修饰要内联的函数即可。inline 对编译器只是建议&#xff0c;不一定执行。一般将规模小、非递归且频繁调用的函数内联。inline 不建议声明和定义分离&…

作者头像 李华