CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。 属于硬件级别的操作,效率比加锁操作高;
- 首先看一把Unsafe的代码:
- AtomicInterger的代码,只说明getAndAdd方法的原理,其它的都大同小异:
- 至于偏移量的理解:
一个java对象在内存中是一块内存,各个字段都得按照一定的顺序放在这段内存里,同时考虑到对齐要求,可能这些字段不是连续放置的,用这个方法能准确地告诉你某个字段相对于对象的起始内存地址的字节偏移量。 - 接下来看下hotspot底层实现:
- 查看 unsafe.cpp 代码位置:/hotspot/src/share/vm/prims/unsafe.cpp
- 再看: Atomic::cmpxchg的实现:
代码位置:/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
- lock前缀的作用说明:
1.禁止该指令与之前和之后的读和写指令重排序;
2.把写缓冲区中的所有数据刷新到内存中;
原子操作的弊端
- ABA问题: CAS存在一个很明显的问题,即ABA问题
如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾 经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference或者,它可以通过控制版本号stamp来保证CAS的正确性。
- 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
把自己成长经历上学到的东西总结起来,只是希望一段时间之后,自己看到这篇文章能知其然知其所以然;