面试官今天我们来聊聊 lock 锁吧?
    候选者:嗯嗯嗯,没问题
    面试官:先问点简单的吧,刚睡醒,还是有点困的。
    候选者:刚睡醒来面我干嘛?你就这态度?
    面试官:哈?你刚说了什么?
    候选者:没事,我没说话…
    面试官你知道什么叫做公平和非公平锁吗
    候选者:公平锁指的就是:在竞争环境下,先到临界区的线程比后到的线程一定更快地获取得到锁
    候选者:那非公平就很好理解了:先到临界区的线程未必比后到的线程更快地获取得到锁
    面试官如果让你实现的话,你怎么实现公平和非公平锁?
    候选者:公平锁可以把竞争的线程放在一个先进先出的队列上
    候选者:只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁就好了
    候选者:非公平锁的概念上面已经提到了:后到的线程可能比前到临界区的线程获取得到锁
    候选者:那实现也很简单,线程先尝试能不能获取得到锁,如果获取得到锁了就执行同步代码了
    候选者:如果获取不到锁,那就再把这个线程放到队列呗
    候选者:所以公平和非公平的区别就是:线程执行同步代码块时,是否会去尝试获取锁。
    候选者:如果会尝试获取锁,那就是非公平的。如果不会尝试获取锁,直接进队列,再等待唤醒,那就是公平的。
    AQS 和 ReentrantLock - 图1
    面试官为什么要进队列呢?线程一直尝试获取锁不就行了么?
    候选者:一直尝试获取锁,专业点就叫做自旋,需要耗费资源的。
    候选者:多个线程一直在自旋,而且大多数都是竞争失败的,哪有人会这样实现的
    候选者:不会吧,不会吧,你不会就是这样实现的吧
    面试官:我就问问…
    面试官那上次面试所问的 synchronized 锁是公平的还是非公平的?
    候选者:非公平的。
    候选者:偏向锁很好理解,如果当前线程 ID 与 markword 存储的不相等,则 CAS 尝试更换线程 ID,CAS 成功就获取得到锁了
    候选者:CAS 失败则升级为轻量级锁
    候选者:轻量级锁实际上也是通过 CAS 来抢占锁资源(只不过多了拷贝 Mark Word 到 Lock Record 的过程)
    候选者:抢占成功到锁就归属给该线程了,但自旋失败一定次数后升级重量级锁
    候选者:重量级锁通过 monitor 对象中的队列存储线程,但线程进入队列前,还是会先尝试获取得到锁,如果能获取不到才进入线程等待队列中
    候选者:综上所述,synchronized 无论处理哪种锁,都是先尝试获取,获取不到才升级 || 放到队列上的,所以是非公平的
    AQS 和 ReentrantLock - 图2
    面试官:嗯,讲得挺仔细的。AQS 你了解吗?
    候选者:嗯嗯,AQS 全称叫做 AbstractQueuedSynchronizer
    候选者:是可以给我们实现锁的一个「框架」,内部实现的关键就是维护了一个先进先出的队列以及 state 状态变量
    候选者:先进先出队列存储的载体叫做 Node 节点,该节点标识着当前的状态值、是独占还是共享模式以及它的前驱和后继节点等等信息
    候选者:简单理解就是:AQS 定义了模板,具体实现由各个子类完成。
    候选者:总体的流程可以总结为:会把需要等待的线程以 Node 的形式放到这个先进先出的队列上,state 变量则表示为当前锁的状态。
    候选者:像 ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore 这些常用的实现类都是基于 AQS 实现的
    候选者:AQS 支持两种模式:独占(锁只会被一个线程独占)和共享(多个线程可同时执行)
    AQS 和 ReentrantLock - 图3
    面试官你以 ReentrantLock 来讲讲加锁和解锁的过程呗
    候选者:以非公平锁为了,我们在外界调用 lock 方法的时候,源码是这样实现的
    候选者:1):CAS 尝试获取锁,获取成功则可以执行同步代码
    候选者:2):CAS 获取失败,则调用 acquire 方法,acquire 方法实际上就是 AQS 的模板方法
    候选者:3):acquire 首先会调用子类的 tryAcquire 方法(又回到了 ReentrantLock 中)
    候选者:4):tryAcquire 方法实际上会判断当前的 state 是否等于 0,等于 0 说明没有线程持有锁,则又尝试 CAS 直接获取锁
    候选者:5): 如果 CAS 获取成功,则可以执行同步代码
    候选者:6): 如果 CAS 获取失败,那判断当前线程是否就持有锁,如果是持有的锁,那更新 state 的值,获取得到锁(这里其实就是处理可重入的逻辑)
    候选者:7):CAS 失败 && 非重入的情况,则回到 tryAcquire 方法执行「入队列」的操作
    候选者:8): 将节点入队列之后,会判断「前驱节点」是不是头节点,如果是头结点又会用 CAS 尝试获取锁
    候选者:9): 如果是「前驱节点」是头节点并获取得到锁,则把当前节点设置为头结点,并且将前驱节点置空(实际上就是原有的头节点已经释放锁了)
    候选者:10): 没获取得到锁,则判断前驱节点的状态是否为 SIGNAL,如果不是,则找到合法的前驱节点,并使用 CAS 将状态设置为 SIGNAL
    候选者:11): 最后调用 park 将当前线程挂起
    面试官:你说了一大堆,麻烦使用压缩算法压缩下加锁的过程。
    候选者:压缩后:当线程 CAS 获取锁失败,将当前线程入队列,把前驱节点状态设置为 SIGNAL 状态,并将自己挂起。
    AQS 和 ReentrantLock - 图4
    面试官为什么要设置前驱节点为 SIGNAL 状态,有啥用?
    候选者:其实就是表示后继节点需要被唤醒
    候选者:我先把解锁的过程说下吧
    候选者:1): 外界调用 unlock 方法时,实际上会调用 AQS 的 release 方法,而 release 方法会调用子类 tryRelease 方法(又回到了 ReentrantLock 中)
    候选者:2):tryRelease 会把 state 一直减(锁重入可使 state>1),直至到 0,当前线程说明已经把锁释放了
    候选者:3): 随后从队尾往前找节点状态需要 < 0,并离头节点最近的节点进行唤醒
    候选者:唤醒之后,被唤醒的线程则尝试使用 CAS 获取锁,假设获取锁得到则把头节点给干掉,把自己设置为头节点
    候选者:解锁的逻辑非常简单哈,把 state 置 0,唤醒头结点下一个合法的节点,被唤醒的节点线程自然就会去获取锁
    面试官:嗯,了解了。
    候选者:回到上一个问题,为什么要设置前驱节点为 SIGNAL 状态
    候选者:其实归终结底就是为了判断节点的状态,去做些处理。
    候选者:Node 中节点的状态有 4 种,分别是:CANCELLED (1)、SIGNAL (-1)、CONDITION (-2)、PROPAGATE (-3) 和 0
    候选者:在 ReentrantLock 解锁的时候,会判断节点的状态是否小于 0,小于等于 0 才说明需要被唤醒
    候选者:另外一提的是:公平锁的实现与非公平锁是很像的,只不过在获取锁时不会直接尝试使用 CAS 来获取锁。
    候选者:只有当队列没节点并且 state 为 0 时才会去获取锁,不然都会把当前线程放到队列中
    面试官:最后画个流程图吧,你画好了,他们会给你点赞和转发的
    候选者:真的假的?
    AQS 和 ReentrantLock - 图5
    AQS 和 ReentrantLock - 图6