2025合集Java最全八股文总结!
Java并发与JVM面试摘要 并发编程 进程与线程:进程是资源分配单位,线程是CPU调度单位。线程共享进程资源,切换开销小,但崩溃可能影响整个进程。 并行与并发:并发是交替处理任务(如单核多线程),并行是同时执行(多核)。 线程创建方式:继承Thread、实现Runnable/Callable(支持返回值)、线程池(推荐,资源复用)。 线程状态:NEW、RUNNABLE、BLOCKED、WAITI
1、进程和线程的区别 ⭐⭐⭐⭐
定义:
- 进程是操作系统资源分配的基本单位,是正在运行的程序的实例。
- 线程是进程内的执行单元,是CPU调度的基本单位。
资源分配:
- 进程拥有独立的内存空间和资源,进程间资源隔离。
- 线程共享进程的内存空间和资源,线程间通信更方便。
上下文切换:
- 进程切换开销大,需要保存和恢复整个进程的状态。
- 线程切换开销小,只需保存和恢复线程的执行状态。
独立性:
- 进程间相互独立,一个进程崩溃不会影响其他进程。
- 线程共享进程资源,一个线程崩溃可能导致整个进程崩溃。
2、并行和并发有什么区别? ⭐⭐⭐⭐
-
并发是同一时间段内处理多件事情(如多线程轮流使用CPU),适合I/O密集型任务。
-
并行是同一时刻执行多件事情(如多核CPU同时运行多个线程),适合CPU密集型任务。
3、 创建线程的四种方式 ⭐⭐⭐⭐
-
继承Thread类: 重写run()方法,直接调用start()启动线程。 缺点:单继承限制,扩展性差。
-
实现Runnable接口:实现run()方法,通过Thread类启动线程。 优点:避免单继承限制,适合资源共享。
-
实现Callable接口:实现call()方法,可以返回结果和抛出异常,通常与FutureTask或线程池结合使用。
-
线程池创建线程:通过线程池管理线程,避免频繁创建和销毁线程的开销。 优点:资源复用、控制并发数、提供定时任务等功能。
-
总结:项目中通常使用线程池创建线程,因为线程池可以更好地管理线程资源,避免资源浪费和性能问题。
4、runnable 和 callable 有什么区别 ⭐⭐⭐⭐
返回值:
- Runnable 的 run() 方法没有返回值。
- Callable 的 call() 方法有返回值,返回类型是泛型。
异常处理:
- Runnable 的 run() 方法不能抛出受检异常,只能在内部处理。
- Callable 的 call() 方法可以抛出受检异常。
使用场景:
- Runnable 适合简单的异步任务,不需要返回值。
- Callable 适合需要返回结果或抛出异常的任务。
执行方式:
- Runnable 通过 Thread 或线程池执行。
- Callable 通常与 Future 或线程池结合,通过 FutureTask.get() 获取结果(此方法会阻塞主线程)。
5、 线程的 run()和 start()有什么区别? ⭐⭐⭐⭐
功能:
- start() 用于启动新线程,JVM 会自动调用 run() 方法执行任务。
- run() 封装了线程要执行的逻辑代码,直接调用不会启动新线程。
调用次数:
- start() 只能调用一次,多次调用会抛出异常。
- run() 可以调用多次。
执行方式:
- start() 是异步执行,不会阻塞主线程。
- run() 是同步执行,会阻塞当前线程。
实际应用:
- start() 用于启动新线程,执行异步任务(如并发处理、耗时操作)。
- run() 通常用于测试或直接执行任务逻辑,不启动新线程。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
6、线程包括哪些状态,状态之间是如何变化的 ⭐⭐⭐⭐⭐
线程的六种状态:
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、有时限等待(TIMED_WAITING)、终结(TERMINATED)。
状态切换:
- NEW → RUNNABLE:调用 start() 方法,线程进入可运行状态。
- RUNNABLE → TERMINATED:线程代码执行完毕,进入终止状态。
- RUNNABLE → BLOCKED:线程获取锁失败,进入阻塞状态;锁释放后恢复为可运行状态。
- RUNNABLE → WAITING:调用 wait()、join() 方法,线程进入等待状态;被唤醒后恢复为可运行状态。
- RUNNABLE → TIMED_WAITING:调用 sleep(long)、wait(long)方法,线程进入有时限等待状态;超时后恢复为可运行状态。
实际应用:
- 多线程竞争锁时会发生 RUNNABLE → BLOCKED 切换。
- 线程间协作或等待资源时会发生 RUNNABLE → WAITING/TIMED_WAITING 切换。
7、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行? ⭐⭐⭐
可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
比如说:使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成
8、notify()和 notifyAll()有什么区别? ⭐⭐⭐⭐
-
notifyAll:唤醒所有wait的线程
-
notify:只随机唤醒一个 wait 线程
9、在 java 中 wait 和 sleep 方法的不同? ⭐⭐⭐⭐⭐
共同点:wait() 和 sleep() 都会让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
方法归属:
- wait() 是 Object 的成员方法,每个对象都有。
- sleep() 是 Thread 的静态方法。
醒来时机:
- wait() 可以在指定时间后自动醒来,也可以被 notify() 或 notifyAll() 唤醒。
- sleep() 只能超时自动醒来或被中断唤醒。
锁特性:
- wait() 调用前必须先获取对象锁,执行后会释放锁。
- sleep() 调用前无需获取锁,执行期间也不会释放锁。
使用场景:
- wait() 用于线程间通信,通常与 synchronized 配合使用。
- sleep() 用于让线程暂停执行一段时间。
10、如何停止一个正在运行的线程? ⭐⭐⭐
有三种方式可以停止线程
- 使用退出标志:定义一个布尔类型的标志位,线程在执行过程中会不断检查该标志位的状态。若标志位被设置为特定值,线程就会停止执行。这是一种较为安全且推荐使用的方法。
- 使用stop方法:强行终止,可能会导致线程持有的锁被突然释放(不推荐,方法已作废)
- 使用interrupt方法:线程在运行过程中可以通过检查自身的中断状态来决定是否停止执行。
6.2丶线程并发安全
1、 讲一下synchronized关键字的底层原理? ⭐⭐⭐⭐⭐
synchronized 是 Java 中用于实现线程同步的关键字,它的底层原理是基于 JVM 的 Monitor(监视器锁)实现的,每个 Java 对象都可以关联一个 Monitor。synchronized 通过操作对象头中的锁标志位来获取和释放锁,若锁标志位显示对象处于无锁状态,线程会尝试修改锁标志位以获取锁,同时关联对应的 Monitor,之后进入同步代码块执行。
Monitor 内部维护了三个关键部分:
- Owner:当前持有锁的线程。
- EntryList:等待获取锁的线程队列(阻塞状态)。
- WaitSet:调用了 wait() 方法而进入等待状态的线程队列。
当线程尝试获取锁时,如果锁已被占用,线程会进入 EntryList 等待;当锁释放时,JVM 会从 EntryList 中唤醒线程竞争锁。synchronized 是一种悲观锁,假设并发环境下会发生冲突,因此每次访问共享资源时都会加锁。
在 JDK 1.6 之后,synchronized 引入了锁升级机制(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),以优化性能。不过,由于 synchronized 依赖于 JVM 级别的 Monitor,它的性能相对较低,尤其是在高并发场景下。
2、了解synchronized锁升级吗? ⭐⭐⭐⭐
-
无锁
对象刚被创建时,Mark Word 的锁标志位为 01,偏向锁标志位为 0,表示无锁状态。 -
偏向锁
适用场景:单线程反复访问同步代码块(无竞争)。
实现原理:对象头记录第一个获取锁的线程ID。后续该线程进入同步代码块时,只需检查线程ID是否一致,无需任何锁操作(直接通行)。
优点:无竞争时几乎零开销(仅内存读取)。
缺点:一旦有其他线程竞争,立即升级为轻量级锁。 -
轻量级锁
适用场景:低竞争(如线程交替执行,无并发冲突)。
实现原理:使用CAS尝试修改对象头的锁标记。成功:获取锁;失败:自旋(循环等待)一小段时间。若自旋失败,升级为重量级锁。
优点:避免线程阻塞(用户态解决竞争),开销小于重量级锁。
缺点:自旋消耗CPU,高竞争下性能下降。 -
重量级锁
适用场景:高竞争(多线程同时抢锁)。
实现原理:基于操作系统底层的互斥量(mutex)实现。未抢到锁的线程会被挂起,进入等待队列,由操作系统调度唤醒。
优点:严格保证线程安全,适合高并发场景。
缺点:涉及用户态到内核态的切换,性能开销最大。 -
锁升级流程
无锁 → 偏向锁:首次线程访问同步块时,对象头记录线程ID。
偏向锁 → 轻量级锁:其他线程尝试竞争时,撤销偏向锁,改用CAS自旋。
轻量级锁 → 重量级锁:自旋失败(如自旋次数超过阈值,默认10次)或竞争加剧时升级。
⚠️ 注意:锁升级是单向的(不可降级),由JVM自动完成。
3、谈谈 JMM(Java 内存模型) ⭐⭐⭐⭐
Java内存模型是Java虚拟机规范中定义的一种非常重要的内存模型。它的主要作用是描述Java程序中线程共享变量的访问规则,以及这些变量在JVM中是如何被存储和读取的,涉及到一些底层的细节。
-
这个模型有几个核心的特点。首先,所有的共享变量,包括实例变量和类变量,都被存储在主内存中,也就是计算机的RAM。需要注意的是,局部变量并不包含在内,因为它们是线程私有的,所以不存在竞争问题。
-
其次,每个线程都有自己的工作内存,这里保留了线程所使用的变量的工作副本。这意味着,线程对变量的所有操作,无论是读还是写,都必须在自己的工作内存中完成,而不能直接读写主内存中的变量。
-
最后,不同线程之间不能直接访问对方工作内存中的变量。如果线程间需要传递变量的值,那么这个过程必须通过主内存来完成。
4、 什么是CAS? ⭐⭐⭐⭐
CAS(Compare And Swap) 是一种乐观锁的实现方式。该操作涉及三个操作数:内存位置(V)、预期原值(A)和新值(B)。其核心思想是,仅当内存位置 V 中的值与预期原值 A 相同时,才会将该内存位置的值更新为新值 B;若不同,则不进行更新操作。
应用场景:
- AQS 框架:用于实现线程的排队和锁的获取与释放。
- AtomicXXX 类:是Java提供的原子操作工具类,如 AtomicInteger、AtomicLong 等
- 集合框架:ConcurrentHashMap、CopyOnWriteArrayList等使用CAS优化线程安全操作。
- 轻量级锁(自旋锁):Java的轻量级锁在竞争时通过CAS尝试修改对象头中的锁标记。
优点:
- 无锁并发:CAS 是一种无锁算法,避免了传统锁机制带来的线程阻塞和上下文切换开销,在高并发场景下能显著提升性能。
- 原子性:CAS 操作是原子性的,这意味着在多线程环境中,该操作要么全部完成,要么完全不执行,不会出现部分更新的情况,保证了数据的一致性。
缺点:
- ABA 问题:若一个值从 A 变为 B,再从 B 变回 A,CAS 操作会认为该值没有发生变化,从而继续执行更新操作。可以通过版本号或时间戳解决。
- 自旋开销:当 CAS 操作失败时,线程通常会进行自旋重试。若长时间重试都无法成功,会消耗大量的 CPU 资源。
5、乐观锁和悲观锁 ⭐⭐⭐⭐
乐观锁:
- 思想:假设在大多数情况下不会发生冲突,允许多个线程同时访问共享资源,只有在更新数据时才会检查是否有冲突。
- 实现方式:通常通过版本号或时间戳来实现。
- 优点:在高并发、低冲突的场景下,性能较好。
- 缺点:在冲突较多的情况下,可能会导致频繁的重试。
悲观锁:
- 思想:假设在大多数情况下会发生冲突,因此在访问共享资源时,先获取锁,确保其他线程无法修改数据,直到当前线程释放锁。
- 实现方式:通常通过 synchronized 关键字或 ReentrantLock 来实现。
- 优点:在冲突较多的场景下,可以有效地保证数据的一致性。
- 缺点:性能开销较大,可能会导致线程阻塞。
6、谈谈你对 volatile 的理解 ⭐⭐⭐⭐
volatile 是 Java 中的一个关键字,用于修饰类的成员变量或静态成员变量,主要解决多线程环境下的可见性和有序性问题。
功能:
- 可见性:volatile修饰的变量在被修改后,会立即写回主内存,并且其他线程在读取该变量时,会直接从主内存中获取最新值,确保变量的修改对所有线程立即可见。
- 禁止指令重排序:Java 编译器和处理器为了提高性能,可能会对指令进行重排序,只要保证程序的最终执行结果和顺序执行的结果一致即可。但在多线程环境下,指令重排序可能会导致程序出现错误。volatile 修饰的变量会禁止编译器和处理器对其进行指令重排序,保证代码执行的有序性。
底层实现:
通过插入内存屏障来实现可见性和禁止指令重排序。
7、什么是AQS? ⭐⭐⭐⭐
AQS (AbstractQueuedSynchronizer) 是 Java 并发包中的一个核心框架,它为构建锁和其他同步器提供了基础实现。许多 Java 并发工具如 ReentrantLock、Semaphore、CountDownLatch 等都是基于 AQS 构建的。
核心思想:AQS 的核心是基于一个 volatile int 类型的同步状态变量(state)来表示同步状态,通过 CAS 操作来修改这个状态。当线程请求获取同步资源时,如果当前状态满足条件,则获取成功;否则,线程会被包装成一个节点(Node)并加入到一个双向队列(CLH 队列)中等待。当持有同步资源的线程释放资源时,会从队列中唤醒等待的线程,使其有机会获取同步资源。
应用场景:
AQS 定义了需要子类实现的抽象方法,具体同步器实现这些方法来定义自己的同步逻辑
- 实现阻塞式锁: ReentrantLock 就是基于 AQS 实现的独占锁,通过 AQS 来管理锁的获取和释放,实现线程的同步。
- 实现信号量:Semaphore 利用 AQS 来控制同时访问某个资源的线程数量,通过 acquire 和 release 方法来获取和释放信号量。
- 实现倒计时锁:CountDownLatch 通过 AQS 实现了一种线程等待机制,让一组线程等待其他线程完成一系列操作后再继续执行。
8、ReentrantLock的实现原理 ⭐⭐⭐
ReentrantLock 是 Java 并发包中的一个可重入锁实现,属于 API 层面的锁,与 synchronized 一样,都是悲观锁。
底层实现:
AQS:状态变量 state,初始值为 0,表示锁未被持有。被线程持有时,state 递增(支持重入),释放时递减,减为 0 时锁被完全释放。
FIFO 等待队列:未获取到锁的线程会被封装成 Node 节点进入队列阻塞,等待唤醒。
核心特性:
- 可重入性:线程可以重复获取同一把锁,重入次数通过 state变量记录。每次获取锁时,重入次数增加;每次释放锁时,重入次数减少。重入次数减到 0 时,锁完全释放。
- 核心方法:lock() 获取锁,unlock() 释放锁,tryLock() 尝试获取锁,lockInterruptibly()可中断获取锁。
公平锁与非公平锁:
- 非公平锁:默认模式,允许“插队”,提高吞吐量。
- 公平锁:按照线程等待顺序分配锁,避免线程饥饿。
9、 synchronized和ReentrantLock有什么区别 ? ⭐⭐⭐⭐⭐
好的,以下是优化后的对比表格:
ReentrantLock 与 synchronized 的核心差异
特性 | ReentrantLock | synchronized |
---|---|---|
公平性 | 支持公平/非公平锁(通过构造函数选择) | 仅支持非公平锁 |
可中断性 | 支持(lockInterruptibly() ) |
不支持 |
尝试锁 | 支持(tryLock() ) |
不支持 |
锁超时 | 支持(tryLock(timeout, unit) ) |
不支持 |
条件变量 | 支持多个 Condition 对象(精准唤醒) |
单个 wait() /notify() 队列 |
性能 | 高并发下表现更优 | 低竞争下 JVM 会优化(锁升级) |
关键差异解释
-
公平锁:
ReentrantLock
可通过new ReentrantLock(true)
创建公平锁,保证线程按请求顺序获取锁,避免饥饿;synchronized
无法实现公平性。 -
可中断锁:
lockInterruptibly()
允许线程在等待锁时响应中断,防止死锁。 -
尝试锁:
tryLock()
支持无阻塞获取锁,或通过超时参数避免长时间等待。 -
条件变量:
ReentrantLock
的Condition
支持多个等待队列(如生产者-消费者模式中的notFull
和notEmpty
),而synchronized
只能有一个等待队列。 -
性能:
- 低竞争:两者性能接近,JVM 会对
synchronized
进行锁升级优化。 - 高竞争:
ReentrantLock
的 CAS 操作避免线程挂起,性能更优。
- 低竞争:两者性能接近,JVM 会对
**10、死锁产生的条件是什么?**⭐⭐⭐⭐
死锁产生的条件包括以下四个必要条件:
- 互斥条件:资源一次只能被一个线程占用。
- 占有并等待:线程在持有资源的同时,还在等待获取其他资源。
- 不可抢占:线程已获取的资源不能被其他线程强行抢占。
- 循环等待:存在一个线程等待的循环链,每个线程都在等待下一个线程占用的资源。
如果 T1 持有 A 锁并等待 B 锁,而 T2 持有 B 锁并等待 A 锁,就会形成死锁。
避免死锁的方法:
- 破坏占有并等待条件:一次性申请所有资源。
- 破坏不可抢占条件:允许资源抢占。
- 破坏循环等待条件:对资源进行排序并按顺序申请。
**11、 如何进行死锁诊断?**⭐⭐⭐
jps:输出JVM中运行的进程状态信息
jstack:查看java进程内线程的堆栈信息
- 先通过jps来查看当前java程序运行的进程id,
输入:
jps
AI优化运行代码java运行
- 1
输出:
1234 MainClass
5678 AnotherClass
AI优化运行代码java运行
- 1
- 2
- 然后通过jstack来查看这个进程id,就能展示出来死锁的问题
输入:
jstack 1234
AI优化运行代码java运行
- 1
- 并且,可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。
输出:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000012345678 (object 0x0000000765432100, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000087654321 (object 0x0000000712345678, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadlockExample.lambda$main$1(DeadlockExample.java:22)
- waiting to lock <0x0000000765432100> (a java.lang.Object)
- locked <0x0000000712345678> (a java.lang.Object)
"Thread-0":
at DeadlockExample.lambda$main$0(DeadlockExample.java:12)
- waiting to lock <0x0000000712345678> (a java.lang.Object)
- locked <0x0000000765432100> (a java.lang.Object)
Found 1 deadlock.
AI优化运行代码java运行
12、 导致并发程序出现问题的根本原因是什么 ⭐⭐⭐
导致并发程序出现问题的根本原因 可以归结为 Java 并发编程的三大核心特性:原子性、可见性 和 有序性。
原子性:
-
定义:操作不可分割,要么全部执行成功,要么全部不执行。
-
问题:如 i++ 操作可能导致数据不一致。
-
解决方案:使用 synchronized、Lock 或 AtomicXXX 类。
可见性:
-
定义:一个线程对共享变量的修改能够及时被其他线程看到。
-
问题:线程可能读取到共享变量的旧值。
-
解决方案:使用 volatile、synchronized 或 Lock。
有序性:
-
定义:程序执行顺序按照代码的先后顺序执行。
-
问题:指令重排序可能导致多线程环境下程序行为异常。
-
解决方案:使用 volatile 或 synchronized 禁止指令重排序。
6.3丶线程池
1、说一下线程池的核心参数(线程池的执行原理知道嘛) ⭐⭐⭐⭐⭐
在线程池中一共有7个核心参数:
- corePoolSize 核心线程数目 - 池中会保留的最多线程数
- maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
- keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
- workQueue 队列 - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
- handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
拒绝策略有4种,当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。
执行原理
- 判断核心线程数:线程池首先会检查当前正在运行的线程数量是否小于核心线程数(corePoolSize)。如果是,则创建一个新的工作线程来执行该任务,即使此时线程池中可能有空闲的线程。
- 放入任务队列:如果当前正在运行的线程数量已经达到或超过核心线程数,线程池会尝试将任务放入任务队列中。如果任务队列未满,任务会被成功放入队列,等待有空闲的工作线程来执行。
- 创建新线程:如果任务队列已满,线程池会检查当前正在运行的线程数量是否小于最大线程数(maximumPoolSize)。如果是,则创建一个新的工作线程来执行该任务。
- 执行拒绝策略:如果当前正在运行的线程数量已经达到最大线程数,并且任务队列也已满,线程池会执行拒绝策略来处理新提交的任务。
2、线程池中有哪些常见的阻塞队列 ⭐⭐⭐⭐
ArrayBlockingQueue和LinkedBlockingQueue是Java中两种常见的阻塞队列,它们在实现和使用上有一些关键的区别。
- 首先,ArrayBlockingQueue是一个有界队列,它在创建时必须指定容量,并且这个容量不能改变。而LinkedBlockingQueue默认是无界的,但也可以在创建时指定最大容量,使其变为有界队列。
- 其次,它们在内部数据结构上也有所不同。ArrayBlockingQueue是基于数组实现的,而LinkedBlockingQueue则是基于链表实现的。这意味着ArrayBlockingQueue在访问元素时可能会更快,因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue则在添加和删除元素时可能更快,因为它不需要移动其他元素来填充空间。
- 另外,它们在加锁机制上也有所不同。ArrayBlockingQueue使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的。而LinkedBlockingQueue则使用两把锁,一把用于控制读操作,另一把用于控制写操作,这样可以提高并发性能。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
3、如何确定核心线程数 ⭐⭐⭐
CPU 密集型任务:
核心线程数 = CPU 核心数 + 1,充分利用 CPU 资源,减少空闲。如 8 核 CPU,设核心线程数为 9。
I/O 密集型任务:
理论公式:核心线程数 = CPU 核心数 * [1 + (I/O 等待时间 / CPU 计算时间)]。例如,I/O 等待 5ms,CPU 计算 1ms,4 核 CPU,则核心线程数为 24。
经验法则:常将核心线程数设为 CPU 核心数的 2 倍或更多,如 4 核 CPU,可设 8 - 16,再性能测试定最佳值。
混合型任务:
分析 CPU 与 I/O 占比,依占比大的部分参考对应类型设置,或先按经验值设初始值,经性能测试和监控调整优化。
4、 线程池的种类有哪些 ⭐⭐
在jdk中默认提供了4中方式创建线程池
- FixedThreadPool
特点:线程池中的线程数量固定。当提交任务时,如果线程池中有空闲线程,则直接使用;如果没有空闲线程,任务会在队列中等待,直到有线程可用。
使用场景:适用于处理稳定的、并发量相对固定的任务,比如服务器中处理固定数量的客户端连接请求。 - CachedThreadPool
特点:线程池中的线程数量不固定,会根据任务的提交情况动态创建和销毁线程。如果线程池中有空闲线程,会优先使用空闲线程;如果没有空闲线程,会创建新的线程来处理任务。当线程空闲时间超过一定阈值时,线程会被销毁。
使用场景:适用于处理突发性、并发量变化较大的任务,比如在 Web 应用中处理突发的大量短时间任务。 - ScheduledThreadPool
特点:主要用于定时任务和周期性任务的执行。可以指定任务在特定的延迟后执行,或者按照一定的周期定期执行。
使用场景:常用于需要定时执行的任务,如定时备份数据、定时发送邮件等。 - SingleThreadExecutor
特点:线程池中只有一个线程,所有任务会按照提交的顺序依次在这个线程中执行。它保证了任务的顺序执行,不会出现并发执行的情况。
使用场景:适用于需要顺序执行的任务,且不希望有多个线程同时访问共享资源导致数据不一致的情况,比如对数据库进行单线程的顺序操作。
5、 为什么不建议用Executors创建线程池 ⭐⭐⭐⭐
主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。
所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。
6.4丶使用场景
1 、你们项目哪里用到了线程池 ⭐⭐⭐
订单创建:当用户下单后,需要进行一系列操作,如验证用户信息、检查商品库存、计算订单金额等。将这些操作封装成任务提交到线程池,可以并行处理多个订单的创建任务,提高系统的并发处理能力,减少用户等待时间。
2、如何控制某个方法允许并发访问线程的数量? ⭐⭐⭐
在jdk中提供了一个Semaphore类(信号量)
它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了。第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1
3、谈谈你对ThreadLocal的理解 ⭐⭐⭐⭐⭐
两个主要功能:
- 资源对象的线程隔离:能让每个线程拥有并使用自己独立的资源对象,避免因多个线程争用同一资源而引发线程安全问题。
- 线程内的资源共享:可在同一个线程的不同代码片段之间共享资源对象。
底层原理
ThreadLocal 内部维护了一个 ThreadLocalMap 类型的成员变量,用于存储资源对象。具体操作如下:
- set 方法:调用此方法时,会以 ThreadLocal 自身作为 key,要存储的资源对象作为 value,将其存入当前线程的ThreadLocalMap 集合中。
- get 方法:调用该方法时,会以 ThreadLocal 自身作为 key,在当前线程的 ThreadLocalMap中查找与之关联的资源值。
- remove 方法:调用此方法时,会以 ThreadLocal 自身作为 key,从当前线程的 ThreadLocalMap中移除与之关联的资源值。
内存溢出问题
ThreadLocalMap 中的 key 被设计为弱引用,在垃圾回收(GC)时,key 会被被动回收。然而,value 是强引用,不会被自动回收。通常,ThreadLocal 会被定义为静态变量(强引用),无法依赖 GC 自动回收。为避免内存溢出,建议在使用完 ThreadLocal 后,主动调用 remove 方法释放 key 和对应的 value。
七、JVM
7.1丶JVM组成
1、JVM由那些部分组成,运行流程是什么? ⭐⭐⭐⭐⭐
JVM 主要由类加载子系统、运行时数据区、执行引擎和本地方法接口四大部分组成:
- 类加载子系统:加载 Java 字节码文件(.class文件)到 JVM 中,并生成对应的Class对象,供运行时数据区使用。包括:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器
- 运行时数据区:包括堆(存对象实例和数组)、虚拟机栈(存储方法调用时的局部变量表、操作数栈、动态链接、方法出口等)、本地方法栈(为本地方法提供内存空间)、方法区(存类信息、静态变量、常量池)、程序计数器(记录当前线程正在执行的字节码指令的地址)。
- 执行引擎:负责执行字节码指令,将字节码转换为特定平台的机器码。包含解释器(逐行解释执行字节码)和JIT编译器(将热点代码编译为本地机器码以提升执行效率)和垃圾回收器GC。
- 本地方法接口:调用操作系统或C/C++库的功能。
运行流程:
- 类加载:通过类加载器加载.class文件,经过加载、验证、准备、解析、初始化五个步骤,将类信息存入方法区,并在堆中创建Class对象。
- 内存分配:JVM划分运行时数据区:堆存储对象实例,栈管理方法调用(线程私有),方法区存类元信息,程序计数器记录执行位置。
- 执行指令:执行引擎解释或编译(JIT)字节码为机器码,由CPU执行;必要时通过本地方法接口调用系统资源,垃圾回收(GC)自动回收堆内存。
2、什么是程序计数器 ⭐⭐⭐
程序计数器是JVM中线程私有的内存区域,每个线程都有一个独立的程序计数器。
- 作用:记录当前线程正在执行的字节码指令的地址。
- 特点:不会发生内存溢出的区域,所以也不会垃圾回收
- 意义:确保线程切换后能恢复到正确的执行位置。决定字节码的执行顺序。
3、什么是Java堆? ⭐⭐⭐
Java堆是JVM管理的最大一块内存区域,所有线程共享,用于存储对象实例和数组。
组成:
- 年轻代:包括Eden区和两个Survivor区,新创建的对象首先分配在Eden区,经过多次GC后存活的对象晋升到老年代。使用复制算法垃圾回收
- 老年代:存放生命周期较长的对象,GC频率较低。使用标记-清除或标记-整理算法垃圾回收
JDK 1.7与1.8的区别:
-
JDK 1.7使用永久代存储类信息、静态变量等,容易导致内存溢出。
-
JDK 1.8移除了永久代,引入元空间,使用本地内存,默认不限制大小,避免了内存溢出问题。
4、什么是虚拟机栈 ⭐⭐⭐
虚拟机栈属于线程私有的内存区域。它主要用于管理方法的调用和执行过程,存储方法运行时所需的局部变量、操作数栈、动态链接和方法出口等信息。栈的生命周期与线程一致,线程结束时,对应的栈内存随之释放。
栈帧:
- 每次方法调用会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法返回地址等数据。
- 当前正在执行的方法对应的栈帧称为活动栈帧,每个线程同一时刻只能有一个活动栈帧。
异常:
- 栈溢出(StackOverflowError):常见于递归调用过深。
- 栈内存不足(OutOfMemoryError):线程数过多导致栈内存耗尽
栈内存分配
- 不是越大越好,默认的栈内存通常为1Mb,栈帧过大会导致线程数变少
局部变量线程安全性
-
基本数据类型的局部变量(如 int, boolean 等)是线程安全的。
因为每个线程在调用方法时,都会在各自的栈帧中创建独立的变量副本,互不干扰。 -
对象类型的局部变量(即引用类型)的线程安全性取决于对象的作用域:
如果对象仅在方法内创建和使用,且未暴露给其他线程,则是线程安全的。
如果对象被共享给其他线程(例如通过返回值、静态变量或传递给其他线程),则需要额外同步机制来保证线程安全,此时问题在于对象本身的状态管理。
5、堆栈的区别是什么? ⭐⭐⭐⭐⭐
特性 | 栈 | 堆 |
---|---|---|
存储内容 | 局部变量表、操作数栈、动态链接、返回地址 | 对象实例、数组 |
线程共享性 | 线程私有 | 线程共享 |
内存管理 | 自动分配和释放,速度快 | 由垃圾回收器(GC)管理,速度较慢 |
内存大小 | 较小,默认1MB | 较大,默认物理内存的1/4 |
异常 | 栈溢出:StackOverflowError |
内存不足:OutOfMemoryError |
6、什么是方法区,什么是运行时常量池 ⭐⭐⭐⭐
方法区是JVM中所有线程共享的内存区域,用于存储类的元数据、运行时常量池、静态变量、即时编译器编译后的代码等。
内容:
- 类的元数据:类的名称、字段、方法、接口等描述信息。
- 运行时常量池:类和接口中的常量(如字符串常量、数字常量等)。
- 静态变量:类中定义的静态变量。
- 即时编译器编译后的代码:JIT编译后的机器码。
实现:
- JDK 1.7及之前:方法区由永久代实现,存储在堆内存中。
- JDK 1.8及之后:方法区由元空间实现,存储在本地内存中,默认不限制大小。
运行时常量池是方法区的一部分,每个类或接口在 JVM 加载时都会创建对应的运行时常量池。它用于存储:
- 编译期常量:如字符串字面量(“hello”)、基本数据类型常量(100、3.14)。
- 符号引用:类和接口的全限定名、字段名和描述符、方法名和描述符。JVM在运行时将其解析为直接引用。
- 动态生成的常量:如String.intern()方法将字符串添加到常量池中。
7、什么是直接内存? ⭐⭐⭐
直接内存是JVM外部的内存区域,由操作系统的本地内存分配,不由JVM管理。
优点:
- 减少数据拷贝,提升 IO 效率:当 Java 程序进行网络通信、文件读写或与底层系统交互时。直接内存可直接与内核空间交互,避免 “堆内存→直接内存” 的拷贝步骤。
- 突破 JVM 堆内存限制:堆内存受-Xmx参数限制,而直接内存由操作系统管理,可利用更大的物理内存。例如,处理超大容量数据(如大数据框架 Hadoop 的块存储)时,直接内存能避免堆内存溢出。
- 降低 GC 压力:堆内存中的对象会触发 GC,而直接内存的回收由 JVM 通过Unsafe类或DirectByteBuffer的cleaner机制手动控制,可减少 GC 对应用程序的暂停影响
应用场景:
- NIO 框架(如 Netty)、文件读写(MappedByteBuffer)、图像处理、数据库连接池(如 MySQL 的 JDBC 驱动使用直接内存缓存结果集)等。
直接内存通过牺牲部分易用性(需手动管理)换取性能和内存容量的优势,是 Java 与底层系统高效交互的重要桥梁。
7.2丶类加载器
1、什么是类加载器,类加载器有哪些 ⭐⭐⭐
类加载器是JVM的核心组件,负责在程序运行时动态加载类文件(.class)
分类:
- 启动类加载器:加载Java 核心类库,如java.lang.*、java.util.*等。
- 扩展类加载器:加载扩展类库(JAVA_HOME/jre/lib/ext)。
- 应用类加载器:加载用户类路径(ClassPath)下的类。
- 自定义类加载器:用户继承ClassLoader实现自定义加载规则。
2、什么是双亲委派模型,为什么采用双亲委派机制? ⭐⭐⭐⭐⭐
双亲委派模型是JVM类加载器的一种工作机制,它定义了类加载器在加载类时的层次关系和协作方式。
核心机制:
- 当一个类加载器收到类加载请求时,首先委托父类加载器加载。
- 如果父类加载器无法完成加载任务,子类加载器才会尝试加载。
加载顺序:
- 启动类加载器 → 扩展类加载器 → 应用类加载器 → 自定义类加载器。
优点:
- 避免重复加载:父加载器已加载的类,子加载器不会再次加载,保证类的全局唯一性。
- 安全性:防止用户伪造同名类篡改核心功能
- 隔离性:不同加载器加载的类默认处于不同命名空间,实现类隔离
3、类装载的执行过程? ⭐⭐⭐⭐
目标:将 .class 文件加载到 JVM,形成 Class 对象,供程序运行时使用。
分为三个阶段:加载(Loading)→ 链接(Linking)→ 初始化(Initialization)。
加载(Loading)
- 定位字节码:通过类全限定名(如 java.lang.Object)查找 .class 文件(可从文件、网络、JAR包等来源加载)。
- 存储数据结构:将字节码解析为方法区(元空间)的运行时数据结构(类信息、常量池、字段、方法等)。
- 生成 Class 对象:在堆中创建 java.lang.Class 对象,作为访问方法区数据的入口。
- 双亲委派:类加载器优先委派父加载器加载,避免重复加载(安全性和一致性保障)。
链接(Linking)
- 验证:确保字节码合法(文件格式、元数据语义、字节码逻辑、符号引用可访问性)。
- 准备:为静态变量分配内存并赋零值(如 int 为 0),static final 常量直接赋最终值。
- 解析:将常量池中的符号引用(如类/方法名)转为直接引用(内存地址或句柄)。
初始化(Initialization)
- 执行 () 方法(编译器自动生成,合并所有静态变量赋值和 static{} 静态代码块)。
- 触发条件:首次主动使用类时(如 new、访问静态字段/方法、反射、子类初始化等)。
- 线程安全:JVM 加锁保证 () 只执行一次,且父类先于子类初始化。
7.3丶垃圾回收
1、简述Java垃圾回收机制?(GC是什么?为什么要GC) ⭐⭐⭐⭐⭐
Java垃圾回收机制(GC)是JVM自动管理内存的机制,负责回收不再使用的对象,释放内存空间。
目的:
- 避免内存泄漏:若对象不再使用但未被回收,会导致内存占用持续增长,最终引发OutOfMemoryError。
- 简化内存管理:开发者无需手动编写free()或delete()代码,降低了内存泄漏的风险
- 提高内存利用率:GC 会整理内存碎片,使堆内存空间更连续
工作原理:
- 标记垃圾算法:通过可达性分析算法或引用计数算法找到不再使用的对象。
- 回收垃圾算法:使用标记-清除、标记-整理、标记-复制算法等回收内存。
- 分代回收策略:年轻代频繁回收,老年代较少回收。年轻代内存不足时触发Minor GC,老年代内存不足时触发Full GC。
2、标记垃圾的算法⭐⭐⭐⭐
- 引用计数法:为每个对象维护引用计数器,记录有多少个变量引用该对象。无法解决循环引用问题。
- 可达性分析算法:从GC Roots出发,遍历所有可达对象,未遍历到的对象被视为垃圾(JVM默认使用)。
GC Roots 包括:
1)虚拟机栈(栈帧中的本地变量表) 中引用的对象:当前正在执行的方法中的局部变量(包括参数)所引用的对象。
2)本地方法栈中JNI引用的对象:当前正在执行的Native方法(用C/C++等写的本地方法)中引用的Java对象。
3)方法区(或元空间)中类静态属性引用的对象: 类的static变量(静态变量)所引用的对象。
4)方法区(或元空间)中常量引用的对象:被声明为static final的常量所引用的对象(通常是字符串常量池里的字符串或基本类型包装类的缓存对象)。
5)Java虚拟机内部的引用:虚拟机自身运行所需的一些内部对象引用,基本数据类型对应的Class对象(如Integer.TYPE)。系统类加载器(System Class Loader)。一些常驻的异常对象(如NullPointerException, OutOfMemoryError)。 预加载的核心类库中的对象。
6)被同步锁(synchronized)持有的对象:当前正被用作监视器锁(Monitor) 的对象。
7)反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等:一些用于监控、调试、性能分析等管理接口相关的对象。 - 重点: 两栈(虚拟机栈+本地方法栈)+两方法区(类静态变量+常量)
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案【点击此处即可/免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
3、JVM 垃圾回收算法? ⭐⭐⭐⭐⭐
标记-清除算法
- 过程:标记所有可达对象,清除未标记对象。
- 优点:简单高效。
- 缺点:产生内存碎片。
标记-整理算法
- 过程:标记所有可达对象,将存活对象向内存一端移动,清理边界以外的内存。
- 优点:无内存碎片。
- 缺点:对象移动开销大,效率较低。
标记-复制算法
- 过程:将内存分为两块,复制存活对象到另一块内存,清空当前内存。
- 优点:无内存碎片,效率高。
- 缺点:内存利用率低。
分代回收策略:
- 年轻代:使用复制算法,适合生命周期短的对象。
- 老年代:使用标记-清除或标记-整理算法,适合生命周期长的对象。
4、JVM中的分代回收 ⭐⭐⭐⭐⭐
JVM中的分代回收基于对象的生命周期,将堆内存分为新生代和老年代,采用不同的垃圾回收策略。
堆的区域划分:
- 新生代:占堆内存的1/3,分为Eden区(80%)和Survivor区(From和To,各10%)。
- 老年代:占堆内存的2/3,存放生命周期较长的对象。
分代回收策略:
- 对象分配:新创建的对象首先分配在Eden区。
- Minor GC:当Eden区内存不足时触发,将存活对象复制到Survivor区(To),清空Eden区和Survivor区(From),交换From和To的角色。
- 对象晋升:对象在Survivor区经过多次Minor GC后(默认15次)仍存活,晋升到老年代。
优点:
- 提高垃圾回收效率,减少Full GC的频率。
5、MinorGc、Mixed Gc、FullGC的区别是什么 ⭐⭐⭐⭐⭐
Minor GC:
-
范围:仅回收新生代(Eden区和Survivor区)。
-
触发条件:Eden区内存不足时触发。
-
特点:暂停时间短,使用复制算法,效率高。
Mixed GC:
- 范围:回收新生代和老年代的部分区域,G1垃圾回收器特有。
- 触发条件:老年代占用率达到一定阈值时触发。
- 特点:选择性回收老年代,减少Full GC的频率,暂停时间介于Minor GC和Full GC之间。
Full GC:
- 范围:回收整个堆内存(新生代和老年代)及方法区(元空间)。
- 触发条件:老年代或方法区内存不足,或显式调用System.gc()。
- 特点:暂停时间长,对程序性能影响较大,应尽力避免。
6、 JVM 有哪些垃圾回收器? ⭐⭐⭐⭐⭐
新生代
- Serial GC :采用复制算法,单线程执行。 特点:简单高效,适用于单线程环境
- ParNew GC:采用复制算法,多线程并行执行。是 Serial GC 的多线程版本,主要与 CMS GC 配合使用
- Parallel Scavenge GC:采用复制算法,多线程并行执行。吞吐量优先,自动调整堆大小以达到预设的吞吐量目标
老年代
- Serial Old GC:采用标记 - 整理算法,单线程执行。用于配合 Serial GC 或作为 CMS GC 的后备方案
- Parallel Old GC:采用标记 - 整理算法,多线程并行执行。与 Parallel Scavenge GC
配合,实现全堆的吞吐量优先回收。 - CMS GC:采用标记 - 清除(为了低延迟)算法,多阶段与用户线程并发执行。以最短回收停顿时间为目标,通过并发标记和清除减少 STW 时间,但可能产生内存碎片。运作阶段分为:
初始标记:GC Roots直接关联的对象,速度很快,需要停顿
并发标记:从GC Roots直接关联的对象开始变量整个对象图,不需要停顿
重新标记:标记那些由于程序运作产生变动过的对象,需要停顿
并发清除:标记-清除掉已经死亡的线程,不需要停顿
全堆垃圾回收器
- G1 GC:将堆划分为多个大小相等的Region,混合使用标记-复制算法和标记 - 整理算法。兼顾吞吐量和低延迟,优先回收垃圾最多的 Region(Garbage-First 策略),适用于大内存场景。
7、说说G1垃圾回收器 ⭐⭐⭐⭐⭐
G1(Garbage-First)是JDK 7u4引入、JDK 9成为默认的垃圾回收器,目标是在可控的停顿时间(STW)内实现高吞吐量,特别适合大内存且对延迟敏感的应用场景。其核心特点:
-
内存划分:将堆分为多个大小相等的Region,动态充当 Eden、Survivor、Old 或 Humongous(大对象)区。
-
核心目标:G1的核心设计目标是允许用户设置期望的最大停顿时间目标(通过 -XX:MaxGCPauseMillis= 指定,如200ms)。,在吞吐量和低延迟间平衡。
-
工作阶段:
Young GC:
触发: Eden区Region满了。
过程:暂停应用线程,采用复制算法,将Eden和一个Survivor区(From)中的存活对象复制到另一个Survivor区(To)或直接晋升到Old区的Region中。回收的是整个年轻代的Region(Eden + Survivor)
Mixed GC:
触发:老年代占比超阈值(默认45%)且并发标记完成。
过程:初始标记 (停顿): 借Young GC停顿标记直接引用。
并发标记 (不停顿):与应用线程并发,标记全堆可达对象。
最终标记 (停顿): 处理并发期间的引用变化。
混合回收 (停顿): 选择一组包含Young和部分高价值Old Region进行回收(复制算法)。避免全堆扫描! -
大对象处理:Humongous 区存储≥Region 一半大小的对象,避免频繁 GC
-
注意:G1 垃圾回收器既不是单纯基于标记 - 清除,也不是单纯基于标记 - 复制。从整体来看,G1 是基于标记 -整理算法实现的收集器;从局部(两个 Region 之间)上来看,是基于 “标记 - 复制” 算法实现
-
简要对比CMS的优势:内存碎片控制、可预测停顿、全功能回收
8、强引用、软引用、弱引用、虚引用的区别? ⭐⭐⭐⭐
- 强引用:只要强引用存在,对象就不会被回收。即使内存不足,JVM抛出OutOfMemoryError也不会回收强引用对象。用于表示程序中必须存活的对象。
- 软引用:通过SoftReference类实现。当内存不足时,JVM会回收软引用对象。
- 弱引用:通过WeakReference类实现。只要发生垃圾回收,无论内存是否充足,弱引用对象都会被回收。适用于那些希望在对象不再被使用时尽快被回收的场景。
- 虚引用:通过PhantomReference类实现。无法通过虚引用来获得对象实例,为一个对象设置虚引用唯一目的只是能为了这个对象被垃圾回收器回收时收到一个系统通知。
更多推荐
所有评论(0)