深入分析大厂面试题三

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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容