美文网首页
深入分析大厂面试题三

深入分析大厂面试题三

作者: Java及SpringBoot | 来源:发表于2021-01-25 17:26 被阅读0次

    1 AbstractQueuedSynchronizer之AQS

    1.1 先从字节跳动及其它大厂面试题说起

    • 从集合开始吧,介绍一下常用的集合类,哪些是有序的,哪些是无序的
    • hashmap是如何寻址的,哈希碰撞后是如何存储数据的,1.8后什么时候变成红黑树、说下红黑树原理有什么好处?
    • reentrantlock实现原理,简单说下AQS
    • synchronized实现原理,monitor对象是什么时候生成的?知道monitor和monitorenter和monitorexit这2个是怎么保证同步的吗,或者说底层是如何执行的
    • 偏向锁和轻量级锁有什么区别?

    1.2 前置知识

    • 公平锁和非公平锁
    • 可重入锁
    • LockSupport
    • 自旋锁
    • 数据结构之链表
    • 设计模式之模板设计模式

    1.3 抽象的队列同步器

    image-20210125150805484.png
    AbstractOwnableSynchronizer
    AbstractQueuedLongSynchronizer
    AbstractQueuedSynchronizer                                      
    通常地: AbstractQueuedSynchronizer简称为AQS
    

    是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量 表示持有锁的状态

    image-20210121174820152.png

    1.4 AQS为什么是JUC内容中最重要的基石

    和AQS有关的

    image-20210121174937297.png

    ReentrantLock

    image-20210125151522221.png

    CountDownLatch

    image-20210125151625649.png

    ReentrantReadWriteLock

    image-20210125152612032.png

    Semaphore

    image-20210125152651961.png

    进一步理解锁和同步器的关系

    • 锁,面向锁的使用者
      • 定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
    • 同步器,面向锁的实现者
      • 比如Java并发大神Douglee,提出统一规 范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

    能干嘛

    • 加锁会导致阻塞
      • 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
    抢到资源的线程直接使用办理业务,抢占不到资源的线程的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
     
    既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
     
    如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。
    

    1.5 AQS初步

    1.5.1 AQS初识

    image-20210122124943048.png

    有阻塞就需要排队,实现排队必然需要队列

    AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

    image-20210125162753991.png

    1.5.2 AQS内部体系架构

    image-20210122125108960.png

    AQS自身

    • AQS的int变量

      • AQS的同步状态State成员变量
      /**
       * The synchronization state.
       */
      private volatile int;
      
      //银行办理业务的受理窗口状态
      //零就是没人,自由状态可以办理
      //大于等于1,有人占用窗口,等着
      
    • AQS的CLH队列

      • CLH队列(三个大牛的名字组成),为一个双向队列
    • state变量+CLH双端Node队列

    image-20210122132651197.png

    内部类Node(Node类在AQS类内部)

    • Node的int变量
      • Node的等待状态waitState成员变量 volatile int waitStatus
      • 等候区其它顾客(其它线程)的等待状态
      • 队列中每个排队的个体就是一个Node.
    image-20210125163846064.png image-20210122132912772.png

    1.5.3 AQS同步队列的基本结构

    是用LockSupport.pork()来进行排队的

    image-20210122133036764.png

    1.6 AQS详解(ReentrantLock开始解读AQS)

    1.6.1 Code

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class AQSDemo {
        public static void main(String[] args) {
            ReentrantLock lock = new ReentrantLock();
            //带入一个银行办理业务的案例来模拟我们的AQS如何进行线程的管理和通知唤醒机制
            //3个线程模拟3个来银行网点,受理窗口办理业务的顾客
            //A顾客就是第一个顾客,此时受理窗口没有任何人,A可以直接去办理
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("-----A thread come in");
                    try {
                        TimeUnit.MINUTES.sleep(20);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                } finally {
                    lock.unlock();
                }
            }, "A").start();
    
            //第二个顾客,第二个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时B只能等待,
            //进入候客区
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("-----B thread come in");
                } finally {
                    lock.unlock();
                }
            }, "B").start();
    
            //第三个顾客,第三个线程---》由于受理业务的窗口只有一个(只能一个线程持有锁),此时C只能等待,
            //进入候客区
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("-----C thread come in");
                } finally {
                    lock.unlock();
                }
            }, "C").start();
        }
    }
    

    Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的

    1.6.2 ReentrantLock原理

    image-20210122133555270.png

    1.6.3 lock方法的公平和非公平

    通过ReentrantLock的源码来讲解公平锁和非公平锁

    image-20210125164307411.png

    默认创建的是非公平锁

    image-20210122133705885.png
    可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
    hasQueuedPredecessors()
    hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法
    
    public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    
    对比公平锁和非公平锁的tryAcqure()方法的实现代码, 其实差别就在于非公平锁获取锁时比公平锁中少了一个判断!hasQueuedPredecessors()
        
    hasQueuedPredecessors()中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
     
    公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中;
     
    非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(), 之后还是需要竞争锁(存在线程竞争的情况下)
    
    image-20210122134046648.png
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    

    1.6.4 AQS源码深度分析

    lock()

    image-20210125164849255.png

    acquire()

    image-20210122134242173.png image-20210122134252774.png

    tryAcquire(arg) 非公平锁

    image-20210122134347797.png image-20210122134418124.png

    继续推进条件,走下一步方法addWaiter,否则结束。

    addWaiter(Node.EXCLUSIVE)

    image-20210122134519595.png image-20210122134534059.png

    双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。 真正的第一个有数据的节点,是从第二个节点开始的。

    假如3号ThreadC线程进来,prev-compareAndSetTail-next

    acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

    image-20210122134705598.png

    假如再抢抢失败就会进入shouldParkAterFailedAcquire和parkAndCheckInterrupt方法中

    image-20210122134736962.png

    shouldParkAfterFailedAcquire

    image-20210122134756725.png image-20210122134802529.png

    如果前驱节点的waitstatus是SIGNAL状态(-1),即shouldParkAfterFailedAcquire方法会返回true 程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起

    parkAndCheckInterrupt

    image-20210122134829449.png

    方法unlock()

    sync.release(1);
    tryRelease(arg)
    unparkSuccessor
    
    image-20210122140202420.png

    1.6.5 AQS面试考点

    你应该看过源码了,那么AQS里面有个变量叫State,它的值有几种?
    3个状态:没占用是0,占用了是1,大于1是可重入锁
    
    如果AB两个线程进来了以后,请问这个总共有多少个Node节点?
    答案是3个
    
    image-20210122142648284.png

    相关文章

      网友评论

          本文标题:深入分析大厂面试题三

          本文链接:https://www.haomeiwen.com/subject/suxizktx.html