高并发面试题

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...

相关推荐
海绵波波1071 小时前
flask后端开发(1):第一个Flask项目
后端·python·flask
AI人H哥会Java3 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
凡人的AI工具箱3 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django
奔跑草-3 小时前
【数据库】SQL应该如何针对数据倾斜问题进行优化
数据库·后端·sql·ubuntu
中國移动丶移不动4 小时前
Java 并发编程:原子类(Atomic Classes)核心技术的深度解析
java·后端
Q_19284999065 小时前
基于Spring Boot的旅游推荐系统
spring boot·后端·旅游
愤怒的代码5 小时前
Spring Boot对访问密钥加密解密——RSA
java·spring boot·后端
美美的海顿5 小时前
springboot基于Java的校园导航微信小程序的设计与实现
java·数据库·spring boot·后端·spring·微信小程序·毕业设计
愤怒的代码5 小时前
Spring Boot中幂等性的应用
java·spring boot·后端
xiaocaibao7776 小时前
编程语言的软件工程
开发语言·后端·golang