(高频问题)21-40 计算机 Java后端 实习 and 秋招 面试高频问题汇总

专栏简介

21.怎么判断用户是否登录、如果有多台服务器,怎么判断一个请求是否已经登录

怎么判断用户是否登录:一种常见的方法是使用cookie和session机制,原理就是用户登录的时候在服务器端创建一个session,登录成功后,把用户信息放在session里,接下里,服务器会把sessionID返回给用户的浏览器,浏览器接收到这个cookie后,用户再访问网站的URL地址时,浏览器会顺带着把这个网站下的cookies全部发送给服务器,服务器检查cookies里有木有sessionID,如果有,会根据sessionID找到session,然后再判断session里有木有用户信息,有则用户已登录,反之就是没登录³。

如果有多台服务器,怎么判断一个请求是否已经登录:这个问题涉及到分布式系统中的会话管理问题,一种常用的解决方案是使用单点登录(Single Sign On, SSO),即用户只需在一个系统中进行登录认证,就可以访问其他系统而无需再次登录。SSO的实现方式有多种,例如使用OAuth协议、使用Redis存储共享session、使用JWT(JSON Web Token)等¹²。

22.synchronize原理,自旋锁等⼀系列锁了解吗

synchronize原理:synchronize是Java中的一个关键字,用于实现线程同步,保证多个线程对共享资源的操作是互斥的。synchronize的原理是基于对象内部的一个监视器锁(monitor)来实现的,每个对象都有一个monitor,当一个线程进入synchronize修饰的方法或代码块时,就会获取该对象的monitor,其他线程就无法进入该方法或代码块,直到持有monitor的线程退出并释放monitor。synchronize可以保证原子性、可见性和有序性¹²³。

自旋锁等一系列锁了解吗:自旋锁是一种锁的优化策略,它的思想是当一个线程尝试获取锁时,如果锁已经被占用,就不会立即阻塞,而是在循环中不断尝试获取锁,直到成功或超时。自旋锁适用于锁占用时间较短、线程数不多的场景,可以避免线程切换带来的开销。但是如果锁占用时间较长或者线程数较多,自旋锁会消耗CPU资源,降低性能。自旋锁有多种实现方式,例如CAS(Compare And Swap)操作、Ticket Lock、CLH Lock等⁴⁵。除了自旋锁之外,还有其他类型的锁,例如悲观锁、乐观锁、公平锁、非公平锁、可重入锁、独占锁、共享锁,偏向锁,轻量级锁等²⁴。

偏向锁是Java虚拟机对synchronized关键字的一种优化策略,它利用对象头中的标志位来记录第一个获取该对象锁的线程ID,如果该线程再次请求该对象锁,就不需要进行CAS操作来获取锁,而是直接进入同步代码块。如果有其他线程尝试获取该对象锁,就会触发偏向锁的撤销,将偏向锁升级为轻量级锁或重量级锁 。

  • 轻量级锁需要做CAS吗:是的。轻量级锁是一种利用CAS操作来实现的锁,它的目的是在没有多线程竞争的情况下,减少重量级锁的开销。轻量级锁的加锁过程是通过CAS操作将对象头中的Mark Word替换为指向当前线程栈中的锁记录(Lock Record)的指针,如果替换成功,则表示获取锁成功,否则表示有其他线程竞争锁,需要升级为重量级锁¹²³。
  • CAS操作是什么:CAS(Compare And Swap)操作是一种原子性的比较并替换操作,它需要三个参数:内存地址V、旧值A、新值B。CAS操作会比较内存地址V中的值是否等于旧值A,如果相等,则用新值B替换旧值A,并返回true,否则不做任何操作,并返回false。CAS操作可以保证多线程对共享变量的更新不会发生冲突⁴⁵。

23.java中 G1、CMS是什么

