CAS与AQS简述

CAS的实现与问题

我们知道unsafe类的getAndAddInt()方法循环获取给定对象obj中的偏移量valueOffSet处的值oldValue,然后判断内存值是否等于oldValue。如果相等则将内存值设置为 oldValue + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
 
后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。
 
CAS虽然很高效,但是它也存在三大问题,这里也简单说一下:
 
1.ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
 
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
 
2.循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
 
为解决该问题,自JDK1.6之后,Java引入了适应性自旋锁。
 
3.只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
 
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
 
AbstractQueuedSynchronizer
AbstractQueuedSynchronizer类,简称AQS(后面我们将采用AQS来称呼该类)根据单词来翻译就是:抽象队列同步器,这个类的作用主要是用来实现我们Java并发库中的各种令人眼花缭乱的锁。它本身只是一个抽象类,但并发库中的ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore、ThreadPoolExecutor都用到了这个类,这些类的锁实现都继承自AbstractQueuedSynchronizer类。
 
我们看看下面这个继承图:
 
ReentrantLock及ThreadPoolExecutor.Worker类为独占锁的实现;
CountDownLatch、Semaphore类为共享锁的实现;
而ReentrantReadWriteLock则同时实现了独占锁与共享锁,这里不难理解其原因,因为读写锁中,写锁必须独占,读锁可以共享。
 
AQS提供了一个FIFO队列来实现锁以及各种同步功能的框架。
从锁的类型上说,AQS提供了独占锁与共享锁两种选择;从获取锁的模式上来说,AQS可以支持非公平锁与公平锁;从可重入性上来上说,AQS可以支持可重入锁与非可重入锁。

猜你喜欢

转载自www.cnblogs.com/chenchenxiaobao/p/10113474.html