今天要和大家分享的是我们训练营内部整理的腾讯CSIG(QQ浏览器) 的一面面经。我已经把所有的问题和答案都整理好了,希望对大家有帮助:
什么情况下会使用到责任链?
为什么会用到GC算法?
你有没有遇到过实际使用到GC算法时的一些问题?
怎么解决循环引用问题?
单例模式中有没有什么难点?
双检锁中两个null的作用?
sychonized是对什么上锁?
TCP拥塞控制是怎么实现的?具体算法有了解吗?
http了解吗?http缓存是存储的什么?
进程间有那些通信方式?
什么情况下会使用到什么通信方式?
信号量与互斥锁的区别?
可见性当中,线程的缓存是保存在哪里的?
死锁如何解决?
遇到过OOM吗?怎么解决?
面经详解
什么情况下会使用到责任链?
-
正确答案:责任链模式通常在请求的处理者不确定或者可能有多个处理者的情况下使用,每个处理者可以决定是否处理该请求或将请求传递给下一个处理者。这种模式适用于解耦请求发送者和处理者之间的关系,并允许动态地指定一组处理对象。
-
解答思路:
- 确定问题场景中是否存在多个可能的处理逻辑,并且这些逻辑之间有先后顺序或优先级。
- 判断是否需要将请求的发送者与具体的处理者解耦,使得系统更容易扩展。
- 如果存在多个条件分支判断谁来处理请求,且这些条件可能变化或组合,那么可以考虑使用责任链模式。
- 设计一个通用的处理接口,让每个处理者都实现这个接口,并持有下一个处理者的引用。
- 每个处理者在接收到请求后,根据自身条件决定是处理还是传递下去。
-
深度知识讲解:
责任链模式(Chain of Responsibility Pattern)的核心思想
责任链模式属于行为型设计模式,其目的是将请求的发送者和接收者解耦,使多个对象都有机会处理请求,从而避免请求发送者与具体处理者之间的紧耦合。
主要组成部分:
- Handler(处理器) :定义处理请求的接口,通常包含一个指向下一处理器的引用。
- ConcreteHandler(具体处理器) :实现处理逻辑,决定是否处理当前请求或将请求转发给下一个处理器。
使用场景举例:
- 审批流程(如请假申请、报销审批等)
- 过滤器/拦截器机制(如Web请求中的过滤器链)
- 日志记录系统(不同级别的日志由不同的处理器处理)
- 权限验证(逐层校验用户权限)
优点:
- 请求发送者无需知道哪个对象真正处理请求,只需面向接口编程。
- 增加新的处理者非常容易,符合开闭原则。
- 可以灵活设置处理链的结构,便于动态调整处理顺序。
缺点:
- 若没有明确的终止条件或处理逻辑不当,可能导致请求未被处理。
- 链条过长会影响性能。
- 调试时可能难以追踪请求的流向。
底层数据结构支持:
责任链本质上是一个链表结构,每个节点代表一个处理器,通过指针连接形成一条链。因此底层可使用单链表(每个节点有一个 next 指针)来实现。
为什么会用到GC算法?
-
正确答案:GC(Garbage Collection,垃圾回收)算法用于自动管理内存,其主要目的是识别并释放程序中不再使用的对象所占用的内存空间,从而避免内存泄漏和手动内存管理带来的错误。它广泛应用于Java、Python、C#等现代编程语言中。
-
解答思路:
- 程序在运行过程中会动态分配内存来存储对象或数据结构。
- 当这些对象不再被引用时,它们所占用的内存就成为了“垃圾”。
- 如果不进行清理,这些无用的对象将一直占据内存,最终导致内存耗尽(Out of Memory)。
- GC算法通过自动检测哪些对象是不可达的(即无法再被访问到),然后回收它们所占的空间。
- 不同的GC算法有不同的策略,例如标记-清除、复制、标记-整理、分代收集等。
你有没有遇到过实际使用到GC算法时的一些问题?
-
正确答案:在实际使用垃圾回收(GC)算法时,常见的问题包括内存泄漏、频繁Full GC导致性能下降、对象生命周期管理不当、以及GC停顿时间过长影响系统响应。这些问题通常与程序设计、内存分配模式和GC算法的选择有关。
-
解答思路: 首先要明确GC的作用是自动管理堆内存,回收不再使用的对象所占用的空间。但在实际开发中,由于代码逻辑或配置不当,可能会导致以下情况:
- 内存泄漏:某些对象本应被回收,但由于仍被引用(如静态集合类、监听器等),无法被GC回收。
- Full GC频繁触发:大量短命对象的创建或大对象直接进入老年代,可能引起频繁Full GC,影响应用性能。
- GC停顿时间长:例如Serial、CMS等算法在进行标记或清理阶段时会暂停应用线程(Stop-The-World),如果对象多且复杂,停顿时间就会长。
- GC参数配置不合理:比如堆大小设置不合理、新生代/老年代比例不合适,也会导致GC行为异常。
解决这些问题的关键在于理解不同GC算法的工作机制,并结合监控工具(如JVisualVM、JConsole、MAT、Arthas等)分析内存使用情况和GC日志,优化代码结构和GC参数。
-
深度知识讲解:
常见GC算法及其实现原理
-
标记-清除算法(Mark-Sweep)
- 标记所有可达对象,未被标记的对象为垃圾。
- 清除阶段回收未被标记的对象。
- 缺点:会产生内存碎片。
-
复制算法(Copying)
- 将内存分为两个相等区域,每次只用其中一个。
- 活跃对象复制到另一块区域后清空原区域。
- 优点:无碎片;缺点:空间利用率低。
-
标记-整理算法(Mark-Compact)
- 标记阶段同Mark-Sweep。
- 整理阶段将存活对象向一端移动,然后清理边界以外的内存。
- 优点:消除碎片;适合老年代。
-
分代收集算法(Generational Collection)
- 新生代使用复制算法(如Eden + Survivor区)。
- 老年代使用标记-清除或标记-整理算法。
- Java虚拟机常用这种策略。
-
怎么解决循环引用问题?
正确答案:循环引用是指两个或多个对象相互引用,导致它们的引用计数无法归零,从而引发内存泄漏的问题。解决循环引用的方法包括使用弱引用(weak reference)、手动解除引用、使用垃圾回收机制(如Python的gc模块)等。
-
解答思路:
- 首先识别循环引用的场景,例如在面向对象编程中两个对象互相持有对方的引用。
- 然后选择合适的解决方案,比如将其中一个引用设为弱引用,或者在对象生命周期结束时主动断开连接。
- 在支持自动垃圾回收的语言中,也可以依赖语言本身的GC机制来处理(如Python、Java等),但需注意其局限性。
单例模式中有没有什么难点?
-
正确答案:单例模式的难点主要体现在以下几个方面:线程安全、防止反射破坏单例、防止序列化与反序列化破坏单例、延迟加载(懒汉式)与资源管理之间的权衡,以及在分布式系统中的扩展性问题。
-
解答思路:
- 先回顾单例模式的基本定义:确保一个类只有一个实例,并提供全局访问点。
- 然后分析实现方式中可能遇到的问题,比如多线程环境下是否能保证唯一实例。
- 探讨外部机制如反射、序列化是否会破坏单例结构。
- 进一步考虑性能、资源释放、延迟加载等细节问题。
- 最后总结如何用不同的技术手段解决这些难点。
双检锁中两个null的作用?
-
正确答案:在双检锁(Double-Checked Locking)模式中,两个 null 检查的作用分别是:
- 第一次 null 检查是为了避免不必要的加锁操作,提高性能。
- 第二次 null 检查是在加锁之后进行的,确保在多线程环境下只有一个实例被创建。
这种机制通常用于实现线程安全的单例模式。
-
解答思路:
假设我们要实现一个线程安全的单例类。如果不加锁,在多线程环境下可能会有多个线程同时进入创建实例的代码块,导致创建多个实例。如果直接使用 synchronized 方法或代码块,虽然可以保证线程安全,但会带来较大的性能开销。
双检锁通过两次 null 检查来优化这个问题:
- 第一次检查 instance 是否为 null,如果非 null 则直接返回实例,无需加锁。
- 如果第一次检查为 null,则加锁,再次检查 instance 是否为 null(防止在等待锁的过程中其他线程已经创建了实例),只有为 null 才真正创建实例。
sychonized是对什么上锁?
-
正确答案:在Java中,synchronized关键字是对对象的监视器(Monitor)上锁。每个Java对象都有一个与之关联的监视器锁(也称为内部锁或intrinsic lock),当使用synchronized修饰方法或代码块时,JVM会确保同一时刻只有一个线程可以持有该对象的锁并执行受保护的代码。
-
解答思路:
-
首先明确 synchronized 是 Java 提供的一种同步机制,用于控制多线程环境下的并发访问。
-
然后理解 synchronized 可以作用于实例方法、静态方法和代码块。
-
接着分析不同场景下锁定的对象是什么:
- 实例方法:锁定的是当前实例对象 this。
- 静态方法:锁定的是当前类的 Class 对象(即 类.class)。
- 同步代码块:锁定的是括号中指定的对象(如 synchronized(obj))。
-
最后总结 synchronized 的本质是基于对象的监视器锁来实现同步控制。
-
TCP拥塞控制是怎么实现的?具体算法有了解吗?
-
正确答案:TCP拥塞控制是一种动态调整发送速率以避免网络过载的机制,主要通过维护一个称为“拥塞窗口”(Congestion Window, cwnd)的变量来实现。具体算法包括慢启动(Slow Start)、拥塞避免(Congestion Avoidance)、快速重传(Fast Retransmit)和快速恢复(Fast Recovery)。
-
解答思路: 首先明确TCP是面向连接、可靠的传输协议,它不仅负责端到端的数据可靠传输,还要防止网络拥塞。拥塞控制的目标是在不过载网络的前提下尽可能提高吞吐量。
TCP使用滑动窗口机制进行流量控制,而拥塞控制则是基于对网络状态的估计来动态调整发送窗口大小。常见的拥塞控制算法包括Reno、Tahoe、New Reno、Cubic等,其中Reno是最经典的实现之一。
http了解吗?http缓存是存储的什么?
-
正确答案:HTTP 是超文本传输协议,用于客户端和服务器之间传输网页内容。HTTP 缓存是浏览器为了减少网络请求、提高页面加载速度而将资源(如 HTML 页面、图片、CSS 和 JavaScript 文件等)存储在本地的一种机制。缓存的内容主要包括响应体(即资源本身)以及与该资源相关的 HTTP 响应头信息。
-
解答思路:
- 先明确 HTTP 协议的基本作用:它是基于请求/响应模型的应用层协议,常用于 Web 浏览器与服务器之间的通信。
- 然后解释 HTTP 缓存的目的:提升性能、减少带宽使用、降低服务器压力。
- 接着说明缓存存储的是什么:不仅仅是资源内容,还包括控制缓存行为的头部字段。
- 最后可以扩展缓存的类型(强缓存、协商缓存)及其工作原理。
进程间有那些通信方式?
-
正确答案:进程间通信(IPC,Inter-Process Communication)是指在不同进程之间交换数据或协调行为的机制。常见的进程间通信方式包括:管道(Pipe)、命名管道(FIFO)、消息队列(Message Queue)、共享内存(Shared Memory)、信号量(Semaphore)、套接字(Socket)、信号(Signal)等。
-
解答思路: 面试中问及进程间通信方式时,考察的是你对操作系统中多进程协作机制的理解。你需要从不同通信方式的特点、适用场景、效率、同步机制等方面进行回答,并能简要说明其底层实现原理。
解题步骤如下:
- 列举主要的进程间通信方式;
- 简述每种方式的基本原理;
- 比较它们的优缺点和适用场景;
- 可适当结合代码示例或伪代码加深理解。
什么情况下会使用到什么通信方式?
- 正确答案:在不同的系统架构、网络环境、性能需求和应用场景下,会选择不同的通信方式。常见的通信方式包括进程间通信(IPC)、线程间通信、Socket通信、HTTP/HTTPS协议通信、消息队列(如Kafka、RabbitMQ)、远程过程调用(RPC)、RESTful API、WebSocket等。
选择通信方式时需要考虑的因素有:
-
通信是否在同一台机器上(本地 vs 远程)
-
是否需要保证数据顺序、可靠性、安全性
-
对延迟、吞吐量的要求
-
系统的可扩展性与维护成本
-
解答思路: 首先明确“通信”的定义,它可能指的是操作系统层面的进程或线程之间通信,也可能是不同服务器之间的网络通信。接着分析具体场景:
- 如果是同一进程内的线程间通信,可以使用共享内存、锁机制(互斥量、信号量)、条件变量等。
- 如果是同一台机器上的不同进程通信,可以选择管道(Pipe)、命名管道(FIFO)、消息队列、共享内存、套接字(Unix Domain Socket)等。
- 如果是跨网络的不同主机间的通信,则通常使用TCP/IP协议栈中的Socket通信、HTTP/HTTPS、FTP、SMTP、WebSocket等。
- 在分布式系统中,为了解耦服务和提高可用性,常使用消息中间件(如Kafka、RabbitMQ、RocketMQ)进行异步通信。
- 微服务架构中常用RPC框架(如gRPC、Dubbo)或RESTful API进行服务间调用。
信号量与互斥锁的区别?
-
正确答案:信号量(Semaphore)和互斥锁(Mutex)都是用于控制并发访问共享资源的同步机制,但它们在使用方式、语义和功能上存在显著区别。互斥锁是一种特殊的二值信号量,只能由持有它的线程释放,而信号量可以由任意线程释放,并且允许多个线程同时访问资源。
-
解答思路: 首先明确两者的基本定义和用途:
- 互斥锁用于保护临界区,确保一次只有一个线程可以访问共享资源。
- 信号量用于控制对有限数量资源的访问,允许指定数量的线程同时进入。
然后比较它们的核心特性:
- 所有权:互斥锁具有所有权概念,只有加锁的线程才能解锁;信号量没有所有权,任何线程都可以增加计数。
- 计数机制:互斥锁是二值的(0或1),信号量可以是任意非负整数。
- 使用场景:互斥锁适用于一对一的资源保护,信号量适用于一对多的资源池管理。
可见性当中,线程的缓存是保存在哪里的?
-
正确答案:线程的缓存通常保存在CPU的寄存器和各级缓存(L1、L2、L3 Cache)中,而不是主内存中。每个线程运行时所访问的变量可能会被加载到这些高速缓存中以提高访问速度,这会导致多线程环境下可见性问题的出现。
-
解答思路: 在多线程编程中,当多个线程同时访问共享变量时,由于现代CPU为了提升性能引入了多级缓存机制,每个线程可能读取的是自己所在CPU核心中的缓存副本,而不是主内存中的最新值。这就导致了一个线程对变量的修改,其他线程可能看不到,从而引发可见性问题。
因此,线程的缓存并不是保存在Java堆内存(即主内存)中,而是保存在CPU的寄存器或高速缓存中。为了解决这个问题,Java提供了volatile关键字、synchronized关键字以及显式锁(如ReentrantLock)来保证变量的可见性和有序性。
死锁如何解决?
-
正确答案:死锁是指两个或多个进程(线程)在执行过程中,因争夺资源而造成的一种相互等待的状态,若无外力作用,它们都将无法推进下去。解决死锁的方法主要包括预防死锁、避免死锁、检测与恢复以及忽略死锁四种策略。
-
解答思路:
- 首先理解死锁的四个必要条件:互斥、持有并等待、不可抢占、循环等待。
- 根据这些条件,分析如何破坏其中一个或多个条件来防止死锁发生。
- 然后根据系统环境选择合适的策略,如银行家算法(避免)、资源分配图简化(检测)等。
- 最后结合具体应用场景给出实现方式或优化建议。
-
深度知识讲解:
死锁的四个必要条件(必须同时满足才会产生死锁):
- 互斥(Mutual Exclusion) :资源不能共享,一次只能被一个进程使用。
- 持有并等待(Hold and Wait) :一个进程在等待其他资源时,不释放自己已持有的资源。
- 不可抢占(No Preemption) :资源只能由持有它的进程主动释放,不能被强制剥夺。
- 循环等待(Circular Wait) :存在一个进程链,其中每个进程都在等待下一个进程所持有的资源。
解决死锁的四种方法:
1. 预防死锁(Prevention)
-
目标是破坏上述四个条件中的至少一个。
-
常见做法:
- 消除“持有并等待” :要求进程一次性申请所有所需资源,否则不分配任何资源。
- 消除“不可抢占” :允许资源被强行回收(但可能导致数据不一致等问题)。
- 消除“循环等待” :对资源进行排序,要求进程只能按序申请资源。
2. 避免死锁(Avoidance)
-
在资源分配前进行安全性检查,确保不会进入不安全状态。
-
典型算法:银行家算法(Banker's Algorithm)
- 每次分配资源前判断系统是否仍处于安全状态(即是否存在一个调度顺序使得所有进程都能完成)。
- 要求系统预先知道每个进程的最大资源需求。
3. 检测与恢复(Detection and Recovery)
-
不预防死锁,而是定期运行检测算法,发现死锁后采取措施恢复。
-
检测方法:构建资源分配图,尝试简化该图。
-
恢复方法:
- 终止部分或全部死锁进程。
- 回滚到某个检查点(checkpoint),重新执行。
-
缺点:开销较大,且可能导致数据丢失。
4. 忽略死锁(Ostrich Algorithm)
- 即不处理死锁,认为其发生的概率极低。
- 多用于嵌入式系统或实时性要求不高、资源竞争少的系统中。
- Linux 和 Windows 等通用操作系统有时也采用这种方式。
遇到过OOM吗?怎么解决
-
正确答案:OOM(Out Of Memory)是指程序在运行过程中请求的内存超过了系统所能提供的内存,导致程序崩溃或被操作系统强制终止。遇到OOM问题时,常见的解决方法包括优化代码、调整JVM参数、分析内存使用情况并进行内存泄漏排查等。
-
解答思路:
- 首先确认是否真的发生了OOM,可以通过日志查看是否有类似“java.lang.OutOfMemoryError”这样的错误信息。
- 判断是堆内存溢出、栈内存溢出还是元空间/永久代溢出。
- 使用内存分析工具(如VisualVM、MAT、jstat、jmap等)分析内存快照,查找内存泄漏点或大对象占用。
- 根据分析结果优化代码逻辑,例如避免频繁创建大对象、及时释放不再使用的资源、采用更高效的数据结构等。
- 如果确实需要更多内存,可以适当增加JVM启动参数中的堆大小(-Xmx和-Xms)。
- 对于Java应用,注意元空间(Metaspace)默认无上限,可设置-XX:MaxMetaspaceSize防止元空间无限增
- 长。
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。