Java中G1、CMS是什么:G1和CMS都是Java虚拟机中的垃圾收集器,它们的目的是回收堆内存中不再使用的对象,以释放空间给新的对象。G1和CMS有一些共同点,比如都支持并发和分代收集,都关注降低垃圾回收的停顿时间,都适用于服务端应用。但它们也有一些区别,比如:

  1. G1是基于Region(区域)的垃圾收集器,它将堆内存划分为多个大小相等的独立区域,每个区域可以属于新生代或老年代,这样可以避免在整个堆范围内进行回收。CMS是基于分代模型的垃圾收集器,它将堆内存划分为新生代和老年代,每个代可以由多个不连续的空间组成¹²。
  2. G1是基于标记-整理算法的垃圾收集器,它在回收时不会产生内存碎片,而且可以根据用户指定的停顿时间来制定回收计划,从而达到可预测的停顿时间。CMS是基于标记-清除算法的垃圾收集器,它在回收时会产生内存碎片,而且无法处理浮动垃圾(并发清理阶段产生的垃圾),可能导致Concurrent Mode Failure(并发模式失败)¹²³。
  3. G1是Java 9之后的默认垃圾收集器,它是CMS的长期替代方案,也是未来发展的方向。CMS已经在Java 14中被废弃⁴ 。
  4. CMS是把堆空间划分为新生代和老年代,每个代可以由多个不连续的空间组成,但是在回收时,CMS会对整个新生代或老年代进行扫描,而不是只针对某个空间。G1则是把堆空间划分为多个大小相等的独立区域,每个区域可以属于新生代或老年代,这样在回收时,G1可以根据区域的回收价值和成本来选择性地回收部分区域,而不是对整个堆进行扫描。这样可以减少垃圾回收的停顿时间和开销。

24.rabbit内部存储结构是什么

RabbitMQ存储层包含两个部分:队列索引和消息存储。

RabbitMQ消息有两种类型:持久化消息和非持久化消息,这两种消息都会被写入磁盘。

持久化消息在到达队列时写入磁盘,同时会内存中保存一份备份,当内存吃紧时,消息从内存中清除。非持久化消息一般只存于内存中,当内存吃紧时会被换入磁盘,以节省内存空间。

RabbitMQ中的持久化消息和非持久化消息是由用户声明的¹²。持久化消息会被保存在磁盘中,不会因为RabbitMQ服务重启或崩溃而丢失²³。非持久化消息只会在内存中存储,当内存不足时可能会被写入磁盘,但是重启后就会消失²。持久化消息的性能要低于非持久化消息,因为需要写入磁盘²。

持久化队列和非持久化队列的区别是,持久化队列会被保存在磁盘中,固定并持久的存储,当Rabbit服务重启后,该队列会保持原来的状态在RabbitMQ中被管理¹³⁴,而非持久化队列不会被保存在磁盘中,Rabbit服务重启后队列就会消失¹³⁴。非持久化队列的性能要高于持久化队列,因为不需要写入磁盘¹。持久化队列的优点是会一直存在,不会随服务的重启或服务器的宕机而消失¹。

持久化队列和非持久化队列中保存的消息可以是持久化消息也可以是非持久化消息¹。持久化消息会同时写入磁盘和内存,加快读取速度¹。非持久化消息一般只保存在内存中,在内存吃紧的时候会被写入到磁盘中,以节省内存空间¹。这两种类型的消息的落盘处理都在RabbitMQ的"持久层"中完成¹。

非持久化队列中的消息,如果用户指定了持久化消息,仍然要保存到磁盘中的。但是,如果RabbitMQ服务重启或崩溃,非持久化队列本身会消失,所以保存在磁盘中的消息也就无法被消费了。

队列索引:rabbit_queue_index(下文简称index)

index维护队列的落盘消息的信息,如存储地点、是否已被交付给消费者、是否已被消费者ack等。每个队列都有相对应的index。

index使用顺序的段文件来存储,后缀为.idx,文件名从0开始累加,每个段文件中包含固定的segment_entry_count条记录,默认值是16384。每个index从磁盘中读取消息的时候,至少要在内存中维护一个段文件,所以设置queue_index_embed_msgs_below值得时候要格外谨慎,一点点增大也可能会引起内存爆炸式增长。

