面试准备 --- CAS 和 AQS

这是面试准备过程的学习笔记,不能保证理解的正确性,慎,欢迎纠正

  • CAS和AQS其实并不是一类,只是这两个可能从名字容易搞混,所以放在一起说一下

CAS

  • 要说Java中的CAS,还是要先说一下自旋锁,有人认为二者是同一样东西,但我认为CAS操作是实现自旋锁的一部分.
  • 在锁的基础上,未进入临界区的线程应该处于一种怎样的状态,有两种情况,一是阻塞自身线程,等待释放锁,这明显就是互斥锁,第二种是不阻塞自身,而是在一个循环中等待释放锁,而且一直访问查看是否释放锁.这就是自旋锁.
    • 自旋锁的好处是避免了多余的线程上下文切换,执行速度快,缺点也和这个有关系,如果一个线程持有锁太久,导致其他锁一直循环等待,浪费了CPU.
  • Java中CAS操作,也就是compare and swap,即比较,如果满足期待,就完成设置或交换.具体的方法就是Unsafe包下面的一系列compareAndSwap方法,也就是各种原子类也提供了conpareAndSet方法.有一点需要注意,CAS其实是无锁的,它只是抱着一种乐观的态度,认为自己可以完成任务,但是多个线程同时对一个变量进行CAS操作,只有一个操作可以完成.未完成的线程不会挂起,只是收到一个通知,告知失败.说它是无锁,是指硬件层面是无锁的,CAS操作只是一个的指令.
  • ABA问题: 是指在一个线程进行CAS修改初始值为A的变量,这个操作的目的是将A换成B,如果最后成功换成了B,那能代表程序的执行就是正确的吗?是不行的,如果在获取A这个状态后,有其他线程将A换成B,又换成A,然后原来的线程继续进行,将A换成B.如果只是字面量还可以,但是如果这里的A是一种状态呢,就可能有别的问题.
    • 解决ABA问题,只要规定状态变换的方向不可逆就可以了,或者给每一个状态添加一个时间戳等等.
  • CAS和自旋锁,上面并没有说清楚,我说CAS是实现自旋锁的一部分,也说了自旋锁的定义,但怎么个实现法.其实很简单,我们将CAS操作放在一个循环条件中,将这个循环分别作为加锁和释放锁的操作逻辑.这里的锁只是逻辑上的,硬件层面没有锁.

AQS

  • AQS是抽象同步队列AbstractQueuedSynchronizer的简称,是实现同步机制的基础,并发包中的各种所谓的锁就是通过AQS实现的.
  • LockSupport
    • 顾名思义,锁支持,是rt.jar包中的工具类,提供硬件层次的挂起和唤醒锁
    • 主要两个静态方法:
      • LockSupport关联的许可证:根据名字看做一个黑盒的许可证
      • pack():如果当前线程获取到许可证,调用pack方法后会马上返回,结束方法,如果没有获取到许可证,就会挂起处于阻塞状态,线程默认情况下没有许可证.
      • unpack(Thread thread):获取许可证.有几种情况:
        • 先执行unpack,再执行pack,pack会直接返回,结束方法.
        • 先执行pack,线程挂起,再执行unpack,线程被唤醒.
    • 还有一些带时限的方法,就不详细说了.毕竟是底层的东西,知道大意就可以了,一般用不上.
  • AQS给我的感觉像是一个"接口"(实际是一个抽象类),又像是一个 底层工具类.AQS中最重要的是其维持的一个单一的状态变量state(int),不同类型的锁机制就是通过对state的不同的定义来实现的.例如,可重入锁ReentrantLock,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示获取到的读锁的线程的可重入次数,低16位是写锁.对于smaphore来说,state用来表示当前可用信号的个数;对于CountDownlatch来说,state用来表示计数器当前的值.
  • AQS根据对于state的操作方式分为独占方式和共享方式:
    • 独占方式:
      • 获取资源void acquire(int arg)void acquireInterruptibly(int arg)
      • 释放资源boolean release(int arg)
    • 共享方式:
      • 获取资源void acquireShared(int arg)void acquireSharedInterruptibly(int arg)
      • 释放资源boolean releaseShared(int arg)
    • 光看这几个方法没什么用,但是暂时先记住名字,解释一下获取资源中的两个方法(即有无Interruptibly)的区别:有Interruptibly意味着要对中断进行响应,也就是说如果获取资源失败后进入挂起状态时,有其他线程中断该线程,该线程会抛出InterruptedException而中断,而没有Interruptibly的意味着忽略中断,不理睬其他线程的中断请求.
  • 获取资源和释放资源的过程:
    • 独占
      • 当一个线程调用acquire方法时,会使用tryAcquire方法尝试获取资源(可以暂时将这个方法视为一个黑盒),后续具体的操作就是设置状态变量的值,成功则直接返回,失败则将当前线程封装成AQS的一个内部类Node插入到AQS的阻塞队列尾部,并调用LockSupport.pack方法挂起自己.
      • 当一个线程调用release方法时,会使用tryRelease方法释放资源,还是设置状态变量state的值,然后调用LockSupport.unpack方法激活AQS队列中的被阻塞的一个线程,被激活的线程使用tryAcquire尝试,看当前的state是否满足要求,满足则成功激活,失败则重进入队列挂起.
      • 我上面说,将tryAcquire和tryRelease视为一个黑盒,是因为AQS并没有实现这两个方法,而是交由具体的子类自行实现.具体的操作也说了,就是通过CAS设置状态变量的值,来控制同步.
    • 共享
      • 类似独占,不同的是,获取资源失败的线程被封装称为Node.SHARED类型的Node节点插入队列,而独占中是封装称为Node.EXCLUSIVE类型的节点.
  • 插入队列的方法不详说,和一般的链表插入有一点不同.
  • 条件变量
    • 条件变量ConditionObject也是AQS的内部类.有什么用呢?其实就是类似wait和notify,不过在AQS中称为await和signal.就是控制同步,你在基础编程中如何使用wait和notify,AQS实现类中就怎么使用await和signal.类似的,wait和notify一定要在synchronized代码块或方法中使用,await和signal也必须在实现类的lock和unlock中间使用.
    • 内部实现
      • 过程是这样的:当前线程调用条件变量的await时(当前线程一定调用过lock方法,内部会构造一个类型为Node.CONDITION类型的node节点,然后插入到队列末尾(这个队列不是AQS的阻塞队列,是条件变量这个内部类中的一个条件队列),然后释放获得的锁,这时如果有其他线程使用lock获取锁,会获得锁.这时如果有另一个线程调用的了条件变量的signal方法,在内部会把条件队列中队头的一个节点从条件队列里面移到AQS的阻塞队列中,等待时机获取锁.
    • 一个AQS可以有多个条件变量,通过newCondition方法获取条件变量实例,这个方法也需要子类实现.

猜你喜欢

转载自blog.csdn.net/qq_36865108/article/details/86679822