高并发面试题

1.高并发的三要素

原子性、 有序性、可见性

2. 形成死锁的必要条件

互斥条件、 请求和保持、 不可剥夺、 循环等待

3. 线程状态,BLOCKED 和 WAITING 有什么区别

BLOCKED 是锁竞争失败后被被动触发的状态,WAITING 是人为的主动触发的状态
可以使用Object.wait()、Object.join()、LockSupport.park() 这些方法 使得线程进入到 WAITING 状态,在这个状态下,必须要等待特定的方法来唤醒, 比如 Object.notify 方法可以唤醒 Object.wait()方法阻塞的线程 LockSupport.unpark()可以唤醒 LockSupport.park()方法阻塞的线程。

4. sleep()和wait() 方法的区别

都可以用来暂停线程的执行
API不同,sleep是Thread的静态方法,不会释放锁, wait()方法是对象的方法,会释放锁
阻塞结束了以后,sleep会继续执行,wait被唤醒了以后只是进入了runnable

5. 解释Happens-Before 模型

在看极客时间 Java 性能调优实战 的时候才彻底明白这个问题 前一个操作的结果可以被后续的操作获取。这条规则规范了编译器对程序的重排序优化。 一句话就明白了所有,这个就是Happens-Before 模型 单例模式双重检验锁就是利用了其中的volatile
Happens-Before 是一种可见性模型,也就是说,在多线程环境下。 原本因为指令重排序的存在会导致数据的可见性问题,也就是 A 线程修改某个共享变量 对 B 线程不可见。因此,JMM 通过 Happens-Before 关系向开发人员提供跨越线程的内存可见性保证。 如果一个操作的执行结果对另外一个操作可见,那么这两个操作之间必然存在 Happens-Before 管理。 其次,Happens-Before 关系只是描述结果的可见性,并不表示指令执行的先后顺序, 也就是说 只要不对结果产生影响,仍然允许指令的重排序。
程序顺序规则, as-if-serial 也就是不管怎么重排序,单线程的程序的执行结果不能改变
传递性规则,也就是 A Happens-Before B,B Happens-Before C。 就可以推导出 A Happens-Before C。
volatile 变量规则,对一个 volatile 修饰的变量的写一定 happens-before 于任意 后续对这个 volatile 变量的读操作
监视器锁规则,一个线程对于一个锁的释放锁操作,一定 happens-before 与后续线程对这个锁的加锁操作
线程启动规则,如果线程 A 执行操作 ThreadB.start(),那么线程 A 的 ThreadB.start()之前的操作 happens-before 线程 B 中的任意操作。
join 规则,如果线程 A 执行操作 ThreadB.join()并成功返回, 那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功的返回。

6 stop()方法和suspend()方法为啥不推荐使用

stop()剩余的代码得不到执行,不能优雅的stop

suspend 会导致死锁,导致持有的锁得不到释放,

7 CAS会有哪些问题

ABA问题,用AtomicStamp的Reference解决

自旋时间过长

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

8 CopyOnWriteArrayList的底层原理

在进行写操作的时候会创建一个底层数组的副本,并在副本上进行操作,而不是在原始数组上直接修改 写完成后,把原数组的引用指向新数组,读的操作还在原数组中执行,读取不需要加锁 适用于读多写少的场景 是fail-safe的

9 线程池submit和execute()有什么区别

execute接受参数只能执行runnable类型的任务,submit只能执行runnable和callable submit会返回有计算结果的future对象,execute返回void submit需要处理Exception

10 MESI 协议

保持缓存的一致性, modify修改、exclusive独占、share共享、invalid失效, 4种状态能够两两转换,形成16种状态,其实就是一个状态机,
CPU A通过总线地址冲突检测到CPUB有这个值,于是向B请求这个值,这个时候值N变成共享状态 当CPUA要修改n的时候会通过总线发出失效命令,让CPUB的高速缓存对应的n的状态变成失效,CPUA变成独占状态, 等待总线发出失效再确认ACK,严重影响了CPU的利用率,所以引入了store buffer,有了它以后cpu可以直接修改共享状态的变量修改后直接扔到store buffer,由store buffer来等待其他cpu的ack确认,收到确认以后才将变量值写入本地缓存,这样CPU就可以继续去做其他事了
store buffer的容量很小, 加入store buffer 和失效队列打破了一致性,希望在需要一致性的地方加入内存屏障,屏蔽的是store buffer和invalid queue 我们在写操作后边加入写屏障,cpu就必须等待存储队列的所有写操作都刷到对面的失效队列 在读操作之前加入读屏障,必须把失效队列的值都处理完,再去读变量,一写一读组合起来就达到了一致的效果

