1.原子性
原子性:一个或多个指令在CPU执行过程中是不允许中断的。
问:i++是否是原子性操作
答:否,i++操作一共有三个指令,不属于原子性操作
-
getfield:从主内存拉取数据到CPU寄存器
-
iadd:在寄存器内部对数据进行+1
-
putfield:将CPU寄存器中的结果写到主内存中
如何保证i++是原子性的
synchronized
lock
使用lock锁会有和synchronized类似的概念,在操作i++的三个指令前,先基于AQS成功修改state后才可以操作
CAS(compare and swap)
线程基于CAS修改数据的方式:先获取主内存数据,在修改之前,先比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃这次修改
CAS在Java层面就是Unsafe类中提供的一个native方法,底层通过CPU指令cmpxchg(x86,不同架构CPU或有不同指令支持)保证了操作的原子性,这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略,需要自己在代码中实现
CAS问题:
-
CAS只能对一个变量的修改实现原子性。
-
CAS存在ABA问题。
- A线程修改主内存数据从1~2,卡在了获取1之后。
- B线程修改主内存数据从1~2,完成。
- C线程修改主内存数据从2~1,完成。
- A线程执行CAS操作,发现主内存是1,没问题,直接修改
- 解决方案:加版本号
-
在CAS执行次数过多,但是依旧无法实现对数据的修改,CPU会一直调度这个线程,造成对CPU的性能损耗
- synchronized的实现方式:CAS自旋一定次数后,如果还不成,挂起线程
- LongAdder的实现方式:当CAS失败后,将操作的值,存储起来,后续一起添加
CAS:在多核情况下,有lock指令保证只有一个线程在执行当前CAS
2.有序性
指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。
如果不希望CPU对指定进行重排序,怎么办?
可以对属性追加volatile修饰,就不会对当前属性的操作进行指令重排序。
什么时候指令重排: 满足happens-before原则,即可重排序
单例模式中为什么要使用DCL(double checked locking)双重判断
正常顺序:
申请内存,初始化,关联
如果CPU对指令重排,可能会造成指令顺序为:
申请内存,关联,初始化
在还没有初始化时,其他线程来获取数据,导致获取到的数据虽然有地址引用,但是内部的数据还没初始化,都是默认值,导致使用时,可能出现与预期不符的结果
3 可见性
可见性:前面说过CPU在处理时,需要将主内存数据拉取到CPU寄存器中再执行指令,指令执行后,需要将寄存器数据写回到主内存中。
基于缓存一致性协议(MESI等)CPU处理完数据后会先放到缓存中,然后同步到内存中。
不是每次操作结束就立刻将CPU缓存数据同步到主内存。造成多个线程看到的数据不一样。
-
volatile每次操作后,立即同步数据到主内存。
-
synchronized,触发同步数据到主内存。
-
final未发生this引用逃逸的情况下也可以保证可见性