并发编程-原子操作实现原理
一、简介
原子,不能进一步分割的最小粒子;原子操作,不可被中断的一个或一系列操作。
二、处理器实现原子操作
处理器会保证基本的原子性,例如从内存中读一个数据或写一个数据。但是对复杂的内存操作不能保证其原子性。
处理器提供总线锁和缓存锁来保证复杂内存操作的原子性。
1. 使用总线锁
总线锁,就是使用处理器提供的一个LOCK # 信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器就可独占共享内存。
2. 使用缓存锁
缓存锁定, 是指内存区域如果被缓存在处理器的缓存行中,并且在 Lock 操作期间被锁定,那么当它执行锁操作写回内存时,处理器不在总线上声言 LOCK # 信号,而是修改内部的内存地址,并允许它的缓存一执行机制来保证操作的原子性,因为缓存一执行机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
有以下两种情况不会使用缓存锁定:
1.当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
2.当处理器不支持缓存锁定时,处理器会调用总线锁定。
三、Java 实现原子操作
Java 通过 锁 和 循环 CAS 的方式来实现原子操作。
1.使用循环 CAS
JVM 中的 CAS 操作利用了处理器提供的 CMPXCHG 指令实现的。自旋 CAS 实现的基本思路就是循环进行 CAS 操作直到成功为止。
从 Java 1.5 开始,JDK 的并发包提供了一些原子操作类,如 AtomicBoolean、AtomicInteger、AtomicLong
2.CAS 实现原子操作的三大问题
问题一:ABA问题
因为 CAS 需要在操作旧值时,检查值是否发生变化,若无变化则更新,但是如果一个值的变化是这样的 A->B->A, 那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA 问题的解决思路是使用版本号,在变量前面追加版本号,每次变量更新的时候把版本号加1,那么 A->B->A 就会变成 1A->2B->3A。
从 Java 1.5 开始,JDK 的 Atomic 包里提供了一个类AtmoicStampedReference来解决ABA问题;这个类的 compareAndSet 方法的作用首先是检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,若全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
问题二:循环时间长开销大
自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 支持处理器提供的 pause 指令,那么效率会有一定的提升。
pause 指令作用:
1.延迟流水线执行指令
2.避免因内存顺序冲突引起的CPU流水线被清空
问题三:仅能保证一个共享变量的原子操作
从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以实现多个变量放在一个对象里来进行 CAS 操作。
3.使用锁机制
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM 内部实现了很多种锁机制,除了偏向锁,JVM 实现锁的方式都是循环 CAS。

浙公网安备 33010602011771号