可以参考这个:www.bilibili.com/video/BV1cT...

11 synchronized原理

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和 共享锁。 排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该共享资 源, 也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 重 入锁实现就是用到了 AQS 中的排它锁功能。 共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。

12 AQS学习理解

AbstractQueuedSynchronized 为什么采用双向链表

双向链表的优势

双向链表提供了双向指针,可以在任何一个节点方便向前或向后进行遍历,这种对于有反向遍历需求的场景来说非常有用。
双向链表可以在任意节点位置实现数据的插入和删除,并且这些操作的时间复杂度都是 O(1), 不受链表长度的影响。这对于需要频繁对链表进行增删操作的场景非常有用。

AQS 采用双向链表的原因

存储在双向链表中的线程,有可能这个线程出现异常不再需要竞争锁,所以需要把这些异常节点从链表中删除,而删除操作需要找到这个节点的前驱结点,如果不采用双向链表,就必须要从头节点开始遍历,时间复杂度就变成了 O(n)。
新加入到链表中的线程,在进入到阻塞状态之前,需要判断前驱节点的状态,只有前驱节点是 Sign 状态的时候才会让当前线程阻塞,所以这里也会涉及到前驱节点 的查找,采用双向链表能够更好的提升查找效率

线程在加入到链表中后,会通过自旋的方式去尝试竞争锁来提升性能,在自旋竞争 锁的时候为了保证锁竞争的公平性,需要先判断当前线程所在节点的前驱节点是否是头节点。这个判断也需要获取当前节点的前驱节点,同样采用双向链表能提高查 找效率。

state :锁标识 exclusiveOwnerThread:存储当前持有锁的线程 CLH: 双向队列 waitStatus: AQS内部使用的状态位,

为什么等待队列中第一个节点是哑节点

释放锁的线程在唤醒等待线程的时候,是通过前一个线程的waitStatus字段来判断后续节点是否可以唤醒,如果没有dummy节点,队列的第一个节点就没有前置节点了

AQS获取锁和释放锁的流程

CASState(0, 1),更新成功就获取到锁, exclusiveOwnerThread设置为当前线程 state不为0有两种情况,根据exclusiveOwnerThread判断是否锁重入,是的话state加1,不是的话进入CLH

入队的逻辑

入队之前会检查head节点,为空说明CLH还没有初始化,去初始化CLH,将自己的prev指向tail节点,通过CAS将tail更新为自己,新入队的节点会再次检查一下自己是不是队列中的第一个可用节点,如果是会尝试获取锁,防止入队的时候持有锁的线程已经把锁释放了,入队成功后会把前置节点的waitStatus设置为SIGNAL

持有锁的线程释放锁的操作

检查state是否大于1,大于1是锁重入直接减返回成功,等于1设置exclusiveOwnerThread为空,state设置为0,从后找到第一个signal的节点

锁等待队列什么时候初始化

new reentrantLock的时候不会初始化,只有发生第一次锁竞争,第一个线程要入队的时候,才会初始化队列

13 常用的并发工具类

CountDownLatch,门栓

CyclicBarrier (回环栅栏)

Semaphore (信号量)

14 select、poll、epoll之间的区别

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。select的时间复杂度O(n)。它仅仅知道有I/O事件发生了,却并不知道是哪那几个流,只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的时间复杂度,同时处理的流越多,轮询时间就越长。poll的时间复杂度O(n)。poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.epoll的时间复杂度O(1)。epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动的。

3.参考文档

juejin.cn/post/685457...

相关推荐
快手技术几秒前
快手Klear-Reasoner登顶8B模型榜首,GPPO算法双效强化稳定性与探索能力!
后端
二闹10 分钟前
三个注解,到底该用哪一个?别再傻傻分不清了!
后端
用户490558160812521 分钟前
当控制面更新一条 ACL 规则时,如何更新给数据面
后端
林太白23 分钟前
Nuxt.js搭建一个官网如何简单
前端·javascript·后端
码事漫谈25 分钟前
VS Code 终端完全指南
后端
该用户已不存在1 小时前
OpenJDK、Temurin、GraalVM...到底该装哪个?
java·后端
怀刃1 小时前
内存监控对应解决方案
后端
码事漫谈1 小时前
VS Code Copilot 内联聊天与提示词技巧指南
后端
Moonbit2 小时前
MoonBit Perals Vol.06: MoonBit 与 LLVM 共舞 (上):编译前端实现
后端·算法·编程语言
Moonbit2 小时前
MoonBit Perals Vol.06: MoonBit 与 LLVM 共舞(下):llvm IR 代码生成
后端·程序员·代码规范