目录
1. JMM
主内存:共享区域,就是内存条
工作内存:每个线程的内存,是线程独有的。
JMM的性质:
- 可见性
- 原子性
- 有序性
JMM规范是大多数并发编程所要遵守的规范。
可见性:当其中一个线程对共享变量完成更改的时候,那么其它线程就会第一时间更新自己的共享变量的副本(及时通知)。
原子性:原子性指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
有序性:
- 这里我们就要讲一下指令重排了
看下面这一行代码,int a,b,x,y = 0,没有数据依赖,所以计算机可以对其进行重排,但是若我们在多线程下运行,就有可能不能得到我们期望的结果。
2. volatile
volatile是低配版的同步机制,比JMM要弱一些
volatile: 是Java虚拟机提供的轻量级同步机制
- 保证可见性
- 不保证原子性(与JMM不同)
- 禁止指令重排
因为volatile不保证原子性,所以是一个轻量级的同步机制,只能修饰变量。
-
如何让volatile保持原子性?
答:用JUC下对应的Atomic类,例如int类型对应的就是 AtomicInteger 类。 -
那么AtomicInteger的底层是如何实现的呢?
答: CAS -
什么是CAS?
答: 我们接下来讲解
2.1. volatile原理
3. CAS
想一想:为什么Atomic类底层用的是CAS,而不是 synchronized呢?
答:synchronized是直接将那个方法锁住,同时只能有一个线程访问,这样有点杀鸡用牛刀,换言之,用synchronized来解决原子性,有点太重了。
3.1. CAS是什么?
比较并交换
CAS:就是compareAndSet的缩写
3.2. CAS原理
- 线程把修改后的值写入主存的时候,会有包含两个值,一个是expect(期望值)也就是读取时候主存原本的值,另一个是update(修改值)就是修改之后的值。
- 当要写入的时候,线程发现主存的值和expect无法匹配,说明已经主存的值已经被其它线程抢先修改了,于是就会重新读取主存的值。
图示:
比较:比较期望值和主内存的值
交换:将主存的值覆盖(或者重新读取值)
3.3. CAS底层
CAS的底层其实是由Unsafe类实现的,那么这个类中的方法是可以直接操作地址的,所以不会被打断。
3.4. CAS的缺点
- 循环时间长,开销大
- 举例: 一个线程比较倒霉,每一次想修改的主存的数据的时候,那个值都已经被其它线程修改过了,那么它就陷入了死循环,这可能会给CPU带来很大的开销。
- 只能保证一个共享变量的原子操作。
- 引出ABA问题。
3.5. ABA问题
- 什么是ABA问题?
- 答:狸猫换太子
CAS的原理是在取出数据的时候一致,写入数据的时候,主存中的数据与期望数据也一直,线程就会认为这个数据没有被动过,但是在中间的这个过程,这个A值可能很多次被换成了B值,又被换了回A值,这就是ABA问题。
3.6. ABA问题解决(时间戳原子引用)
- 原子引用(AtomicReference)
-
AtomicReference是作用是对”对象”进行原子操作。
提供了一种读和写都是原子性的对象引用变量。原子意味着多个线程试图改变同一个AtomicReference(例如比较和交换操作)将不会使得AtomicReference处于不一致的状态。 -
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。
- 时间戳原子引用(AtomicStampedReference)
时间戳可以理解为版本号,这样我们就能知道一个值是否被修改过。
- 看下表
值 | 版本号 |
---|---|
A | 1 |
B | 2 |
A | 3 |
- 我们可以发现,在有版本号的情况下,虽然A的值看似没变,其实它的版本号已经变化,所以它一定被其它线程更改过。
3.7. CAS解决了什么问题?
答:解决了sync执行效率低的问题。
问:为什么?
答:synchronized是操作系统层面的 主要是对象状态控制的,但是锁的获取和释放需要借助操作系统的用户状态和系统内核状态切换。
cas的话就比较简单粗暴了,一直不停的判断,判断,判断,状态满足了就替换,不满足再判断