消息存储:rabbit_msg_store(下文简称store)

store以键值的形式存储消息,所有队列共享同一个store,每个节点有且只有一个。 从技术层面上说,store还可分为msg_store_persistent和msg_store_transient,前者负责持久化消息的持久化,重启后消息不会丢失;后者负责非持久化消息的持久化,重启后消息会丢失。通常情况下,两者习惯性的被当作一个整体。

store使用文件来存储,后缀为.rdq,经过store处理的所有消息都会以追加的方式写入到该文件中,当该文件的大小超过指定的限制(file_size_limit)后,将会关闭该文件并创建一个新的文件以供新的消息写入。文件名从0开始进行累加。在进行消息的存储时,RabbitMQ会在ETS(Erlang Term Storage)表中记录消息在文件中的位置映射和文件的相关信息。

消息(包括消息头、消息体、属性)可以直接存储在index中,也可以存储在store中。最佳的方式是较小的消息存在index中,而较大的消息存在store中。这个消息大小的界定可以通过queue_index_embed_msgs_below来配置,默认值为4096B。当一个消息小于设定的大小阈值时,就可以存储在index中,这样性能上可以得到优化(可理解为数据库的覆盖索引和回表)。 读取消息时,先根据消息的ID(msg_id)找到对应存储的文件,如果文件存在并且未被锁住,则直接打开文件,从指定位置读取消息内容。如果文件不存在或者被锁住了,则发送请求由store进行处理。

删除消息时,只是从ETS表删除指定消息的相关信息,同时更新消息对应的存储文件和相关信息。在执行消息删除操作时,并不立即对文件中的消息进行删除,也就是说消息依然在文件中,仅仅是标记为垃圾数据而已。当一个文件中都是垃圾数据时可以将这个文件删除。当检测到前后两个文件中的有效数据可以合并成一个文件,并且所有的垃圾数据的大小和所有文件(至少有3个文件存在的情况下)的数据大小的比值超过设置的阈值garbage_fraction(默认值0.5)时,才会触发垃圾回收,将这两个文件合并,执行合并的两个文件一定是逻辑上相邻的两个文件。

25.zset是用什么实现的

有序集合(zset)同样使用了两种不同的存储结构,分别是 zipList(压缩列表)和 skipList(跳跃列表),当 zset 满足以下条件时使用压缩列表:

ziplist:满足以下两个条件的时候

  • 元素数量少于128的时候
  • 每个元素的长度小于64字节

skiplist:不满足上述两个条件就会使用跳表,具体来说是组合了map和skiplist

  • map用来存储member到score的映射,这样就可以在O(1)时间内找到member对应的分数
  • skiplist按从小到大的顺序存储分数
  • skiplist每个元素的值都是[score,value]对

skiplist本质上是并行的有序链表,但它克服了有序链表插入和查找性能不高的问题。它的插入和查询的时间复杂度都是O(logN)

普通有序链表的插入需要一个一个向前查找是否可以插入,所以时间复杂度为O(N),比如下面这个链表插入23,就需要一直查找到22和26之间。

在上面这个结构中,插入23的过程是

  • 先使用第2层链接head->7->19->26,发现26比23大,就回到19
  • 再用第1层连接19->22->26,发现比23大,那么就插入到26之前,22之后

上面这张图就是跳表的初步原理,但一个元素插入链表后,应该拥有几层连接呢?跳表在这块的实现方式是随机的,也就是23这个元素插入后,随机出一个数,比如这个数是3,那么23就会有如下连接:

  • 第3层head->23->end
  • 第2层19->23->26
  • 第1层22->23->26

下面这张图展示了如何形成一个跳表

总结一下跳表原理:

每个跳表都必须设定一个最大的连接层数MaxLevel

第一层连接会连接到表中的每个元素

插入一个元素会随机生成一个连接层数值[1, MaxLevel]之间,根据这个值跳表会给这元素建立连接

插入某个元素的时候先从最高层开始,当跳到比目标值大的元素后,回退到上一个元素,用该元素的下一层连接进行遍历,周而复始直到第一层连接,最终在第一层连接中找到合适的位置

