术语定义
原子操作: 不可中断的一个或一系列操作
CPU术语定义
术语名称 | 英文 | 解释 |
---|---|---|
缓存行 | Cache Line | 缓存的最小单位 |
比较并交换(CAS) | Compare and Swap | CAS操作需要输入两个数值,一个旧值和一个新值,操作期间必须比较旧值有无发生变化,没有发生变化才会交换成新值 |
CPU流水线 | CPU pipeline | CPU中5 ~ 6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5 ~ 6步后再由这些电路单元分别执行,实现一个CPU时钟周期内完成一条指令。 |
内存顺序冲突 | Memory order violation | 一般由假共享引起。假共享指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效。出现该冲突时,CPU必须清空流水线。 |
处理器实现原子操作
对于基础内存操作,处理器可以自动保证操作的原子性
对于复杂内存操作,处理器提供总线锁定和缓存锁定两个机制来保证原子性
1)使用总线锁保证原子性
如果多个处理器同时对共享变量进行读改写操作(例如i++),那么共享变量会同时被多个处理器同时操作,这样的读改写操作是不原子的,原因在于多个处理器同时从各自的缓存中读取变量,分别进行修改操作,然后写回到系统内存中。
处理器使用总线锁处理该问题。
总线锁:使用一个 LOCK # 信号,当一个处理器在总线上输出该信号时,其他处理器的请求会被阻塞,该处理器即可独占内存。
2)通过缓存锁保证原子性
在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定会锁住CPU和内存之间的通信,这样会带来较大的开销。因此部分场合下处理器会使用缓存锁定来代替总线锁定。
频繁使用的内存会缓存在处理器的L1、L2、L3高速缓存中,原子操作就可以直接在处理器内部缓存中
缓存锁定:内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,则它执行锁操作回写到内存时,处理器修改的是内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。
缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
以下两种情况处理器下处理器不会使用缓存锁定:
第一种情况:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,处理器会调用总线锁定。
第二种情况:部分处理器不支持缓存锁定
针对这两种情况,Intel处理器提供了很多Lock前缀的指令来实现,例如以下几条指令。
位测试和修改指令:BTS、BTR、BTC
交换指令:XADD、CMPXCHG
其他一些操作数和逻辑指令:例如ADD、OR等
被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。
Java实现原子操作
Java中可以使用锁和循环CAS的方式实现原子操作。
1)使用循环CAS实现原子操作
JVM中的CAS操作是利用了处理器提供的CMPXCHG指令实现的。
自旋CAS实现的基本思路:循环进行CAS操作直到成功为止
Java 1.5以后,JDK的并发包提供了一些类来支持原子操作:
AtomicBoolean:用原子方式更新的boolean值
AtomicInt:用原子方式更新的int值
AtomicLong:用原子方式更新的long值
2)CAS实现原子操作的三大问题
CAS虽然能高效解决原子操作,但仍然存在以下三大问题
1.ABA问题
如果一个值原来是A,变成了B,又变成A,那么CAS检查时会认为它的值没有发生变化,但实际上已经发生过变化了。
解决思路:版本号。在变量前追加版本号,每次变量更新的时候再把版本号加1。
Java提供的类:AtomicStampedReference,该类的compareAndSet方法的作用是检查当前引用是否等于预期引用,并检查当前标志是否等于预期标志,如果都相等,则以原子方式将该引用和该标志的值设为给定的更新值。
2.循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
解决思路:JVM支持处理器的pause指令。
pause指令有以下两个作用:
- 延迟流水线执行的命令,使CPU不会消耗过多的执行资源。延迟时间取决于具体实现的版本
- 避免退出循环时因内存顺序冲突引起的CPU流水线清空,从而提高CPU执行效率
3.只能保证一个共享变量的原子操作
当对一个共享变量执行操作时可以使用循环CAS的方式来保证原子操作,但对多个共享变量时就不行了。
解决思路:
- 锁操作
- 将多个共享变量合并成一个共享变量。JDK提供了AtomicReference来保证引用对象之间的原子性,就可以把多个变量放在一个对象里进行CAS操作
3)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能操作锁定的内存区域。
但除了偏向锁以外,JVM实现锁的方式都使用了循环CAS
即当一个线程想进入同步块的时候使用循环CAS获得锁,退出的时候使用循环CAS释放锁