(拿铁)- Java并发编程&JUC真实超高频八股速成/中频八股提升/低频八股扩展(持续更新中)

之前看面经分享帖的时候,学到了已经上岸大厂的前辈的做法。在准备暑期实习时,我也效仿着根据以往的真实面经整理八股。从牛客、小破站等各个平台搜集了上千篇真实面经,自己整理得到了面试题,根据题目在面试题中出现的频率以及我自己、交流群、好朋友面试被问到的频率进行了分类整理,得到⭐🌟💡三种级别的。在此,给大家分享一下我自己面试被问到的题目,以及我根据以往面经整理得到的题目。各位uu可在专栏关筑一波:

https://www.nowcoder.com/creation/manager/columnDetail/Mq7Xxv

Top 博主都订阅了,比如“Java 抽象带篮子”(7000+ 粉丝),在这里感谢篮子哥的支持!

所有内容经过科学分类巧妙标注,针对性强,让你的学习事半功倍:

  • 必须掌握(必看):时间紧迫时的救命稻草,优先攻克核心要点。(可参考神哥的高频题,但我整理出来的比神哥还会多一些,另外还包括以下内容
  • 🌟 尽量掌握(有时间就看):适合两周以上备考时间的同学稳步提升,冲击大厂的uu们建议看!
  • 💡 了解即可(知识拓展):时间充裕时作为补充,拓宽视野,被问到的概率小,但如果能答出来就是加分项
  • 🔥 面试真题:根据真实面经整理出来的面试题,有些可能难度很高,可根据自身水平酌情参考。

按照推荐观看顺序 “🔥⭐> 🔥🌟 > > 🔥💡” 有条不紊地学习,让每一分每一秒都用在刀刃上,自此一路畅行。

全面覆盖面试核心知识

面试真题涵盖技术领域的核心考点,从高频热点到冷门难点一网打尽。以下是部分模块概览:

Java基础&集合 :

https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=4f5b4cac4b9f4dee8b4b213851c154c5

JVM篇:

https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=c87d9ad65eb840728ae63774893bccf5

Java并发编程&JUC:

https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=28c748189f6b471f9f4218791778f41c

MySQL

https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=55b03d6d16604319a24395f393d615be

Redis:

https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=77bd828f85984c22858c3724eef78723

计网:

https://www.nowcoder.com/issue/tutorial?zhuanlanId=Mq7Xxv&uuid=65e9951c2e754d7086d26b9b46aa4a1a

后续还将持续更新 操作系统、设计模式、场景题、智力题等丰富内容

独特解析:知其然,更知其所以然

我整理的八股面经绝非简单的问答堆砌。

每一道题目都配有深度剖析的思考过程,在你看题之前,便清晰呈现出题意图,让你迅速抓住题目核心,加深对题目的理解与记忆,做到 “知己知彼,百战不殆”。

对于容易关联的知识点,更是独具匠心地进行关联标注。

提及死锁,马上为你串联起操作系统与 MySQL 中的相关知识;

说到乐观锁、悲观锁,同步展示 Java 与 MySQL 的实现方式

助力你举一反三,深度梳理知识点之间的内在逻辑联系,真正实现知识的融会贯通,做到知其然更知其所以然。

后续还会分享如何包装项目、leetcode 刷题模版与刷题技巧、各种学习经验以及真实面经等,从多个角度助力牛u提升技术能力和面试水平。

更新日志:

2025.4.15 更新:基础(重要,很多概念可以结合操作系统来理解,比如进程和线程、线程的状态)

2025.4.18 更新:线程间通信(主要问线程间同步,偶尔会问JMM、Happens-before等原理)

2025.4.21 更新:volatile关键字(很重要)、synchronized关键字(很重要)、CAS(很重要,CAS的原理、存在的问题要熟悉,相关的乐观锁、原子类要能说出来)

2025.4.24 更新:Lock(主要考察 ReentrantLock的实现)、AQS(一般不会问太难,能说出AQS是什么以及底层的数据结构就可以了)、死锁(结合操作系统、MySQL死锁来理解)

2025.4.26 更新:线程池(太重要了,线程池的原理、核心参数、常见的线程池类型、线程池的参数怎么设置比较好都是常问的!)

2025.4.27更新:并发工具(经常问CyclicBarrier 和 CountDownLatch 有什么区别)

暂时更新完毕。。。

基础(重要,很多概念可以结合操作系统来理解,比如进程和线程、线程的状态)

🔥⭐Q: 看你技术栈写了多线程编程,为什么在 Java 中会使用多线程呢?/你觉得多线程编程能带来哪些好处呢?

思考过程:

主要从提升 CPU 利用率和应用性能两个方面进行思考。

  • 多核 CPU 利用率: 并发可以使多个线程在不同的 CPU 核心上执行,提高 CPU 的整体利用率。
  • 提升应用性能: 对于需要进行多个独立操作的场景,并发可以缩短整体执行时间,提高响应速度。
  • 业务拆分: 并发编程更适合复杂业务模型的拆分和并行处理。

回答提问:

好的面试官,使用并发编程的主要目的是为了充分利用现代计算机多核 CPU 的计算能力,从而提升应用程序的性能和响应速度

具体来说,现在的主机通常拥有多个 CPU 核心,通过创建多个线程,我们可以将不同的任务分配给不同的核心去并行执行,这样就能更有效地利用 CPU 资源,而不是让某个核心空闲而其他核心过载。

此外,并发编程也方便我们对复杂的业务进行拆分。例如,在处理用户请求时,我们可以将不同的操作(比如数据校验、业务逻辑处理、发送通知等)交给不同的线程去并行执行,这样可以显著缩短用户的等待时间,提高应用的响应速度。总的来说,并发编程能够更好地适应现代计算机硬件架构和复杂的业务需求。

🔥⭐Q: 你觉得并发编程中需要考虑的三个核心要素是什么?

思考过程:

这个问题考察对 并发编程基本概念的理解,需要掌握可见性、原子性和有序性这三个关键要素。

  • 可见性(Visibility): 一个线程对共享变量的修改,其他线程能够立即看到。
  • 原子性(Atomicity): 一组操作要么全部执行成功,要么全部执行失败,不可中断。
  • 有序性(Ordering): 程序执行的顺序按照代码的先后顺序执行(在单线程环境下)。

回答提问:

好的面试官,我认为并发编程中需要考虑的三个核心要素是可见性、原子性和有序性

可见性指的是当一个线程修改了共享变量的值时,其他线程能够立即看到这个修改后的值。在多线程环境下,由于 CPU 缓存的存在,一个线程对共享变量的修改可能不会立即刷新到主内存,导致其他线程读取到的仍然是旧的值。

原子性指的是一个操作或者一系列操作是不可分割的,要么全部执行成功,要么全部执行失败,在执行过程中不会被其他线程中断。例如,i++ 这个操作在多线程环境下就不是原子性的,因为它包含了读取、加一和写入三个步骤。

有序性指的是程序执行的顺序按照代码的先后顺序执行。但在多线程环境下,为了提高性能,处理器和编译器可能会对指令进行重排序,这可能会导致程序实际的执行顺序与代码的顺序不一致,从而引发问题。

为了保证多线程程序的正确性,我们需要采取相应的措施来保证这三个要素,比如使用 volatile 关键字保证可见性和有序性,使用 synchronized 关键字或 Lock 来保证原子性和可见性,以及通过 Happens-Before 原则来保证一定的有序性。

🔥⭐Q: 那在你们这个项目中,你有哪些方法来保证多线程的运行安全?(可以结合项目来答,然后自己提一些常见的技术点,引导面试官提别的问题~)

思考过程:

这个问题考察对 Java 中线程安全保障机制的掌握,需要从可见性、原子性和有序性三个方面给出解决方案。

  • 可见性:volatile 关键字,synchronized,ReentrantLock。
  • 原子性:synchronized,ReentrantLock,原子类(AtomicInteger 等)。
  • 有序性:volatile 关键字,synchronized,Happens-Before 规则。

回答提问:

好的面试官,在 Java 程序中,保证多线程运行安全主要需要解决可见性、原子性和有序性这三个问题。我通常会使用以下方法:

  1. 使用 volatile 关键字:volatile 可以确保被修饰的共享变量在被一个线程修改后,其最新的值会立即刷新到主内存,并且禁止指令重排序,从而保证了变量的可见性和一定的有序性。但是 volatile 不能保证复合操作的原子性。
  2. 使用 synchronized 关键字:synchronized 可以提供互斥锁,保证在同一时刻只有一个线程可以执行被 synchronized 修饰的代码块或方法,从而保证了操作的原子性和可见性。当一个线程释放锁时,会将工作内存中的变量刷新到主内存。
  3. 使用显式锁(如 ReentrantLock):ReentrantLock 提供了比 synchronized 更灵活的锁机制,同样可以保证原子性和可见性。它还提供了诸如公平锁、尝试获取锁等高级功能。
  4. 使用原子类(如 AtomicInteger、AtomicLong 等):java.util.concurrent.atomic 包下提供了一些原子类,它们利用 CAS(Compare-and-Swap)等机制实现了高效的原子操作,可以在不使用锁的情况下保证线程安全。

通过合理地选择和使用这些机制,我们可以有效地保证 Java 多线程程序的运行安全。

🔥⭐Q: 并行和并发有什么区别?

思考过程:

这个问题考察对 并行和并发概念的辨析,需要理解它们在执行方式上的差异以及对硬件的要求。

  • 并行: 多个任务在同一时刻同时执行,需要多核 CPU 支持。
  • 并发: 在一段时间内交替执行多个任务,单核 CPU 也可以实现。
  • 关键: 并行是“同时”,并发是“交替”。

回答提问:

好的面试官,并行(Parallelism)并发(Concurrency) 这两个概念经常被一起提及,但它们之间有着重要的区别。

并行指的是在同一时刻,多个任务被同时执行。这通常需要多核 CPU 的支持,每个 CPU 核心可以独立地执行一个或多个线程,从而实现真正的同时执行。就像我们同时做多件事情一样,比如一边听歌一边写代码。

并发指的是在一段时间内,多个任务交替执行。即使在单核 CPU 的情况下,通过时间片轮转等机制,CPU 也可以快速地在不同的任务之间切换,使得从宏观上看,这些任务好像是在同时执行一样。但实际上,在任何一个给定的时刻,只有一个任务在真正地占用 CPU 资源。就像我们一个人在不同的时间段内处理不同的事情。

简单来说,并行的关键在于“同时”,需要有足够的硬件资源(比如多个 CPU 核心)来支持多个任务在同一时刻执行。而 并发的关键在于“交替”,它侧重于如何有效地管理和调度多个任务,使得它们能够在有限的资源下高效地运行。

🔥⭐(结合操作系统来理解)Q: 什么是进程,什么是线程?

思考过程:

这个问题考察对 操作系统中进程和线程基本概念的理解,需要从资源分配、调度单位、内存共享等方面进行区分。

  • 进程: 操作系统进行资源分配和管理的基本单位,拥有独立的内存空间。
  • 线程: 操作系统进行任务调度和执行的基本单位,是轻量级进程,共享进程资源。
  • 主要区别: 线程是调度的基本单位,进程是资源拥有的基本单位。

回答提问:

好的面试官,进程(Process)线程(Thread) 都是操作系统中非常重要的概念,它们都是为了支持多任务并发执行而提出的。

进程可以被定义为操作系统进行资源分配和管理的基本单位。当一个程序被执行时,操作系统会为其创建一个进程,并分配独立的内存空间以及其他必要的资源(如文件句柄、网络连接等)。由于每个进程都拥有独立的内存空间,进程之间的切换开销比较大,因为需要切换进程的页表、寄存器等上下文信息。

线程则是操作系统进行任务调度和执行的基本单位,也被称为轻量级进程。一个进程可以包含一个或多个线程,线程是进程内部的执行单元。与进程最大的区别在于,线程本身并不拥有独立的系统资源,而是与所属进程内的其他线程共享该进程所拥有的资源,包括内存空间、代码段、数据段等。由于同一个进程内的线程共享内存空间,它们之间的通信相对简单高效,可以通过线程同步机制(如互斥锁、信号量)进行。线程切换的开销相对于进程切换要小得多,只需要切换线程的寄存器和栈等少量上下文信息。

总的来说,线程是调度的基本单位,而进程是资源拥有的基本单位。内核实际调度的是线程,而进程则为线程提供了运行所需的资源环境。

🔥⭐Q: Java 线程有几种状态?分别是什么?

思考过程:

这个问题考察对 Java 线程生命周期的理解,需要列出 Thread.State 枚举中定义的六种线程状态。

  • NEW: 新建状态。
  • RUNNABLE: 运行态(包含就绪和运行中)。
  • BLOCKED: 阻塞态,等待获取锁。
  • WAITING: 等待态,等待其他线程通知。
  • TIMED_WAITING: 延迟等待态,带有超时时间的等待。
  • TERMINATED: 终止状态。

回答提问:

好的面试官,Java 线程在 java.lang.Thread.State 枚举中定义了六种状态,分别是:

  • NEW(新建状态):当线程对象被创建但尚未调用 start() 方法时,它处于新建状态。
  • RUNNABLE(运行态):当线程调用了 start() 方法后,它就进入了运行态。需要注意的是,这里的运行态包含了两种情况:一是线程正在被 CPU 调度执行(Running),二是线程已经准备好被 CPU 调度执行(Ready)。Java 中并没有区分 Runnable 和 Ready 这两种状态。
  • BLOCKED(阻塞态):当线程试图获取一个被其他线程持有的锁时,它会进入阻塞态。
  • WAITING(等待态):当线程调用了 Object.wait() 方法(没有指定超时时间)、Thread.join() 方法(没有指定超时时间)或者 LockSupport.park() 方法时,它会进入等待态,等待其他线程的特定通知。
  • TIMED_WAITING(延迟等待态):与等待态类似,但线程在等待一定的时间后会自动返回。当线程调用了 Thread.sleep() 方法、带有超时时间的 Object.wait(long timeout) 方法、带有超时时间的 Thread.join(long timeout) 方法、带有超时时间的 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long deadline) 方法时,它会进入延迟等待态。
  • TERMINATED(终止状态):当线程的 run() 方法执行完毕,无论是正常结束还是因为异常而结束,线程都会进入终止状态。