redis中skiplist的MaxLevel设定为32层

skiplist原理中提到skiplist一个元素插入后,会随机分配一个层数,而redis的实现,这个随机的规则是:

一个元素拥有第1层连接的概率为100%

一个元素拥有第2层连接的概率为50%

一个元素拥有第3层连接的概率为25%

以此类推...

为了提高搜索效率,redis会缓存MaxLevel的值,在每次插入/删除节点后都会去更新这个值,这样每次搜索的时候不需要从32层开始搜索,而是从MaxLevel指定的层数开始搜索

这句话是关于跳跃表的一种优化方法,跳跃表是一种用来实现有序集合的数据结构,它由多层链表组成,每一层链表都是前一层链表的子集,每个结点都有一个指向下一层的指针。跳跃表的查询效率很高,因为它可以从高层链表开始搜索,然后逐渐降低层级,直到找到目标结点。但是如果每次搜索都从最高层开始,而最高层的结点数很少,那么就会浪费很多时间。所以为了提高搜索效率,redis会缓存MaxLevel的值,这个值表示当前跳跃表的最高有效层级,也就是最高层中有两个或以上的结点的层级。这样每次搜索的时候就不需要从32层开始搜索,而是从MaxLevel指定的层数开始搜索²³。这样可以减少不必要的比较次数,提高搜索效率。

26.CAS和版本号

乐观锁和悲观锁是两种并发控制的策略,乐观锁认为数据在一般情况下不会造成冲突,所以在访问数据时不会加锁,而是在更新数据时判断是否有其他线程修改过数据,如果有则重试或者放弃操作;悲观锁认为数据在一般情况下会造成冲突,所以在访问数据时会加锁,防止其他线程修改数据,直到操作完成后释放锁。乐观锁和悲观锁各有优缺点,适用于不同的场景。乐观锁适合读多写少的场景,可以减少锁的开销,提高并发性能;悲观锁适合写多读少的场景,可以避免数据不一致的问题,保证数据的安全性。

乐观锁的实现方式有两种:CAS(Compare And Swap)和版本号机制。CAS是一种原子操作,它需要三个参数:内存地址V,预期值A和新值B。CAS指令执行时,比较内存地址V的值是否等于预期值A,如果相等则将新值B写入V,否则不做任何操作。CAS可以保证共享变量的原子性,但是也有一些缺点,比如ABA问题(即一个值被修改了两次又恢复原值),自旋开销(即如果更新失败则不断重试),以及只能操作一个变量¹²。

版本号机制是另一种实现乐观锁的方式,它需要在数据表中增加一个版本号字段,每次更新数据时,将版本号加一,并且检查更新前后的版本号是否一致。如果一致,则说明没有其他线程修改过数据,更新成功;如果不一致,则说明有其他线程修改过数据,更新失败³⁴。版本号机制可以避免ABA问题,也可以操作多个变量,但是也需要额外的空间来存储版本号,并且可能存在并发冲突导致更新失败的情况。

CAS只能保证单个变量的原子性,是因为它的底层实现是基于CPU的cmpxchg指令,这个指令只能对一个内存地址进行比较和交换操作²⁴。如果要对多个变量进行原子操作,就需要使用锁或者其他的方式¹³⁵。CAS只能操作一个变量,也是它的一个缺点之一,除此之外,CAS还可能存在ABA问题,自旋开销和并发冲突等问题¹⁶。

27.mysql怎么处理并发访问呢

mysql处理并发访问的主要方式是通过读写锁和多版本并发控制 (MVCC)。¹

读写锁可以分为表锁和行锁,表锁会锁定整张表,行锁会锁定某一行数据。¹²

多版本并发控制 (MVCC) 是一种优化并发读写的机制,它可以让读操作不用加锁,而是根据每行记录的版本号来判断数据的可见性。¹

mysql的并发控制也受到存储引擎的影响,不同的存储引擎有不同的实现方式。³

