news 2026/4/20 22:07:31

Atomic类lazySet的奥秘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Atomic类lazySet的奥秘

引言

只要你使用Java进行开发工作,那么在漫长的工作生涯中或多或少都需要面对高并发问题以及涉及使用JUC工具类,笔者当年从OTA互联网行业进入Java的世界,在负责库存管理时偶尔会遇到高并发协同而导致的系统吞吐下降,于是深入研究无锁、轻量级锁等并发技术,开始阅读JUC工具类的实现逻辑,就会遇到这个在Atomic相关类中的方法lazySet。初次遇到这个方法肯定是摸不着头脑,不能明白Doug Lea设计这个方法的初衷,搞不清楚它和普通的set方法到底有什么不同。当想要自己一探究竟时,没翻两行代码就遇到了native关键字,直接被拦截在门外。不死心那就只能上网搜索开始“人云亦云”,遂收藏几个博客链接,默认自己掌握了其中的奥秘,起码面试的时候有了“你来我往”的谈资……所以你是不是也这样子?那就跟着笔者来一探究竟吧!

一、putOrderedXXXvolatile set的实现原理

因为hotspot虚拟机分为解释执行和JIT编译执行,我们从这两种不同的执行方式来分析它们的实现差异。

1. 解释执行

为了可以控制寄存器的行为,实现TOS(top of stack cache)栈顶缓存技术和各种fast版本的字节码指令(如使用fast_iload指令替代iload指令)等优化机制,hotspot虚拟机使用汇编模版来生成字节码对应的汇编代码,而不是使用C++代码来实现各种字节码。

putOrderedXXX的实现

我们以putOrderedObject为例子:

UNSAFE_ENTRY(void,Unsafe_SetOrderedObject(JNIEnv*env,jobject unsafe,jobject obj,jlong offset,jobject x_h))UnsafeWrapper("Unsafe_SetOrderedObject");oop x=JNIHandles::resolve(x_h);oop p=JNIHandles::resolve(obj);// 通过obj的字段offset获取对应的目标地址void*addr=index_oop_from_field_offset_long(p,offset);// 内存release语义OrderAccess::release();// 保存x_h到目标地址if(UseCompressedOops){oop_store((narrowOop*)addr,x);}else{oop_store((oop*)addr,x);}// 内存fence语义OrderAccess::fence();UNSAFE_END

这里出现了操控内存顺序语义的函数,我们先了解一下编译器重排序和cpu重排序,才能明白如何实现内存操作的顺序。由于编译器为了提高代码执行效率,会对没有读写依赖关系的操作进行顺序打乱,而编译后的代码顺序(已重排)进入cpu进行执行时,又会受到cpu执行单元和乱序流水线的影响,在cpu侧执行时再次进行重排执行。这两种代码执行的重排序都是为了提高代码执行效率,但是某些场景(特别在多线程下)我们并不需要这种优化,它会导致我们的代码执行出现错误,所以需要操控内存顺序的手段(如内存屏障)来保证代码的逻辑符合我们的预期。
我们只需要关注四种基本内存屏障操作:

Load1(s); LoadLoad; Load2: Load1之前的加载操作*不能*重排序到Load2之后 Store1(s); StoreStore; Store2: Store1之前的存储操作*不能*重排序到Store2之后 Load1(s); LoadStore; Store2: Load1之前的加载操作*不能*重排序到Store2之后 Store1(s); StoreLoad; Load2: Store1之前的存储操作*不能*重排序到Load2之后

由于涉及内存语义和cpu重排序,我们以x86架构的cpu为例:

inlinevoidOrderAccess::release(){volatilejint local_dummy=0;}inlinevoidOrderAccess::fence(){if(os::is_MP()){#ifdefAMD64__asm__volatile("lock; addl $0,0(%%rsp)":::"cc","memory");#else__asm__volatile("lock; addl $0,0(%%esp)":::"cc","memory");#endif}}

特别说明,x86架构的cpu由于TSO内存模型只实现了StoreLoad的重排序,所以只要针对其进行限制就可以让cpu不再进行任何重排序操作。
我们可以看看以上两个函数编译后的代码:
release():

movl $0, -4(%rsp)

fence():

lock; addl $0,0(%rsp)

就是这么朴实无华的两个操作,看似不起眼,实则力挽狂澜:

  1. release()函数首先使用volatile关键字禁止编译器进行重排序优化,这是一个编译器内存屏障,release()函数之前的内存读写都无法重排序到release()之后,再者,movl $0, -4(%rsp)本身就是一个对内存的写操作,所以release()相当于一个StoreStore/LoadStore内存屏障;而x86不会对StoreStore/LoadStore进行重排序,所以不需要额外对cpu执行进行任何操控。
  2. fence()函数首先使用__asm__ volatile()volatile的汇编内嵌禁止编译器进行重排序优化,而addl $0,0(%rsp)是一个对内存进行读写和逻辑加法的双重操作,但由于它对内存地址对应的值加0,并不会更改内存里的值,非常巧妙地利用volatile关键字实现了StoreLoad内存屏障;而x86会对StoreLoad进行重排序,所以需要额外对cpu执行进行干预,我们可以使用x86架构的cpu指令mfence指令来实现全内存屏障,保证所有mfence指令之前的指令不会重排序到mfence指令之后,但是这个指令的成本太高,hotspot使用lock前缀即在普通指令之前增加lock指令来实现StoreLoad内存屏障。(lock前缀请参考x86的操作手册)

volatile set的实现

主要对应在类对象字段的set操作上,解释执行时生成汇编代码的入口如下:

TemplateTable::putfield(intbyte_no)
整体调用链
putfield(byte_no) └─► putfield_or_static(byte_no, is_static=false) ├─ resolve_cache_and_index() // 解析常量池缓存 ├─ load_field_cp_cache_entry() // 加载字段元数据 ├─ 按类型分发写入字段 └─ volatile_barrier() // volatile 内存屏障

第一步:解析常量池缓存resolve_cache_and_index
resolve_cache_and_index(byte_no,cache,index,sizeof(u2));
  • 从字节码流(BCP + 1)读取常量池缓存索引。
  • 检查缓存项中是否已存储当前字节码(即是否已解析过):
    • 已解析:直接跳到resolved标签,跳过InterpreterRuntime::resolve_get_put调用。
    • 未解析(首次执行):调用InterpreterRuntime::resolve_get_put,由运行时完成字段查找、访问权限检查、类初始化等工作,并将结果写入常量池缓存。

第二步:加载字段元数据load_field_cp_cache_entry
load_field_cp_cache_entry(obj,cache,index,off,flags,is_static);

从常量池缓存项中读取三个关键信息:

寄存器含义
off(rbx)字段在对象中的字节偏移量(来自f2_offset
flags(rax)字段的栈顶缓存类型标识(tos_state)和volatile 标志
obj(rcx)仅 static 时使用,存放Klass的 mirror 对象

第三步:提取 volatile 标志
__movl(rdx,flags);__shrl(rdx,ConstantPoolCacheEntry::is_volatile_shift);__andl(rdx,0x1);

volatile标志位单独保存到rdx,供最后的内存屏障判断使用。


第四步:按字段类型分发写入

flags中提取tos_state(栈顶缓存类型标识),通过一系列分支判断跳转到对应的类型处理分支:

flags >> tos_state_shift & mask → 类型判断

每个分支的逻辑为:

1. __ pop(Xtos) // 从操作数栈弹出值到 rax/xmm0 2. pop_and_check_object(obj) // 弹出对象引用到 rcx,并做 null check 3. __ movX(field, rax) // 将值写入 [obj + off] 4. patch_bytecode(...) // 将字节码改写为快速版本 5. __ jmp(Done)

各类型对应的写入指令:

类型写入指令说明
btos(byte)movb(field, rax)写 1 字节
ztos(boolean)andl(rax,1)+movb取最低位后写 1 字节
atos(reference)do_oop_store(...)写对象引用,触发写屏障(GC)
itos(int)movl(field, rax)写 4 字节
ctos(char)movw(field, rax)写 2 字节
stos(short)movw(field, rax)写 2 字节
ltos(long)movq(field, rax)写 8 字节
ftos(float)movflt(field, xmm0)写 4 字节浮点
dtos(double)movdbl(field, xmm0)写 8 字节浮点

其中field的地址计算为:Address(obj, off, times_1)=rcx + rbx,即对象基址 + 字段偏移。


第五步:volatile 内存屏障
__testl(rdx,rdx);__jcc(Assembler::zero,notVolatile);volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad|Assembler::StoreStore));__bind(notVolatile);
  • 若字段是volatile,在写入完成后插入StoreLoad + StoreStore屏障(StoreLoad 在 x86 上对应lock addl),保证写操作对其他线程立即可见。
  • 注意:写入之前的 LoadStore + StoreStore 屏障在代码中被注释掉了([jk] not needed currently),因为 x86 的 TSO 内存模型天然保证了 Store 不会被重排到前面的 Load 之前。

由于解释执行的汇编代码没有经过c++编译器编译而是直接通过汇编模版生成的,所以这里不需要处理编译器重排序的问题,只要解决cpu重排序的问题就可以了。

2. 编译执行

hotspot的编译部分由于实现了分层编译,分别由C1和C2两种编译器来实现了不同层级的编译功能,我们跳过C1直接研究C2生成汇编的逻辑。当提到C2编译器时,这里需要提及一个叫intrinsic的机制,即提前手写汇编代码,然后通过类名和方法签名把Java方法映射成手写汇编代码,从而跳过编译器编译的机制。为什么要跳过编译器编译呢?由于某些特殊case的代码直接手写汇编的执行效率明显高于编译器按部就班地进行编译优化的产物,所以通过使用这种机制保证更优的代码编译和执行效率。

putOrderedXXX的编译实现

putOrderedXXX的编译实现正好使用了intrinsic机制,我们还是以putOrderedObject为例子:

boolLibraryCallKit::inline_unsafe_ordered_store(BasicType type){...insert_mem_bar(Op_MemBarRelease);insert_mem_bar(Op_MemBarCPUOrder);// Ensure that the store is atomic for longs:constboolrequire_atomic_access=true;Node*store;if(type==T_OBJECT)// reference stores need a store barrier.store=store_oop_to_unknown(control(),base,adr,adr_type,val,type,MemNode::release);else{store=store_to_memory(control(),adr,val,type,adr_type,MemNode::release,require_atomic_access);}insert_mem_bar(Op_MemBarCPUOrder);}
内存屏障组合
insert_mem_bar(Op_MemBarRelease);// ① Release 屏障insert_mem_bar(Op_MemBarCPUOrder);// ② CPU 顺序屏障(前)// ... store ...insert_mem_bar(Op_MemBarCPUOrder);// ③ CPU 顺序屏障(后)

三个屏障的作用:

屏障类型语义
MemBarRelease多 线程 可见Release 语义:屏障之前的所有读写不能重排到 store 之后,保证其他 CPU 可见
MemBarCPUOrder(前)仅编译器顺序防止编译器将 store 提前到 MemBarRelease 之前
MemBarCPUOrder(后)仅编译器顺序防止编译器将后续操作提前到 store 之前

MemBarCPUOrder纯编译器级别的顺序约束(不生成机器指令),用于在 C2 IR 图(理想图)中固定节点顺序,防止 GVN/调度优化等越过屏障移动节点。
所以从代码逻辑上看其只实现了Release语义,而MemBarCPUOrder只是保证前后的代码不重排序。

volatile set的编译实现

voidParse::do_put_xxx(Node*obj,ciField*field,boolis_field){...boolis_vol=field->is_volatile();// If reference is volatile, prevent following memory ops from// floating down past the volatile write. Also prevents commoning// another volatile read.if(is_vol){leading_membar=insert_mem_bar(Op_MemBarRelease);}...if(is_vol){// If not multiple copy atomic, we do the MemBarVolatile before the load.if(!support_IRIW_for_not_multiple_copy_atomic_cpu){Node*mb=insert_mem_bar(Op_MemBarVolatile,store);// Use fat membarMemBarNode::set_store_pair(leading_membar->as_MemBar(),mb->as_MemBar());}}...}
内存屏障组合
insert_mem_bar(Op_MemBarRelease);// ① Release 屏障// ... store ...insert_mem_bar(Op_MemBarVolatile);// ② volatile 屏障

MemBarVolatile就会生成在解释执行中出现过的lock addl指令。

二、putOrderedXXXvolatile set的区别

在解释执行时,两者相差无几,在性能上没有什么优化,而在C2编译执行时,两者相差较大,前者明显在执行性能上优于后者:

1. 内存屏障语义

对比维度putOrderedXXXvolatile set
写操作前Release屏障(StoreStore + LoadStore)Release屏障(StoreStore + LoadStore)
写操作后无屏障StoreLoad屏障(MemBarVolatile/lock addl
内存语义Release语义Release + 全屏障语义

2. 核心差异:写后屏障

  • putOrderedXXX:写操作完成后不插入 StoreLoad 屏障,允许后续的 Load 操作被重排序到该写操作之前(从其他线程视角看,写入结果不保证立即可见)。
  • volatile set:写操作完成后强制插入 StoreLoad 屏障(x86 上为lock addl),禁止任何后续读写越过该写操作,保证写入结果对其他线程立即可见。

3. 性能对比

putOrderedXXXvolatile set
开销较低(无lock addl较高(有lock addl
适用场景只需保证读写操作不被延后到写操作之后(如单生产者队列尾指针更新)需要写操作对其他线程立即可见的强一致性场景

三、实验验证

我们使用jmh来验证我们的分析结论是否正确:

@Warmup(iterations=1,time=5,timeUnit=TimeUnit.SECONDS)@Measurement(iterations=1,time=5,timeUnit=TimeUnit.SECONDS)@Fork(1)@BenchmarkMode(Mode.Throughput)@OutputTimeUnit(TimeUnit.MILLISECONDS)@State(Scope.Benchmark)publicclassVolatilePerfBenchmark{privatestaticAtomicIntegerint1=newAtomicInteger(1);privatestaticAtomicIntegerint2=newAtomicInteger(1);privatestaticvolatilebooleantest1=false;privatestaticbooleantest2=false;@Benchmarkpublicvoidbaseline()throwsException{}@BenchmarkpublicbooleantestVolatileGet()throwsException{returntest1;}@BenchmarkpublicbooleantestNormalGet()throwsException{returntest2;}@BenchmarkpublicinttestAtomicGet()throwsException{returnint1.get();}@BenchmarkpublicvoidtestVolatileSet()throwsException{test1=true;}@BenchmarkpublicvoidtestAtomicSet()throwsException{int1.set(2);}@BenchmarkpublicvoidtestAtomicSetLazy()throwsException{int2.lazySet(3);}@BenchmarkpublicvoidtestNormalSet()throwsException{test2=true;}publicstaticvoidmain(finalString[]args)throwsException{Optionsopt=newOptionsBuilder().include(VolatilePerfBenchmark.class.getSimpleName())//.addProfiler(StackProfiler.class).result(VolatilePerfBenchmark.class.getSimpleName()+".json").resultFormat(ResultFormatType.JSON).build();newRunner(opt).run();}}

最终得到的结果如下:

Benchmark Mode Cnt Score Error Units VolatilePerfBenchmark.baseline thrpt 2728982.687 ops/ms VolatilePerfBenchmark.testNormalSet thrpt 2142444.611 ops/ms VolatilePerfBenchmark.testAtomicSetLazy thrpt 1342074.026 ops/ms VolatilePerfBenchmark.testVolatileSet thrpt 110419.432 ops/ms VolatilePerfBenchmark.testAtomicSet thrpt 110253.050 ops/ms VolatilePerfBenchmark.testNormalGet thrpt 363300.966 ops/ms VolatilePerfBenchmark.testAtomicGet thrpt 301024.312 ops/ms VolatilePerfBenchmark.testVolatileGet thrpt 361102.161 ops/ms

可以参考如下的图表得到更直观的观测结果:

读写操作性能差异显著

  • 写操作比读操作慢得多(testNormalSet vs testNormalGet:6倍差距)
  • volatile读操作与普通读操作性能相当
  • lazy写操作比普通写操作慢约1倍
  • volatile写操作比普通写操作慢约20倍
  • atomic写操作是最慢的,和volatile写操作相差无几

备注,实验使用的工具版本:

  • JMH version: 1.20
  • VM version: JDK 1.8.0_362, VM 25.362-b1

总结

putOrderedXXX只保证Release 语义(读写不被延后),但不保证写后立即对其他线程可见;
volatile set在此基础上额外插入StoreLoad 屏障,保证写操作对所有线程立即可见,代价是更高的性能开销。

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

DebateLab-个人博客(1)后端总体架构与比赛状态机设计

在这一篇博客中,我打算首先把后端整体的框架搭建好,后端是单体 Spring Boot 应用,首先确定下来了项目整体目录安排,由于本次项目涉及到了许多板块和业务,项目内容量较大,如果只是按照单个的controller或者s…

作者头像 李华
网站建设 2026/4/20 22:00:46

学会给AI搭系统,才是2026年最值钱的技能!收藏这份保姆级指南

文章对比了学习AI工具和使用AI系统两种方式,强调后者更具有长远价值。通过实例展示,搭建AI系统可以极大提高效率,且这种能力比单纯会使用AI工具更难掌握,因此更值得学习。文章提出“驾驭工程”概念,并给出普通人学习搭…

作者头像 李华
网站建设 2026/4/20 22:00:15

C# 创建vba用的类库

目录一. 需求二. 初始化项目三. 项目代码3.1 Tool.cs主类3.2 AssemblyInfo.cs配置类四. 编译五. 将.dll类库注册到系统六. vba中使用一. 需求 🔷写vba代码的时候,会想下面这样使用CreateObject创建一个对象,然后使用其中的方法 Sub SendGet…

作者头像 李华
网站建设 2026/4/20 21:59:22

嵌入式BI革命:SaaS/ISV厂商如何用衡石科技快速上线数据分析能力

导语: 客户要求产品内置数据分析功能,但自研成本高、周期长。衡石科技的嵌入式BI解决方案,让SaaS厂商最快两周内就能交付专业级数据分析能力,并将客户活跃度提升40%以上。一、SaaS厂商的共同焦虑在客户数字化需求日益升级的今天&a…

作者头像 李华