🔥⭐Q: Java 中创建线程有哪些方法?能说一下吗?

思考过程:

这个问题考察对 Java 中创建线程的几种方式的掌握,需要列举并说明每种方式的特点和优缺点。

  • 继承Thread 类: 最基本的方式,重写 run() 方法。
  • 实现 Runnable 接口: 更推荐的方式,解耦任务和线程。
  • 实现 Callable 接口: 可以有返回值和抛出异常。
  • 使用线程池 (ExecutorService): 管理大量并发任务,线程复用。
  • CompletableFuture: 异步任务调用,支持链式调用和组合。

回答提问:

好的面试官,在 Java 中,创建线程主要有以下几种常用的方法:

  1. 继承Thread 类:这是最基本的创建线程的方式。我们可以创建一个类继承自 Thread 类,并重写其 run() 方法,将需要在新线程中执行的代码写在 run() 方法里。然后创建该子类的实例,并调用其 start() 方法来启动线程。这种方式的优点是编码简单直观,适合快速实现线程逻辑。缺点是由于 Java 的单继承特性,如果我们的类已经继承了其他的类,就不能再继承 Thread 类了,而且任务和线程绑定在一起,不利于资源的共享。
  2. 实现 Runnable 接口:这是一种更推荐的创建线程的方式。我们可以创建一个类实现 Runnable 接口,并实现其 run() 方法。然后创建一个 Thread 对象,并将实现了 Runnable 接口的实例作为参数传递给 Thread 的构造方法。最后调用 Thread 对象的 start() 方法来启动线程。这种方式的优点是可以避免 Java 单继承的限制,并且同一个 Runnable 实例可以被多个线程共享,更适合资源共享的场景。缺点是 run() 方法没有返回值,异常处理需要自行实现。
  3. 实现 Callable 接口并结合 Future 实现:这种方式类似于实现 Runnable 接口,但 Callable 接口的 call() 方法可以有返回值,并且可以抛出异常。我们需要先创建一个实现了 Callable 接口的类,然后将其实例包装在 FutureTask 中,再将 FutureTask 作为 Thread 的 target 来创建线程。最后可以通过 FutureTask 的 get() 方法来获取线程的执行结果。这种方式的优点是支持返回值和异常抛出,功能比 Runnable 更完善。缺点是 get() 方法会阻塞主线程的执行,如果需要异步获取结果,需要配合其他机制。
  4. 使用线程池 (ExecutorService):通过 java.util.concurrent 包下的 ExecutorService 接口,我们可以方便地管理和调度线程。我们可以将实现了 Runnable 或 Callable 接口的任务提交给线程池来执行,而无需直接创建和管理线程。线程池可以实现线程的复用,减少了线程创建和销毁的开销,并提供了队列管理、拒绝策略等完善的机制,适合管理大量并发任务。
  5. 使用 CompletableFuture:这是 Java 8 引入的一个功能强大的异步编程工具。CompletableFuture 本质上也是基于线程池实现的(默认使用 ForkJoinPool),但它提供了非常方便的链式调用(如 thenApply、thenAccept 等)来处理异步任务的结果,并且可以轻松地组合多个异步任务,简化了异步编程的回调地狱问题。

