注明:
参考书作者:方腾飞 魏鹏 程晓明
参考书目:《Java 并发编程的艺术》
原子操作的实现原理
处理器如何实现原子操作
处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
(1)使用总线锁保证原子性
所谓总线锁就是使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
(2)使用缓存锁保证原子性
在同一时刻,只需保证对某个内存地址的操作是原子性的即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,故用缓存锁代替总线锁进行优化。
使用缓存锁定,当执行锁操作写回到内存中时修改内存中内存地址,并通过缓存一致性阻止同时修改由两个以上处理器缓存的内存区域数据,其他处理器的缓存行会无效。
- 有两种情况处理器不会使用缓存锁定:
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,处理器会调用总线锁定
第二种情况是:有些处理器不支持缓存锁定。
JAVA如何实现原子操作
在Java中可以通过锁和循环CAS的方式来实现原子操作。
(1)使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的,该指令是通过缓存锁定将指令操作的内存区域加锁来实现原子操作。
从Java1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean,AtomicInteger,AtomicLong,可以通过这些类的api来实现自增1,自减1。
(2)CAS实现原子操作的三大问题
- ABA问题
CAS是通过检查值是否发生变化,如果没有变化则更新,有发生变化则不更新。
但是一个值从原来的A ,变成B,再变成A,CAS检查时就发现不了已经变化了。
该问题的解决思路是通过版本号,每次变量更新都将版本号加1。 - 循环时间长开销大
自旋长时间不成功,会给CPU带来非常大的执行开销。
JVM提供pause指令,效率提升。pause指令有两个作用:
第一,延迟流水线执行指令
第二,避免退出循环时内存顺序冲突而引起cpu流水线被清空。 - 只能保证一个共享变量的原子操作
多个共享变量操作时,循环CAS就无法保证操作的原子性。
方法一,CAS是不加锁的,这个时候用锁可以解决
方法二,将多个变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。可以通过AtomicReference类来保证引用对象之间的原子性。
(3)使用锁机制实现原子操作
JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁。
除了偏向锁,当一个线程进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。