欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA锁体系、CAS、AQS详细讲解

程序员文章站 2022-05-04 17:25:38
...

## **Java的锁体系**


JAVA锁体系、CAS、AQS详细讲解

 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中的并发同步组件提供统一的底层支持,例如ReentrantLockCountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
 

同步队列

同步队列是AQS很重要的组成部分,它是一个双端队列,遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点假如到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程是否锁时,会唤醒后面的节点并释放当前头节点的引用。 
 

JAVA锁体系、CAS、AQS详细讲解

 

如下图所示。AQS为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。

JAVA锁体系、CAS、AQS详细讲解

 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。