🔥🌟 Q: RunnableCallable 有什么区别?

思考过程:

这个问题考察对 RunnableCallable 接口异同点的理解,主要从返回值和异常处理两个方面进行区分。

  • 相同点: 都是函数式接口,都用于定义线程执行的任务,最终目的都是实现多线程并发。
  • 主要区别:返回值:Runnable 的 run() 方法没有返回值,Callable 的 call() 方法可以有返回值。异常处理:Runnable 的 run() 方法只能抛出运行时异常,Callable 的 call() 方法可以抛出各种类型的异常。

回答提问:

好的面试官,RunnableCallable 都是 Java 中用于定义需要被线程执行的任务的接口,它们有很多相似之处,比如它们都是函数式接口,都只包含一个抽象方法(run()call()),并且最终的目的都是为了实现多线程并发

它们的主要区别在于以下两点:

  1. 返回值:Runnable 接口中的 run() 方法没有返回值(void)。这意味着,当我们使用 Runnable 来定义一个任务时,任务执行完成后无法直接获取到结果。它更适用于那些不需要返回结果的异步任务,比如后台日志记录、发送通知等。而 Callable 接口中的 call() 方法可以有返回值(泛型类型 V)。这个返回值可以通过 Future 对象来获取。因此,Callable 更适用于那些需要获取异步计算结果的场景,例如复杂的计算任务或者需要返回数据的远程调用等。
  2. 异常处理:Runnable 接口中的 run() 方法只能抛出运行时异常(RuntimeException)。如果 run() 方法抛出了一个检查型异常(Checked Exception),那么编译器会报错。而 Callable 接口中的 call() 方法可以抛出各种类型的异常,包括编译时异常和运行时异常。通过 Future 对象,我们可以捕获并处理 Callable 任务执行过程中抛出的异常。这使得 Callable 在异常处理方面更加灵活和健壮。

