一、volatile的定义与实现原理
Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了 确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言 提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存 模型确保所有线程看到这个变量的值是一致的。
volatile是如何来保证可见性的呢?通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,CPU会做些什么。
java代码如下:
instance = new Singleton(); //instance是volatile声明的变量
转变成汇编代码,如下:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,lock前缀的指令在多核处理器下会引发了两件事情:
- 将当前处理器缓冲行的数据写回到系统内存
- 这个写会内存的操作会使其它CPU里缓存了改内存地址的数据无效
二、原子操作的实现原理
1.处理器实现的原子操作
处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
(1)使用总线锁保证原子性
第一个机制是通过总线锁来保证原子性。如果多个处理器同时对共享变量进行读改写操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的。想要保证读改写共享变量的操作是原始的,就需要保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
处理器使用总线锁来解决这个问题。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出次信号时,其它处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
(2)使用缓存锁保证原子性
第二个机制就是通过换错锁定来保证原子性。在同一个时刻,我们只需要保证对某个内存地址的操作是原子性即可,但总线锁把CPU和内存之间的通讯锁住了,这使得锁定期间,其它处理器不能操作其它内存地址的数据,所以总线锁的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
频繁使用的内存会缓存在处理器的L1、L2、和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁。所谓缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性协议来保证操作的原子性,因为缓存一致性协议会阻止同时修改由两个以上处理器缓存的内存区域数据,当其它处理器回写已被锁定的缓存行数据时,会使缓存行无效。
但是有两种情况下处理器不会使用缓存锁定:
- 当操作的数据不能被缓存在处理器内部,或操作的数据同时跨多个缓存行(cache line),处理器会调用总线锁定。
- 有些处理器不支持缓存锁定。
2. Java如何实现原子操作
在Java中可以通过锁和循环CAS的方式来实现原子操作。
(1)使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令来实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
(2)CAS实现原子操作的三大问题
CAS虽然很高效地解决了原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。
- ABA问题。CAS需要在操作值的时候,检查值有没有发生变化,如果没有变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。从Java1.5开始,JDK的Atomic包里提供了一个类
AtomicStampedReference
来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 - 循环时间开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(depipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体的实现版本;第二,它可以避免在退出循环的时候因为内存顺序冲突(Memory Order Violation)而引起的CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作。对多个共享变量操作时,CAS无法保证操作的原子性。有两个方法可以解决:第一种通过锁的方式,第二种可以把多个共享变脸葛冰成一个共享变脸来操作。JDK提供了
AtomicReference
类来保证引用对象之间的原子性。
(3)使用锁机制实现原子操作
锁机制保证了只有获取锁的线程才能操作锁定的内存区域。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了