MVCC是一种不加锁的读取方式,它可以让读操作和写操作并行进行,提高数据库的性能。¹²

但是MVCC并不是完全不需要加锁,它只能避免读写冲突,也就是快照读不加锁,但是写写冲突还是需要加锁的,也就是当前读需要加锁。³⁴

当前读是指对数据进行修改的操作,例如select ... for update, update, delete, insert等语句,这些语句都会对数据加排他锁,防止其他事务修改同一条记录。⁴⁵

快照读是指对数据进行查询的操作,例如select语句,这些语句不会对数据加锁,而是根据记录的版本号来判断数据是否可见。⁴⁵

MVCC只能工作在REPEATABLE READ (可重复读)和READ COMMITED (提交读)两种事务隔离级别下。其他两个隔离级别都与MVCC不兼容,因为READ UNCOMMITED (未提交读)总是读取最新的数据行,而不是符合当前事务版本的数据行;而SERIALIZABLE则会对所有读取的行都加锁,也不符合MVCC的思想。

InnoDB通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号(LSN)。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。

28.线程通信与进程通信

线程通信和进程通信是两种不同的并发编程的方式,它们的区别和联系如下:

  • 线程通信是指同一进程内的不同线程之间的信息交换,它们可以共享进程的地址空间和资源,因此通信效率高,但需要注意同步和互斥的问题。¹²
  • 进程通信是指不同进程之间的信息交换,它们拥有独立的地址空间和资源,因此通信效率低,但安全性高,需要借助操作系统提供的通信机制,如管道、消息队列、共享内存、信号量、信号、套接字等。¹³
  • 线程通信的目的主要是用于线程同步,保证数据的一致性和正确性,而进程通信的目的主要是用于数据交换,实现不同进程之间的协作和功能拓展。²⁴
  • 线程通信和进程通信都可以提高程序的并发度和性能,但也增加了编程的复杂度和开销,需要根据具体的应用场景和需求来选择合适的方式。¹²

29.Java锁的底层实现

Java锁的底层实现主要依赖于AbstractQueuedSynchronizer(简称AQS)类,它是一个抽象类,提供了一套基于CAS和双向链表的同步器框架。AQS中维护了一个状态值和一个等待队列,状态值用于表示锁的占用情况,等待队列用于存储等待获取锁的线程节点。AQS定义了一些模板方法,如acquire、release、tryAcquire、tryRelease等,具体的锁实现类,如ReentrantLock、ReadWriteLock等,需要继承AQS并实现其中的抽象方法。

当一个线程调用lock方法时,它会首先尝试通过CAS操作修改状态值,如果成功,则表示获取到了锁;如果失败,则表示锁已经被其他线程占用,此时它会将自己封装成一个Node节点,通过自旋和CAS操作将自己加入到等待队列的尾部。然后它会调用L

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

曾获多国内大厂的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。专注后端高频面试与八股知识点,内容系统详实,覆盖约 30 万字面试真题解析、近 400 个热点问题(包含大量场景题),60 万字后端核心知识(含计网、操作系统、数据库、性能调优等)。同时提供简历优化、HR 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容

全部评论
师弟问我要面经,我转发了你的链接
点赞 回复 分享
发布于 03-29 20:20 辽宁

相关推荐