总结来说,如果任务不需要返回值或者不需要抛出检查型异常,可以使用 Runnable。如果任务需要返回结果或者需要抛出检查型异常,那么应该使用 Callable

🔥🌟Q:sleep() 和 wait() 有什么区别?

思考过程

这个问题考察对 Thread.sleep()Object.wait() 这两个线程暂停方法的区别的理解。需要从类归属、锁释放、用途和唤醒机制等方面进行比较。

  • 类归属:sleep() 是 Thread 的静态方法,wait() 是 Object 的实例方法。
  • 锁释放:sleep() 不释放锁,wait() 释放锁。
  • 用途:wait() 主要用于线程间通信的等待-唤醒机制,sleep() 主要用于线程暂停执行一段时间。
  • 唤醒机制:wait() 需要 notify()/notifyAll() 唤醒或等待超时,sleep() 执行完成后自动苏醒。

回答提问

好的面试官,sleep()wait() 都是用来暂停线程执行的方法,但它们之间存在着一些关键的区别:

  • 类归属:sleep() 方法是 Thread 类的 静态方法,而 wait() 方法是 Object 类的 实例方法。这意味着我们可以在任何地方调用 Thread.sleep() 来让当前线程休眠,而 wait() 必须在某个对象的实例上调用。
  • 锁释放:这是它们之间 最核心的区别。当线程调用 sleep() 方法时,它会 暂停执行指定的时间,但不会释放任何已经持有的锁。而当线程调用 wait() 方法时,它会 释放当前持有的对象锁,并进入该对象关联的等待池(WaitSet)中,等待被唤醒。
  • 用途:wait() 方法主要用于 线程之间的通信和协作,通常与 notify() 或 notifyAll() 方法一起使用,实现等待-唤醒机制。线程通过 wait() 进入等待状态,等待其他线程执行某些操作后通过 notify() 或 notifyAll() 来唤醒它。而 sleep() 方法主要用于 让线程暂停执行一段时间,通常用于模拟耗时操作或者进行简单的定时任务。
  • 唤醒机制:线程在调用 wait() 后,需要 其他线程调用同一个对象的 notify() 或 notifyAll() 方法来显式地唤醒,或者等待 指定的时间超时后自动唤醒。而调用 sleep() 方法的线程,在 休眠时间结束后会自动苏醒,不需要其他线程的干预。

简单来说,sleep() 侧重于让线程“睡一觉”,不涉及锁的释放和线程间的通信;而 wait() 则更侧重于线程之间的“等待”和“通知”,并且会释放锁。

🔥🌟Q:Thread.join() 的原理是什么?

思考过程

这个问题考察对 Thread.join() 方法的理解。需要说明其底层使用了 wait()/notify() 机制,以及调用 join() 的线程会等待目标线程执行完毕。

  • 底层机制:使用 synchronized + wait() 机制。
  • 调用 join() 的线程行为:等待目标线程执行完毕。
  • 唤醒机制:目标线程执行完毕后会调用 notifyAll() 唤醒等待的线程。
  • 示例代码分析:主线程调用 thread1.join() 会等待 thread1 执行完毕。

回答提问

好的面试官,Thread.join() 方法的原理实际上是 基于 synchronizedwait() 机制 实现的。

当一个线程(比如主线程)调用另一个线程(比如线程 B)的 join() 方法时,实际上是 主线程会等待线程 B 执行完毕

底层的机制是这样的:当主线程调用 threadB.join() 时,主线程会 获取线程 B 对象上的锁(相当于执行 synchronized(threadB)),然后 调用线程 B 对象的 wait() 方法,使得主线程进入到线程 B 的等待队列中。

当线程 B 执行完毕后(无论是正常结束还是抛出异常),JVM 会 在线程 B 内部调用 notifyAll() 方法,这个 notifyAll() 方法会 唤醒所有在等待线程 B 对象上的线程,包括之前调用了 threadB.join() 而进入等待状态的主线程。

这样,主线程就被唤醒了,它可以继续执行 join() 方法后面的代码。

从源码中也可以看到,join() 方法内部会判断目标线程是否还存活,如果存活则会调用 wait() 方法进行等待,直到目标线程执行完毕并通过 notifyAll() 唤醒等待的线程。

🔥🌟Q:Java 中 Thread.sleep() 和 Thread.yield() 的区别是什么?

思考过程

这个问题考察对 Thread.sleep()Thread.yield() 这两个线程调度方法的区别的理解。需要从线程状态、CPU 占用和行为意愿等方面进行比较。

  • Thread.sleep():主动休眠一段时间,进入 TIMED_WAITING 状态,不占用 CPU。
  • Thread.yield():建议让出 CPU 资源,进入 RUNNABLE 状态,是否让出取决于线程调度器。

回答提问

好的面试官,Thread.sleep()Thread.yield() 都是 Java 中用于控制线程执行的方法,但它们的行为和意图有所不同:

  • Thread.sleep():这个方法会让当前正在执行的线程 主动休眠 指定的一段时间,时间的长短由我们来决定。当线程调用 sleep() 方法后,它会 进入 TIMED_WAITING 状态,并且在休眠期间 不会占用 CPU 资源。当休眠时间结束后,线程会重新进入 RUNNABLE 状态,等待 CPU 的调度。sleep() 可以保证线程至少休眠指定的时间。
  • Thread.yield():这个方法是线程向线程调度器 发出的一个建议,表示当前线程 愿意让出 CPU 的使用权,以便让其他线程有机会执行。当线程调用 yield() 方法后,它会 从当前正在执行的状态变为 RUNNABLE 状态,但 并没有被阻塞。至于线程调度器是否会采纳这个建议,以及将 CPU 时间片分配给哪个线程,是 取决于具体的操作系统和 JVM 的实现 的。调用 yield() 并不能保证当前线程一定会立即让出 CPU,也不能保证其他线程一定会获得 CPU 时间片。

简单来说,Thread.sleep() 是让线程 强制休息 一段时间,而 Thread.yield() 则是线程 建议 让出 CPU 资源,但最终是否让出以及让给谁,是由线程调度器决定的。

