序言

在对java并发编程的考察中CAS和AQS这两个概念的考察出现的频率是非常高的,所以本文主要通过笔者看过的文章和源码解读做一些整理和自己的思考,关于这两个概念。

面试题举例

  • java哪些地方使用到了CAS
  • CAS有什么问题怎么解决
  • AQS独占式获取\释放锁的原理

前置概念

1、线程同步

在多线程环境中,协调多个线程对共享资源的访问,以确保数据的一致性和完整性。在多线程程序中,多个线程可能会同时访问和修改共享资源,如果没有适当的同步机制,可能会导致数据竞争(Race Condition)、数据不一致或死锁等问题。线程同步的目的是通过控制线程的执行顺序,确保在任意时刻只有一个线程可以访问共享资源,或者多个线程可以安全地访问共享资源。

2、线程安全

在多线程环境中,一个类或代码段能够被多个线程安全地访问,而不会导致数据不一致或错误的行为。换句话说,线程安全的代码在并发执行时,能够保证数据的完整性和一致性。

实现方法:

  1. 互斥同步:synchronized,ReentrantLock
  2. 非阻塞同步:CAS,Atomic
  3. 无同步方案:栈封闭,Thread Local,可重入代码

CAS

CAS本质上是通过硬件实现的,是一条CPU的原子指令,让CPU比较两个值是否相等,然后原子地更新某个位置,所以CAS是基于硬件实现的,JVM对底层的接口进行了封装,JDK中的原子类则是对这个接口进行了调用
这里的CAS是基于底层硬件实现的,除了在并发编程中有无锁化这种理念,其实我还觉得,这是由于虚拟机会进行指令重排

CAS可能产生的问题

  1. ABA问题

    解决方案:
    使用版本号,每次更新变量的值的时候,就在版本号上加一Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。其实在MySQL数据库中一些无锁化的处理也是这样基于版本号实现的

  2. 循环时间长开销大

    解决方案:
    JVM支持处理器提供的pause指令

  3. 只能保证一个共享变量的原子操作

    解决方案:
    通过JDK提供的AtomicReference来实现对多个共享变量的原子操作

底层的Unsafe类实现和原子类这里先不说了,等笔者弄明白再继续写,不过感觉Unsafe这个部分在面试中出现的频率并不高,原子类的考察则偏简单一些

AQS

AQS是一个构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的同步器,比如ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue等

AQS作为抽象类只实现了部分逻辑(模版方法),并且使用了队列这个数据结构,具有先进先出的特性

AQS的内部数据结构

  • state 同步状态
  • 实现了一个双端队列,存储着封装成Node节点的线程,实现了一些排队和阻塞的功能

AQS在面试中的主要考察点

AQS独占式获取/释放锁的原理

获取锁:
1获取同步状态时,调用 acquire 方法,维护一个同步队列;
2使用 tryAcquire 方法安全地获取锁,如果成功返回true,失败返回false;
3获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部;
4执行 acquireQueued 方法使得该节点以自旋的方式获取锁,如果获取不到则阻塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点是头结点且成功获取了同步状态。
5成功获取锁后,将从acquireQueued方法中退出,同时返回一个boolean值表示当前线程是否被中断,若被中断,则会执行selfInterrupt方法,响应中断。
释放锁:
1释放同步状态时,同步器调用 tryRelease 方法释放锁;
2然后调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取锁。

AQS共享式获取/释放锁的原理

获取锁:
1首先调用tryAcquireShared方法尝试获取一次共享锁,即修改state的值,若返回值>=0,则表示获取成功,线程不受影响,继续向下执行;
2若返回值小于0,表示获取共享锁失败,则线程需要进入到同步队列中等待,调用doAcquireShared方法。
释放锁:
1使用releaseShared模板方法释放锁,通过调用使用者自己实现的tryReleaseShared方法尝试释放锁,修改state的值,若返回true,表示修改成功,则继续向下调用doReleaseShared唤醒head的下一个节点对应的线程,让它开始尝试获取锁;
2若修改state失败,则返回false。

这里可能还会考察通过AQS实现的一些类比如ReentranLock,这个打算放到锁的那个部分再说。

结语

这篇便主要讲了一下AQS和CAS可能会在面试中考察的点,说的都是可能直接问的地方,也说了一下原理,本身我对这个原理理解也不是特别的深入,这能尽自己所能了。

参考文章:

https://redspider.gitbook.io/concurrent/di-er-pian-yuan-li-pian/11
https://www.pdai.tech/md/java/thread/java-thread-x-juc-AtomicInteger.html#cas-%E9%97%AE%E9%A2%98
https://www.pdai.tech/md/java/thread/java-thread-x-lock-AbstractQueuedSynchronizer.html
https://redspider.gitbook.io/concurrent/di-er-pian-yuan-li-pian/11