原子操作意为不可被中断的一个或一系列操作。我们来看看处理器和Java如何实现原子操作的。
处理器中实现原子操作
处理器提供总线锁定和缓存锁定两种机制来实现原子操作。
使用总线锁保证原子性
多个处理器对共享变量进行读改写操作时,这样读改写操作就不是原子的。
总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
使用缓存锁保证原子性
总线锁定把CPU和内存之间的通信锁住了,其他处理器不能处理其他内存地址的数据,所以总线开销比较大,现在的处理器会选择使用缓存锁定来代替总线锁定。
频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行。
缓存锁定就是内存区域如果被缓存在处理器的缓存行中,并在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已经被锁定的缓存行的数据时,会使缓存行无效。
但两种情况不会使用缓存锁定:
1.当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
2.处理器不支持缓存锁定。
Java中实现原子操作
Java中通过锁和循环CAS的方式来实现原子操作
使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。
CAS(Compare and Swap)比较并交换:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
从Java1.5开始,JDK的并发包里提供了atomic包来支持原子操作。
CAS实现原子操作的三大问题
1.ABA问题
CAS操作时,会检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值为A,变为B,又变为A,那么用CAS进行检查时会发现它的值没有发生变化,但实际发生了变换,这就是ABA问题。
ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加1。从Java1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类里的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2.循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来很大的开销。
解决方式就是如果JVM能支持处理器提供的pause指令,那么效率上会有一定提升。pause指令有两个作用:
1.它可以延迟流水线执行指令,使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本。
2.它可以避免在退出循环的时候因内存顺序冲突而引起CPU流水线被清空,从而提高CPU的执行效率。
内存顺序冲突:内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现内存顺序冲突时,CPU必须清空流水线。
3.只能保证一个共享变量的原子操作
对多个共享变量操作时,循环CAS无法保证操作的原子性。
这个时候可以用锁,或者把多个共享变量合并成一个共享变量来操作。
从Java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放入一个对象来进行CAS操作。
使用锁来实现原子操作
JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。其中除了偏向锁,JVM实现锁的方式都使用了循环CAS来获取和释放锁。