🔥🌟Q: 守护线程和用户线程有什么区别呢?

思考过程:

这个问题考察对 Java 中守护线程和用户线程的理解,需要从生命周期和用途上进行区分。

  • 用户线程: 独立于 JVM,执行程序主要业务逻辑。
  • 守护线程: 依赖于用户线程,为用户线程提供后台服务。
  • 生命周期: 当所有用户线程结束时,守护线程也会随 JVM 退出。
  • 举例: 垃圾回收线程。

回答提问:

好的面试官,Java 中的线程可以分为两种类型:守护线程(Daemon Thread)用户线程(User Thread)。它们之间最核心的区别在于生命周期和用途

用户线程是我们程序中创建的普通线程,它的生命周期独立于 JVM。用户线程主要负责执行程序的主要业务逻辑。只有当所有的用户线程都执行完毕后,JVM 才会退出。

守护线程则是一种特殊的线程,它的生命周期依赖于用户线程。守护线程通常用于为用户线程提供后台服务,比如垃圾回收、日志记录等。当一个 JVM 中所有的用户线程都执行结束时,无论守护线程是否还在运行,JVM 都会强制退出,守护线程也会随之终止。

一个典型的例子是 垃圾回收线程,它就是一个守护线程。它在后台默默地为用户线程提供内存管理服务,当所有的用户线程都不再需要内存时,垃圾回收线程就会结束它的使命,随着 JVM 的退出而结束。

我们可以通过 Thread 类的 setDaemon(true) 方法将一个线程设置为守护线程。需要注意的是,这个方法必须在线程启动之前调用。

🔥💡 Q:能简单介绍一下什么是协程吗?Java能使用协程吗?

思考过程

这个问题考察对 协程 (Coroutine) 的基本概念和特点的理解。需要从它与线程的对比入手,说明其轻量级、用户态调度和非阻塞的特性。

  • 定义:一种比线程更轻量级的执行单元,可以在执行过程中暂停和恢复。
  • 与线程的比较:用户态调度 vs. 内核态调度,效率更高。
  • 特点:轻量级、非抢占式调度、异步化编程。

回答提问

好的面试官,协程可以理解为一种 比线程更轻量级的执行单元。它允许我们在代码执行的过程中 暂停 当前的执行,并且在稍后的某个时间点 恢复 执行,而 不需要像线程那样进行阻塞

与传统的线程相比,协程的一个重要特点是它的 调度是由用户程序自身控制的,而不是由操作系统内核来调度。这意味着协程的切换发生在 用户态,避免了线程切换时涉及到的 内核态上下文切换的开销,因此 效率更高

协程还具有以下特点:首先,它非常 轻量级,创建和切换的成本远低于线程。其次,它的调度是 非抢占式 的,协程的暂停和恢复由程序员显式地通过类似 yieldawait 的操作来控制,这样可以避免多线程环境下的线程中断问题。最后,协程非常适合用于 异步化编程,它可以让异步代码写起来更像同步代码,使得代码结构更加清晰简洁。

Java 在早期版本中并没有原生支持协程。但是,从 Java 19 开始,通过 Project Loom 项目引入了 虚拟线程 (Virtual Threads),并且这个特性在 Java 21 中得到了正式确认。

虚拟线程可以被认为是 Java 对协程的一种实现。虽然它的底层实现原理可能与传统的协程有所不同,但它确实为 Java 开发者提供了 类似协程的功能,比如能够以非常低的成本创建大量的并发任务,并且能够有效地 提高程序的并发性能。因此,可以说 Java 现在通过虚拟线程的方式支持了类似协程的功能,使得开发者能够更容易地编写高效的并发程序。

线程间通信(主要问线程间同步,偶尔会问JMM、Happens-before等原理)

🔥⭐ Q: 线程之间如何进行同步?你知道有哪些常用的同步方式?

思考过程:

这个问题考察对 线程同步概念和实现方式的掌握。可以从互斥同步和非阻塞同步两个方面进行回答,并列举常用的同步工具。

  • 同步定义: 控制不同线程间操作发生的相对顺序的机制,保证共享数据访问的正确性。
  • 互斥同步: 保证同一时刻只有一个线程访问共享资源。
  • 非阻塞同步: 不会阻塞线程,通常基于原子操作实现。
  • 协作同步: 多个线程为了完成共同的任务而进行的协调。

回答提问:

好的面试官,线程同步是指控制程序中不同线程之间操作发生的相对顺序的机制,目的是为了保证多个线程在并发访问共享资源时能够正确地协作,避免出现数据竞争等问题。通常我们需要显式地指定哪些方法或者代码块需要在线程之间互斥地执行。

常用的线程同步方式可以分为以下几类:

互斥同步:这种方式保证在同一时刻只有一个线程能够访问被保护的共享资源。主要的实现方式有:

  • synchronized 关键字:这是 Java 内置的互斥同步机制,可以用于修饰方法或代码块,实现对对象或类的加锁。
  • ReentrantLock:java.util.concurrent.locks 包提供的可重入锁,提供了比 synchronized 更灵活的加锁和释放锁的方式,并且支持公平锁等特性。

非阻塞同步:这种方式不依赖于传统的互斥锁,而是尝试使用原子操作来完成同步。主要的实现方式有:

  • CAS(Compare-and-Swap)原子类:java.util.concurrent.atomic 包下提供了诸如 AtomicInteger、AtomicReference 等原子类,它们利用 CAS 操作来保证对变量的原子更新,避免了锁的开销。

协作同步:这种方式主要用于多个线程之间为了完成某个共同的任务而进行的协调和配合。主要的工具类有:

  • CountDownLatch(倒数门闩):允许一个或多个线程等待其他线程完成操作。
  • CyclicBarrier(循环栅栏):允许多个线程互相等待,直到所有线程都到达某个屏障点,然后这些线程才能继续一起执行。
  • Phaser(阶段同步器):比 CountDownLatch 和 CyclicBarrier 更灵活的同步器,可以控制多个阶段的同步。
  • Semaphore(信号量):控制对共享资源的并发访问数量。

选择哪种同步方式取决于具体的并发场景和需求。

🔥🌟Q: 你能简单介绍一下 Java 内存模型(JMM)吗?它主要解决什么问题?

思考过程:

这个问题考察对 Java 内存模型(JMM)的理解,需要知道 JMM 的作用以及它如何解决并发问题。

  • JMM 定义: 一套规范,描述了 Java 程序中各种变量(包括实例字段、静态字段和构成数组对象的元素)的访问规则,以及在 JVM 中将变量存储到内存和从内存中取出变量这样的底层细节。
  • 解决问题: 主要解决由于 CPU 缓存、指令重排序等带来的可见性、原子性和有序性问题,保证并发程序的正确性。

回答提问:

好的面试官,Java 内存模型(Java Memory Model,简称 JMM) 是一套规范,它描述了 Java 程序中各种变量(包括实例字段、静态字段和构成数组对象的元素)的访问规则,以及在 JVM 中将变量存储到内存和从内存中取出变量这样的底层细节。

JMM 的主要目标是解决在并发环境下由于 CPU 缓存、指令重排序等优化手段带来的可见性、原子性和有序性问题,从而保证多线程程序的正确性。

简单来说,JMM 定义了线程如何与主内存以及自己的工作内存进行交互。每个线程都有自己的工作内存,其中存储了该线程使用的变量的副本。线程对变量的所有操作(读取、赋值等)都必须在自己的工作内存中进行,而不能直接读写主内存中的变量。线程之间变量值的传递需要通过主内存来完成。

JMM 还规定了一系列的规则,比如 Happens-Before 原则,来保证在并发环境下操作的可见性和有序性,避免出现数据不一致等问题。通过遵循 JMM 的规范,我们可以编写出在多线程环境下能够正确运行的 Java 程序。

🔥🌟Q: 什么是 Java 中的指令重排?它会对多线程编程产生什么影响?

思考过程:

这个问题考察对 指令重排的理解及其在多线程环境下的影响。需要区分编译器重排和处理器重排,并说明其可能导致的问题以及 Java 如何应对。

  • 指令重排定义: 编译器和处理器为了优化性能,在不改变单线程程序语义的情况下,对指令执行顺序进行调整的过程.
  • 类型: 编译器重排、CPU 重排、内存系统重排。
  • 多线程影响: 可能导致线程间操作不同步或不可见,引发并发问题。
  • Java 应对: JMM 和相关机制(如 volatile 和 synchronized)限制重排。

回答提问:

好的面试官,指令重排指的是在 Java 程序执行过程中,为了提高性能,Java 编译器和处理器可能会对代码的执行顺序进行调整,只要在单线程环境下不改变程序的语义(即最终的执行结果保持一致)。

指令重排主要有三种类型:

  • 编译器重排:编译器在生成字节码时,会根据优化策略调整代码的顺序。
  • CPU 重排:处理器在执行指令时,可能会对指令的顺序进行调整,以更高效地利用 CPU 资源,例如通过指令流水线和多核并行执行。
  • 内存系统重排:不同线程访问共享内存时,内存系统可能会对内存操作的顺序进行调整。

单线程环境下,指令重排通常不会有问题,因为它可以保证程序的最终结果不变。但是,在多线程环境下,指令重排可能会导致线程之间的操作出现不同步或不可见的现象,从而引发严重的并发问题。例如,一个线程写入共享变量的操作可能会被重排到读取该变量的操作之后,导致另一个线程读取到的是过期的数据。

为了解决指令重排在多线程环境下可能带来的问题,Java 提供了内存模型(JMM)以及相关的机制,比如 volatile 关键字和 synchronized 关键字,来限制这种重排行为,确保并发操作的正确性。例如,volatile 关键字通过插入内存屏障来禁止特定情况下的指令重排,保证了可见性和一定的有序性。

🔥🌟Q: 什么是 Happens-Before 原则?你能举几个重要的 Happens-Before 规则吗?

思考过程:

这个问题考察对 Happens-Before 原则的理解,需要知道其作用以及几个关键的规则。

  • 定义: Java 内存模型中的核心概念,用于定义多线程程序中操作的可见性和顺序性。
  • 作用: 确保线程间的操作是有序的,避免由于重排序或线程间数据不可见导致的并发问题。
  • 重要规则:程序顺序规则。解锁规则。volatile 变量规则。传递规则。线程启动规则。

回答提问:

好的面试官,Happens-Before 原则是 Java 内存模型(JMM)中的一个核心概念,它用于定义多线程程序中两个操作之间的可见性和顺序性。如果一个操作 "happens-before" 另一个操作,这意味着第一个操作的执行结果对第二个操作是可见的,并且第一个操作的执行顺序先于第二个操作。Happens-Before 原则是 JMM 保证多线程程序正确性的重要基石,它可以帮助我们理解在并发环境下哪些操作是安全的,哪些操作可能会导致问题。

几个比较重要的 Happens-Before 规则包括:

  • 程序顺序规则:在一个线程内部,按照代码的先后顺序,书写在前面的操作 happens-before 于书写在后面的操作。这保证了在单线程环境下的执行顺序。
  • 解锁规则:对一个锁的解锁操作 happens-before 于随后对这个锁的加锁操作。这意味着当一个线程释放锁后,另一个线程获取到这个锁,能够看到之前线程对共享变量的修改。
  • volatile 变量规则:对一个 volatile 变量的写操作 happens-before 于后面对这个 volatile 变量的读操作。这保证了 volatile 变量的写操作结果对于后续的读操作是可见的。
  • 传递规则:如果操作 A happens-before 操作 B,并且操作 B happens-before 操作 C,那么操作 A happens-before 操作 C。这个规则允许我们推导出更复杂的 happens-before 关系。
  • 线程启动规则:调用 Thread 对象的 start() 方法 happens-before 于新启动的线程中的任何操作。这意味着新启动的线程能够看到启动它的线程在启动之前所做的任何操作。

这些规则共同构建了 Java 并发编程中操作的可见性和顺序性保证。

volatile关键字(很重要)

🔥⭐Q: volatile 关键字的作用是什么?

思考过程:

这个问题考察对 volatile 关键字作用的理解,需要从可见性和禁止指令重排两个方面进行说明,并提及典型的使用场景。

  • 作用: 保证可见性,禁止指令重排。
  • 可见性原理: 写操作立即刷新到主内存,读操作从主内存读取。
  • 禁止重排原理: 插入内存屏障。
  • 典型使用场景: 状态标志、双重检查锁定。

回答提问:

好的面试官,volatile 关键字在 Java 中主要有两个作用:保证可见性禁止指令重排优化