04-02 10:14
门头沟学院 Java
  楼主经历如题,从三月初开始陆续投递各家大厂,做了一堆测评和笔试,但最终约面的也仅有淘天、字节和腾讯(两个无笔试的,一个一面自我感觉良好结果笔试a了0道题被挂了),忍俊不禁了。  处子面是淘天的电话面试,面试前蛮紧张不过开始面试就还好,自我感觉答得不错但是犯了些低级错误,后续补上笔试后(第一周根本没给我发笔试,然后又先约面再让我补笔试)因为答得很差所以被挂,很能理解。  再是字节面试,这是三家面试里第三个找我的,但是这里放在前面先说了。面试官感觉没什么生气,也没什么和我探讨的想法经常打断,而我自己也有一定问题(HashMap我能想到写时复制进行扩容但是细节没太搞懂,太专注于旧表而没想到可以直接更新新表,问我sentinel组件可能的原理我第一时间脑袋宕机开始自己扯类似于时间窗口的限流实现而没想到漏桶和令牌桶,自己的想法遭到拷打了想别的出路才想到两个最经典的限流想法),面试体验比较差(因为淘和鹅的一面面试官都会引导我深入去想,字节这个就光看着你然后发出质疑)。算法是单链表的快速排序,先让我说了思路我稍加思索说出来了,但写代码我写了二十多分钟剩下合并链表和返回没来得及写,然后被吐槽写代码慢,我确实没给自己做过限时代码训练但也真不至于吧(单链表快排我得考虑找到中间节点分割链表然后合并,交换两个链表中的节点,按照快速排序的思想考虑中间状态和边界条件,自己定义链表节点类,第一次遇到的话真不简单吧,但凡你让我用双向链表呢我请问)。  最后再说腾讯面试吧,我真的是很幸运并且自己也把握住了机会才能用平平无奇的211学历和0实习履历才最终获得腾讯offer。一开始投了腾讯后被晾了两周,心态有点崩,随后约面邀请同淘天和字节一块到来。一面面试官很年轻也很有耐心,在这里我贡献了自己的第一次视频面试,自己答得不错并且面试官也有耐心引导我往细了和宽了想,最后的手撕环节也会引导我纠错,总体是个平等交流的氛围。上午面试完下午便约了我的二面,比较神奇的是约了线下面试,我心情比较忐忑但还是接了下来,面试当日通勤一小时左右到达公司与二面面试官进行面试,这位前辈有很强的个人风格,基础知识问了业务对口的内容但我完全没准备所以相当于完全没答出来,但后续问了我思考题和开放性场景问题并对我的表现感到满意,让人感觉这位前辈的确很有想法很关心后辈也很认真负责,二面结束后我心情比较好便在回去的路上逛了逛,途中看流程已经被推进了。三面总监面比较忐忑,无摄像头且另一边比较嘈杂,能感受到面试官其实也并不太认真但是有在努力听我讲,全程准时聊了四十分钟,问了些项目再加上聊天,后面也一直在链接状态,有怀疑过是所谓的kpi,但我更偏向于这位面试官是忙迷糊了,事实也是如此。次周周一我打电话询问了一下,没过半小时链接状态便消失了,的确是面试官忙得忘了提交我的面评。最后是HR面,面试官比较官方而且应该是在边问边记,其实我最大的优势应该是热情和立即到岗,所以虽然怕遇到横向对比被挂掉功亏一篑心里有些忐忑但是整体上还是比较有信心,随后走了两个工作日的流程我如愿收到了offer,皆大欢喜。  以上是我的面试情况,真的感觉是运气占了大部分因素,尤其是对我这种履历不出彩的角色。遇到认真负责有耐心的面试官,愿意发掘你的闪光点,那么就有可能得到机会。遇到机器人一样对面试候选人兴趣不大,单纯抛问题然后就着哪个知识储备更丰富履历更光鲜就简单高效地去筛选的,不能说这样有问题,只能说会比价难办。  最后其实就是对自己投了非常多家公司但是面试的很少的这件事有点不甘吧,尤其是团子,我去参加了宣讲会参加了两次笔试第二次三道编程题自己a了1.95结果三个志愿全挂还给我发邮件问我愿不愿意接受调剂去其他方向(前端客户端运营啥的),我是什么很差的人吗。我看团的面经基本都比较基础而且流程又短又快,但凡给我个面试问题真不大吧,懒得喷。只能说运气的含金量还在上升。  最后感谢您愿意看到这里,有想要交流的点可以在评论区发出来,我愿意和您沟通交流#暑期实习   ##暑期##腾讯##腾讯求职进展汇总##面试##面试常问题系列##面试体验感最好的是哪家?#
点赞 评论 收藏
分享
评论
8
44
分享

创作者周榜

更多
牛客网
牛客企业服务