JAVA锁体系、CAS、AQS详细讲解
## **Java的锁体系**
1. 悲观锁:悲观锁的实现是synchronized关键字和接口Lock的实现类,适用写的操作多的场景
2. 乐观锁:乐观锁的实现是CAS算法,例如并发包下的AtomicInteger类的原子自增是CAS的自旋操作实现,下面会根据源码分析CAS算法和自旋;
## CAS算法源码分析和应用场景
CAS(compareAndSwap),比较和交换,是一种无锁原子算法,有三个参数CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。**当V值等于E时,才会将V设置为N**;如果V和E不相等,说明已经有其他线程对V做了更新,则当前线程什么都不做,最后,CAS返回当前V的真实值!CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。
通过**AtomicInteger**的**addAndGet**()方法,这个方法是基于CAS写的;
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//得到value这个值在内存中的偏移量即地址
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//共享变量使用volatile关键字修饰,保证共享变量的可见性 AtomicInteger的构造函数中对value进行复制
private volatile int value;
public final int addAndGet(int delta) {
//调用时Unsafe类的getAndAddInt()方法
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
//自旋
do {
//得到此时var2这个偏移量在内存中的值,即期望值var5
var5 = this.getIntVolatile(var1, var2);
//compareAndSwapInt通过var1, var2得出实际值,然后和var5进行对比,
//如果相同则把var5 + var4(新值写入内存),如果不相同不断do,while循环(自旋)直到相同。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
上述代码块就是自旋算法,其中的compareAndSwapInt()是原子操作,不会引发并发问题;
CAS缺点
CAS算法高效的解决了原子操作,但是CAS仍然存在三个问题,ABA问题、循环时间长开销大、只能保证一个共享变量的原子操作
ABA问题
候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
AQS
AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
同步队列
同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点假如到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程是否锁时,会唤醒后面的节点并释放当前头节点的引用。
如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。
state状态
AbstractQueuedSynchronizer维护了一个volatile int类型的变量,用户表示当前同步状态。volatile虽然不能保证操作的原子性,但是保证了当前变量state的可见性。至于volatile的具体语义,可以参考相关文章。state的访问方式有三种:
getState、setState、compareAndSetState 三者均是原子操作;其中compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法,CAS算法!
自定义资源共享方式
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
上一篇: 并发专题(五)CAS与AQS
下一篇: 数据结构(C语言)-循环队列基本操作