可见性的角度来看,当一个共享变量被 volatile 修饰时,任何线程对其值的修改都会立即刷新到主内存中,并且当其他线程需要读取该变量的值时,会强制从主内存中读取最新的值。这样就避免了由于线程本地缓存不一致而导致的读取到“旧值”的情况。

具体来说,当写一个 volatile 变量的时候,JMM 会把该线程对应本地内存中的共享变量值刷新到主内存。当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。

禁止指令重排的角度来看,volatile 关键字通过在指令序列中插入内存屏障来禁止特定情况下的指令重排序,从而保证程序的执行符合预期。具体来说,当对 volatile 变量进行写操作时,会在写操作后插入一个写屏障,保证之前的操作不会被重排序到写操作之后;当对 volatile 变量进行读操作时,会在读操作前插入一个读屏障,保证之后的操作不会被重排序到读操作之前。

volatile 关键字的典型使用场景主要有两种:一种是用作状态标志,例如用 volatile 修饰一个布尔类型的标志位,用于指示线程是否需要停止;另一种是在双重检查锁定(Double-Checked Locking)中使用,用于延迟初始化单例对象,确保在多线程环境下也能安全地创建单例实例。

🔥🌟Q: volatile 可以保证原子性吗?

思考过程:

这个问题进一步考察对 volatile 作用的理解,需要明确 volatile 只能保证可见性,不能保证原子性。

  • volatile 保证可见性。
  • volatile 不能保证原子性。
  • 需要原子性操作仍需同步机制或原子类。

回答提问:

好的面试官,volatile 并不能使得一个非原子操作变成原子操作

volatile 关键字主要保证了变量的可见性,即一个线程对 volatile 变量的修改会立即对其他线程可见。它还具有禁止指令重排序的作用。

然而,对于需要原子性的操作,比如复合操作(包含多个步骤的操作),volatile 是无法提供保证的。例如,像 count++ 这样的操作,即使 count 被声明为 volatile,它仍然包含读取 count 的值、将值加 1,然后将结果写回 count 这三个步骤。在多线程环境下,这三个步骤可能会被其他线程的操作所中断,导致最终的结果不正确。

如果需要保证某个操作的原子性,我们需要使用同步机制(例如 synchronized 关键字或 Lock)或者使用 AtomicInteger原子类来实现。这些机制能够确保在操作执行期间,没有其他线程会干扰,从而保证了操作的完整性。

🔥🌟Q: volatile 变量和 atomic 变量有什么不同?

思考过程:

这个问题考察对 volatile 关键字和原子类之间区别的理解,需要从可见性和原子性两个方面进行对比。

  • volatile: 保证可见性,不保证原子性。
  • atomic 变量: 提供原子操作,保证原子性。
  • CAS 机制: 原子类通常基于 CAS 实现。

回答提问:

好的面试官,volatile 变量和 atomic 变量都是用于处理并发问题的机制,但它们之间有着重要的区别:

volatile 变量主要确保变量的可见性。当一个线程修改了一个 volatile 变量的值后,这个新值会立即同步到主内存,并且当其他线程读取这个 volatile 变量时,会强制从主内存中读取最新的值。此外,volatile 还能禁止指令重排序。但是,volatile不能保证操作的原子性。例如,像 count++ 这样的操作,即使 count 被声明为 volatile,在多线程环境下仍然不是线程安全的,因为它实际上包含了读取、增加和写回三个独立的操作,这些操作之间可能会被其他线程打断。

atomic 变量(例如 AtomicIntegerAtomicLong 等)则提供了一组原子操作,可以保证对变量的操作是原子性的。这些原子类通常是基于底层的 CAS(Compare-And-Swap) 机制实现的,可以在不使用传统锁的情况下实现线程安全的更新操作。例如,AtomicInteger 提供的 incrementAndGet() 方法会原子性地将整数值加一,不会被其他线程中断,从而保证了操作的完整性。

简单来说,volatile 保证了变量在多线程之间的可见性,但不能保证复合操作的原子性。而 atomic 变量则通过提供原子操作来保证操作的原子性,同时也具备可见性。因此,如果需要保证对变量的复合操作是线程安全的,通常会选择使用 atomic 变量而不是 volatile 变量。

synchronized关键字(很重要)

🔥⭐Q: synchronized 关键字的作用是什么?

思考过程:

这个问题考察对 synchronized 关键字基本作用的理解,需要说明它如何保证线程安全。

  • 作用: 保证被修饰方法或代码块在任意时刻只能有一个线程执行。
  • 保证: 保护共享资源的一致性,确保线程间的操作顺序,避免竞态条件。
  • 竞态条件: 多线程同时访问和修改共享资源导致结果不确定或错误。

回答提问:

好的面试官,synchronized 关键字在 Java 中主要的作用是保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行,从而达到线程安全的目的。

具体来说,synchronized 关键字可以:

  1. 保护共享资源在多线程环境中的一致性:通过限制对共享资源的并发访问,确保多个线程不会同时修改同一个资源,从而避免数据损坏或不一致的情况。
  2. 确保线程间的操作顺序,避免出现竞态条件:竞态条件指的是当多个线程同时访问并修改同一个共享资源时,由于它们执行顺序的不确定性,可能会导致最终的结果不确定或者错误。synchronized 可以通过强制线程串行化地访问共享资源来避免这种情况。

补充一点,早期的 synchronized 依赖于操作系统的实现,属于重量级锁,性能相对较低。但在 JDK 1.6 之后,Java 对 synchronized 进行了大量的优化,引入了偏向锁、轻量级锁等机制,使得其在很多情况下性能也能够得到很好的保障。

🔥⭐Q: 你在项目中是如何使用 synchronized 关键字的?能举个例子吗?

思考过程:

这个问题考察对 synchronized 关键字实际应用场景的理解,需要结合项目经验说明其使用方式。

  • 使用方式: 修饰实例方法、修饰静态方法、修饰代码块。
  • 项目应用: 结合具体场景,例如保护共享变量的更新、控制对临界资源的访问等。

回答提问:

