有关volatile和CAS在JUC中的理解
@[JUC|并发]
[TOC]
并发编程一直是Java语言一个非常重要的特性,J.U.C作为Java多线程的工具包,它封装了许多并发操作的实现细节,例如如何使用线程池,提供了各种锁的实现,线程安全的容器等等。J.U.C提供的这些功能,其基础依靠的是volatile关键字和CAS操作实现的。本文将分析volatile和CAS具体实现原理和它们在J.U.C中实际的使用场景,来探讨它们是如何作为J.U.C基础组件的。
从jdk1.5开始,Doug Lea在jdk中增加了J.U.C这个并发工具包,它的全称是java.util.concurrent,在这个包下,提供了许多并发工具:
locks锁工具:例如ReentrantLock
atomic原子类:例如AtomicInteger
executor执行器:线程池执行器ThreadPoolExecutor
collection线程安全容器:ConcurrentHashMap
sync同步器框架:AQS
这些同步工具和框架,它们大部分通过无锁的方式实现线程安全,从而大大提高了多线程并发执行的效率,无锁方式实现线程安全的原理便是通过volatile关键字和CAS操作,J.U.C的整体架构:

有关并发问题的理解
什么是线程安全:在多线程环境下,针对某一共享对象或属性的并发操作,总是能得到我们想要的结果。
在多线程并发过程中,我们通过三个特性来保证并发安全,它们分别是原子性、可见行和有序性
原子性
所谓原子性是指针对某一个操作必须是不可分割的、原子的,例如针对i++这个操作,当多个线程并发执行这个动作,对内存中的同一个属性值执行+1操作,这个操作不是一个原子操作,具体的执行过程可能存在多种情况,从而导致最终结果与我们预想的结果不一致。
对不可分割的理解:对于某一个操作,当前线程要么全部执行完毕,要不全部状态回滚,同时在执行过程中不能让出执行权。

i++被分割成了三个操作,读取、加1和写入操作,在加1阶段,可能发生其它的去读、加1和写入操作,而在写入操作是也可能发生其它的读取、加1和写入操作,因此两个线程执行完i++最后i的结果确实1;
可见性
可见行:当一个线程修改了某一共享变量后,能够对其它线程可见,其它线程可以获取到这个变量对更新值
在Java的内存模型中,每个线程有自己的工作内存,当要读取和修改某一个共享变量的时候,需要从共享的主内存中复制一份变量到工作内存,然后进行修改,如果这时其它线程修改了这个贡献变量,当前线程是无感知的,这也就会导致不可见。
有序性
有序性:程序的执行顺序按照代码的先后顺序执行
在单线程内,程序的执行是串行的,也就是代码顺序执行,但是多线程环境下,不同线程可能存在切换,执行顺序不一定是按照代码的顺序执行
volatile关键字的原理
在前面关于可见性问题上我们认为线程之间是不存在可见行的,我们可以编写一段代码进行验证,首先定义一个共享变量stop
1 | public static boolean stop; |
执行一下方法:
1 | public static void unVisibility() throws InterruptedException { |
执行结果:
1 | Thread Thread-0 is running... |
当Thread-1执行完毕,线程Thread-0一直没有退出,也就是一直在死循环。
我们在定义一个由volatile修饰的变量vStop
1 | public static volatile boolean vStop; |
执行以下方法:
1 | public static void visibility() throws InterruptedException { |
执行结果:
1 | Thread Thread-0 is running... |
可以看到两个线程都正常退出了,因此通过volatile可以保证线程的可见性
volatile可见行原理
volatile关键字的语义:被volatile修饰的共享变量在多个线程之间是可见的,当一个线程修改volatile修饰的属性时,它会将当前线程在CPU缓存区中该属性的值刷新回主内存,而在读取共享属性时,会将读取线程的工作内存中缓存的共享变量置为无效,同时重新加载主内存的值。
实现原理:在执行写操作时,jvm会在写操作的汇编指令上加上lock指令
该指令将使当前处理器中的
缓存行的所有数据会写进主内存在写入主内存后将其它处理器中该共享变量的缓存失效
在写入的过程中,lock指令会对共享变量的缓存区域和主内存区域加锁,避免多个写入操作,而在写入完主内存后根据缓存一致性原理,将使其它处理器中的缓存失效。
CAS操作的原理
CAS全称是Compare and Set,即比较并赋值,虽然这是两个操作,但是在底层操作系统中,这是一个原子不可分割的操作,这也就体现了我们并发时候的原子性。在进行CAS操作的时候需要传入三个参数,分别是目标对象在内存中的地址,目标对象的期望值(期望值不一定是实际内存值,因此CAS操作可能失败),需要更新的值。在进行CAS操作的时候,当前线程首先根据内存地址获取到目标对象的值,同时与期望值进行比较,如果一致,则对其进行更新操作,更新为我们需要更新的值;如果不一致,说明在获取目标对象值之后有其它线程修改了目标对象,那么不需要进行任何操作。
CAS流程图:
在比较失败的时候,有些场景可能出现自旋,当前线程并不是立刻结束退出,而是自己循环一段时间,每次循环重新从内存中更新期望值,然后再次比较。
volatile和CAS是如何作为JUC的基础
Question:
- 关于CAS,如果CAS是原子的,为什么还会出现比较期望值时出现不一致的情况?

