学习情况记录
记录在学习线程安全知识点中,关于 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中并发容器部分的知识,属于比较难的内容。目前先引用几篇文章。作为记录。