学习情况记录
记录在学习线程安全知识点中,关于 CAS
的有关知识点。
线程安全是指:多个线程不管以何种方式访问某个类,并且在主调代码中不需要进行同步,都能表现正确的行为。
常见的线程安全实现方法分为不可变对象、线程互斥同步、非阻塞同步、线程本地存储等方案,本文要讲的就是非阻塞同步中的核心 CAS
.
从处理问题的方式上说,互斥同步属于一种悲观的并发策略。
随着 硬件指令集 的发展,我们可以采用 基于冲突检查的乐观并发策略,通俗地说,就是先行操作,如果没有其他线程争用共享数据,那操作就成功了; 如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现偶读不需要把线程挂起,因此这种同步操作称为非阻塞同步。
乐观锁需要 操作和冲突检测 这两个步骤具备原子性,这里就不能再使用互斥同步来保证了, 只能靠硬件来完成 。 硬件支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS) 。 CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
各种Atomic开头的原子类,内部都应用到了 CAS
。就拿 AtomicInteger
为例。
J.U.C 包里面的原子类 AtomicInteger
的方法调用了 Unsafe
类的 CAS
操作。
看看 AtomicInteger
对象一次自增, CAS
起了什么作用,以下代码是 incrementAndGet()
的源码,可以看到内部调用了 Unsafe
对象的 getAndAddInt()
。
以下代码是 getAndAddInt()
源码, var1
指示对象内存地址, var2
指示该字段相对对象内存地址的偏移, var4
指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2)
得到旧的预期值,通过调用 compareAndSwapInt()
来进行 CAS
比较 , 如果该字段内存地址中的值等于 var5
,那么就更新内存地址为 var1+var2
的变量为 var5+var4
。
compareAndSwapInt(var1, var2, var5, var5 + var4
其实换成 compareAndSwapInt(obj, offset, expect, update)
比较清楚,意思就是如果 obj
内的 value
和 expect
相等,就证明没有其他线程改变过这个变量,那么就更新它为 update
,如果这一步的 CAS
没有成功,那就采用自旋的方式继续进行CAS操作,取出乍一看这也是两个步骤了啊,其实在JNI里是借助于一个CPU指令完成的。所以还是原子操作。
循环时间长开销大
CAS
(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来比较大的执行开销。 只能保证一个共享变量的原子操作
CAS
只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS
无效。但是从 JDK 1.5开始,提供了 AtomicReference
类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS
操作.所以我们可以使用锁或者利用 AtomicReference
类把多个共享变量合并成一个共享变量来操作。 使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。关于非阻塞算法是属于J.U.C中并发容器部分的知识,属于比较难的内容。目前先引用几篇文章。作为记录。