好的面试官,我在项目中使用 synchronized 关键字主要有以下几种方式:

  1. 修饰实例方法:当需要保护某个对象实例的方法不被多个线程同时访问时,我会使用 synchronized 关键字修饰该方法。这时,锁是当前对象实例。例如,在一个计数器类中,为了保证 increment() 方法的线程安全,可以将其声明为 synchronized。
  2. 修饰静态方法:如果需要保护类的静态资源(例如静态变量)不被多个线程同时访问时,我会使用 synchronized 关键字修饰静态方法。这时,锁是当前类本身。这会作用于该类的所有对象实例。
  3. 修饰代码块:当只需要对方法中的某一段代码进行同步控制,而不是整个方法时,我会使用 synchronized 关键字修饰代码块,并指定一个锁对象。这个锁对象可以是当前实例 (this)、类对象 (ClassName.class) 或者任何其他的对象实例。这种方式可以更细粒度地控制同步范围,提高并发性能。例如,在更新一个共享变量时,可能只需要对更新操作的那几行代码进行同步。

在项目中,我通常会根据需要保护的共享资源以及并发访问的场景来选择合适的 synchronized 使用方式。例如,在处理用户订单时,为了保证订单状态更新的原子性,可能会使用 synchronized 关键字修饰更新订单状态的方法。又比如,在实现一个缓存组件时,为了保证多个线程能够安全地访问和修改缓存数据,可能会使用 synchronized 关键字修饰相关的操作方法或者使用 synchronized 代码块来保护缓存的数据结构。

需要注意的是,在使用 synchronized 时,要尽量减小同步代码块的范围,避免长时间持有锁,以减少线程阻塞的可能性,提高程序的并发性能。另外,也要避免使用字符串常量作为锁对象,因为字符串常量池的存在可能会导致意想不到的锁竞争问题。

🔥⭐Q: 你了解 synchronized 的底层实现原理吗?能聊一下吗?

思考过程:

这个问题考察对 synchronized 底层实现机制的理解,需要知道它依赖于 JVM 的 Monitor 和对象头,以及加锁和释放锁的过程。

  • 实现原理: 依赖 JVM 的 Monitor 和对象头。
  • 修饰方法: 通过方法访问标志中的 ACC_SYNCHRONIZED 实现。
  • 修饰代码块: 通过 monitorenter 和 monitorexit 指令实现。
  • Monitor: 每个对象都有一个关联的 Monitor,用于控制线程的同步访问。
  • 可重入性: 通过 Monitor 的入口计数器实现。

回答提问:

好的面试官,synchronized 的底层实现原理主要依赖于 JVM 的 Monitor(监视器锁)和对象头(Object Header)

synchronized 关键字修饰一个方法时,JVM 会在方法的访问标志中设置 ACC_SYNCHRONIZED 标志。当线程访问带有这个标志的方法时,JVM 会隐式地获取该方法所在对象的 Monitor 锁,执行完方法后会隐式地释放 Monitor 锁。整个加锁和释放锁的过程是由 JVM 自动完成的。

synchronized 关键字修饰一个代码块时,JVM 会在同步代码块的起始位置插入 monitorenter 指令,在同步代码块的结束位置(包括正常结束和异常结束)插入 monitorexit 指令。当线程执行 monitorenter 指令时,会尝试获取 Monitor 的所有权。如果 Monitor 未被其他线程持有,当前线程可以成功获取,并将 Monitor 的入口计数器设置为 1。如果 Monitor 已经被其他线程持有,当前线程会被阻塞,进入 Monitor 的 EntryList 队列等待。

synchronized可重入性就是通过 Monitor 内部维护的一个入口计数器来实现的。当一个线程再次尝试获取它已经持有的 Monitor 时,计数器会递增,而在释放锁时,计数器会递减。只有当计数器的值为 0 时,才表示线程完全释放了 Monitor 锁,其他等待的线程才有机会获取锁。

每个 Java 对象都关联着一个 Monitor。Monitor 可以理解为一个同步工具,它负责管理试图获取对象锁的线程。当多个线程竞争同一个对象的 Monitor 时,只有一个线程能够成功获取,其他线程则进入等待状态(位于同步队列中),直到持有锁的线程释放锁后,它们才有机会再次竞争。

我们可以通过反汇编命令 javap 查看字节码文件来观察 synchronized 的实现细节,例如 monitorentermonitorexit 指令。总的来说,无论是修饰方法还是代码块,synchronized 的本质都是对对象监视器 Monitor 的获取和释放。

🔥🌟Q: 多线程中 synchronized 锁升级的原理是什么?

思考过程:

这个问题考察对 synchronized 锁优化机制的理解,需要知道锁升级的过程以及不同锁状态的特点和适用场景。

  • 锁升级原因: 减少获取和释放锁的性能消耗。
  • 升级过程: 偏向锁 -> 轻量级锁 -> 重量级锁。
  • 偏向锁: 偏向第一个获取锁的线程,无竞争开销小。
  • 轻量级锁: CAS + 自旋,减少阻塞开销,适用于竞争不激烈且同步块执行快的场景。
  • 重量级锁: 依赖操作系统 Mutex,竞争激烈时阻塞线程,适用于同步块执行时间较长的场景。

回答提问:

好的面试官,为了减少 synchronized 锁在多线程环境下的性能开销,尤其是在竞争不激烈的情况下,JDK 1.6 之后引入了锁升级的机制。synchronized 锁的状态会根据线程竞争的激烈程度进行升级,主要经历了以下几个阶段:偏向锁轻量级锁重量级锁。这个过程是单向的,只能升级不能降级。

  1. 偏向锁:当一个线程第一次获取锁时,JVM 会将该线程标记为锁的“偏向”状态。在锁对象的对象头的 Mark Word 中会记录持有偏向锁的线程 ID。后续如果该线程再次访问这个同步块,会先检查 Mark Word 中记录的线程 ID 是否是自己的 ID。如果是,则可以直接获得锁,几乎没有额外的开销。偏向锁适用于只有一个线程访问同步块的场景,可以减少锁的获取和释放操作。
  2. 轻量级

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

在准备暑期实习时,从等各个平台搜集了上千篇真实面经,自己整理得到了面试题,根据题目在面试题中出现的频率以及我自己、交流群、好朋友面试被问到的频率进行了分类整理,所有内容经过科学分类与巧妙标注,针对性强: 得到⭐🌟💡三种级别的,其中⭐为最高频的题目(类似神哥总结的高频八股),只是我自己整理出来的这部分更多一些,🌟为中高频题目(冲击大厂的uu们建议看)、💡为低频题,可以作为补充,时间充裕再看!

全部评论

相关推荐

评论
2
7
分享

创作者周榜

更多
牛客网
牛客企业服务