本章主要介绍一下原子 操作的概念以及JAVA是如何保证原子性的。
原子:本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。接下来让我们看一下处理器和Java里是如何实现原子操作的。
1)处理器如何实现原子操作
处理器主要是提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
第一个机制是通过总线锁保证原子性。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
第二个机制是通过缓存锁定来保证原子性。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
2)Java如何实现原子操作
在Java中可以通过锁和循环CAS的方式来实现原子操作。
①使用循环CAS实现原子操作:自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
CAS算法:英文全称是Compare and Swap,比较和交换,CAS算法属于乐观锁。在java.util.concurrent包中借助CAS实现了区别于synchronized悲观锁的一种乐观锁。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS的原理:CAS通过调用JNI的代码实现的。JNI即JAVA本地调用,允许java调用其他语言。java中实现CAS的方法是借助其他语言(比如C语言)来调用CPU底层指令实现的。(CAS算法在硬件中的实现是cmpxchg指令,保证这个指令的原子性。执行这个指令时,会通过CPU锁(就是上面解释的总线锁、缓存锁)来实现的原子性。java在1.5之后其实是提供了调用硬件cmpxchg指令的方法(Unsafe类中那个native CAS方法))。
从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。
使用CAS可以提高性能,但是CAS同样也存在以下几个问题:
ABA问题:CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
②使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁等。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。