2025年八股文java面试题 - 实践
题目答案
1. synchronized 关键字的作用?
答:在Java中synchronized关键字用于实现线程同步,主要解决是多个线程之间访问相同资源不会发生数据不一致的问题。它可以作用于方法或代码块上,以保证同一时间只有一个线程可以执行被同步的方法或代码块。
同步实例方法: 表示该方法在同一时间只能由一个线程访问同一个实例。
public class SynchronizedExample { public synchronized void synchronizedMethod() { // 同步代码 } }AI生成项目java运
同步静态方法:表示该方法在同一时间只能由一个线程访问同一个类的所有实例。
public class SynchronizedExample { public static synchronized void synchronizedStaticMethod() { // 同步代码 } }AI生成项目java运
同步代码块:表示该代码块在同一时间只能由一个线程访问该对象。
public class SynchronizedExample { private final Object lock = new Object(); public void synchronizedBlock() { synchronized (this) { // 同步代码 } } }AI生成项目java运
2. volatile 关键字的作用?
答:在Java中volatile关键字用于声明变量的可见性和防止指令重排,从而提供一种轻量级的同步机制。
在可见性方面,用volatile修饰的变量,来确保一个线程对该变量的修改对其他线程立即可见。
在防止指令重排方面,volatile会禁止JVM对变量操作的指令进行重新排序。
3. synchronized 和 volatile 的区别?
答:synchronized和volatile两个关键字都用于实现线程同步的机制。
- volatile是线程同步的轻量级实现,volatile性能比synchronized要好。volatile关键字只能用于变量,而synchronized关键字可以修饰方法和代码块。
- volatile能保证数据的可见性,但不能保证数据的原子性。synchronized两者都能保证。
- volatile主要解决变量在多个线程之间的可见性,而synchronize解决的是多个线程之间访问资源的同步性。
4. synchronized 和 ReentrantLock 的区别?
答:synchronized和ReentrantLock都是Java中用于实现线程同步的机制,并且都是可重入锁。
- synchronized:
- 是Java语言的内置关键字,用于对代码块或方法进行同步。
- 简单易用,直接在方法或代码块上使用即可。
- 由JVM实现,使用方便,但功能较为有限。
- ReentrantLock:
- ReentrantLock是java.util.concurrent.locks包中的类,提供了更灵活和丰富的锁机制(响应中断、尝试获取锁、使用条件变量等)。
- 需要显示的加锁和解锁,通过代码来控制锁的获取和释放。
- 由Java库实现,功能强大,灵活性高。
5. ThreadLocal有什么用?
答:ThreadLocal在Java中用于创建线程局部变量,确保每个变量都有自己的独立副本变量,从而避免多线程共享一个变量带来的线程安全问题,同时提高了并发性能。然而在使用ThreadLocal时需要注意内存泄露问题,由于ThreadLocal变量的生命周期与线程线相同,如果线程池中线程长时间不被销毁,而ThreadLocal变量没有被正确移除,可能会导致内存泄露问题,因此建议在不再使用ThreadLocal变量时显示调用remove()方法。
它的使用场景有用户会话管理、数据库连接管理、事物管理等。
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可-免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
6. 线程池有什么用?为什么不推荐使用内置线程池?
答:Java中的线程池是一种基于池化技术设计用于执行异步任务的框架,它维护了一定数量的线程,避免频繁地创建和销毁线程带来的性能开销和资源浪费。他的主要作用是提高资源复用、提高系统稳定性、便于管理和提供灵活的并发策略。
不推荐使用内置线程池的主要原因是内置线程池等配置选项有限、不能满足所有的应用场景的需求。Java库中提供了几种预定义线程的实现,如Executors类中的newFixedThreadPool、newCacheThreadPool、newSingleThreadExecutor等。
- FixedThreadPool 和 SingleThreadExecutor 使用无界队列(LinkedBlockingQueue),在任务提交速度超过执行速度时,任务队列可能无限增长,导致内存耗尽。
- CacheThreadPool 使用无界线程池,在高并发下可能会创建大量线程、导致系统资源耗尽。未被回收的线程可能会长时间占用资源,造成线程泄漏问题。
7. 如何自定义线程池?
答:在Java中,自定义线程池可以通过ThreadPoolExecutor类来实现。ThreadPoolExecutor提供了丰富的配置选项,如核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、任务队列(workQueue)和拒绝策略(rejectedExecutionHandler)等,可以根据具体需求进行灵活配置。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
// 核心线程数 :线程池在空闲时保留的线程数,即使没有任务需要处理。
int corePoolSize = 5;
// 最大线程数 :线程池允许创建的最大线程数。
int maximumPoolSize = 10;
// 非核心线程空闲存活时间 :当线程数超过核心线程数时,多余的空闲线程的存活时间。
long keepAliveTime = 1L;
// 时间单位 : 空闲线程存活时间的时间单位。
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列 :用于保存等待执行任务的队列。
ArrayBlockingQueue workQueue = new ArrayBlockingQueue<>(100);
// 线程工厂 : 用于创建新线程。
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略 : 当线程池和队列都满时,如何处理新任务
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
// 创建ThreadPoolExecutor
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 提交任务
for (int i = 0; i {
System.out.println(Thread.currentThread().getName() + " is processing " + taskId);
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池(不再接受新任务,但已提交的任务会继续执行)
executor.shutdown();
// 等待所有任务完成
while (!executor.isTerminated()) {
// 等待一段时间
}
System.out.println("All tasks completed.");
}
}
AI生成项目java运行
8. Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?
答:
线程池的参数:
- corePoolSize(核心线程数):线程池中始终保留的线程数量,即使这些线程处于空闲状态。
- maximumPoolSize(最大线程数):线程池中允许创建的最大线程数量。当任务队列已满且当前线程数小于最大线程数时,会创建新线程来处理任务。
- keepAliveTime(空闲线程存活时间):当线程池中的线程数超过核心线程数时,多余的空闲线程在终止前等待新任务的最长时间。
- timeUnit(时间单位):keepAliveTime 参数的时间单位。常见值包括 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
- workQueue(任务队列):用于保存等待执行任务的队列l。
- threadFactory(线程工厂):用于创建新线程。
- rejectedExecutionHandler(拒绝策略):当线程池和队列都满时,如何处理新任务。
阻塞队列:
- ArrayBlockingQueue:一个基于数组的有界阻塞队列。按FIFO(先进先出)顺序保存任务。
- LinkedBlockingQueue:一个基于链表的可选有界阻塞队列。通常用于无限制的任务队列。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等待一个相应的移除操作。
- PriorityBlockingQueue:一个基于优先级的无限阻塞队列。任务按照优先级顺序执行。
- DelayQueue:一个基于优先级队列的无界阻塞队列,只有在延迟期满时才能从队列中取走元素。
拒绝策略:
- AbortPolicy(默认策略):抛出 RejectedExecutionException 异常,阻止系统正常工作。
- CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。这种策略会降低新任务的提交速度,从而减轻线程池的负载。
- DiscardPolicy:直接丢弃任务,不抛出异常。如果允许任务丢失,这种策略可以用于避免系统过载。
- DiscardOldestPolicy:丢弃队列中最旧的任务(即即将执行的任务),然后重新尝试提交新任务。
9. 线程池处理任务的流程了解吗?
答:线程池处理任务的流程可以总结为:提交任务、检查核心线程数、任务队列处理、非核心线程创建、执行任务以及线程回收。
- 通过submit()或execute()方法提交任务
- 检查核心线程数,如果当前线程数少于核心线程数,那么就创建新的核心线程来执行任务。如果当前线程数已达到核心线程数,将任务放入任务队列。
- 如果任务队列没有满,将任务队列继续放入队列。如果任务队列已满,检查当前线程数是否小于最大线程数。
- 如果当前线程数小于最大线程数,创建新的非核心线程执行任务,如果当前线程已经达到了最大线程数,执行拒绝策略。
- 线程从任务队列取出任务执行。
- 线程池中的非核心线程在完成任务后不会立即销毁,进入保持存活状态,只有当这些线程在空闲时间超过keepAliveTime后被回收。
10. 如何给线程池命名?为什么建议给线程池命名?
答:要给线程池中的线程命名,可以自定义一个 ThreadFactory,在创建线程时设置线程名称。
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 线程工厂,它设置线程名称,有利于我们定位问题。
*/
public final class NamingThreadFactory implements ThreadFactory {
private final AtomicInteger threadNum = new AtomicInteger();
private final String name;
/**
* 创建一个带名字的线程池生产工厂
*/
public NamingThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
return t;
}
}
AI生成项目java运行
在 Java 中,为线程池中的线程命名是一种良好的实践。命名线程有助于调试和监控,使得可以轻松识别和跟踪线程的行为和状态。
1. 什么是线程和进程? 线程与进程的关系和区别?
答:进程是程序一次执行的过程,系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程是程序执行的最小单位,一个进程可以产生多个线程,线程可以共享其进程的资源。
线程与进程的关系:一个进程可以包含多个线程,线程是进程的一部分。线程共享进程的资源,进程之间相互独立。
线程和进程的区别:进程有独立的内存空间,线程共享进程的内存空间。进程间通信复杂,线程可以通过共享内存进行通信速度快。进程独立性强安全性高,线程独立性相对较弱,一个线程崩溃可能导致整个程序崩溃。
2. 为什么要使用多线程?
答:提高性能和响应速度,多线程可以使程序同时执行多个任务,从而充分利用多核处理器的能力,提高程序的执行效率。如一个多线程的Web服务器可以同时处理多个客户端请求。
更好的利用资源,在多核处理器环境下,多线程可以让多个CPU核心并行工作。在单核处理器上,多线程可以让某些任务等待I/O操作时执行其他任务,从而更高效的利用CPU时间。
3. 什么是线程上下文切换?
答:线程上下文切换是指操作系统在多线程任务处理中,将CPU从一个线程切换到另外一个线程的过程。在这个过程中,操作系统需要保存当前线程的状态也称上下文(程序计数器、堆栈信息等),然后加载另外一个线程的状态,以便该线程可以继续执行。
出现线程切换的情况:
- 时间片用完:操作系统将CPU时间划分为多个时间片,每个线程会被分配一个固定的时间片,当时间片用完,操作系统会进行上下文切换,让其他线程活动CPU时间。
- 阻塞操作:当线程执行阻塞操作(如等待I/O操作完成、执行slepp()、wait()、等待同步锁等)时,它会主动放弃CPU的使用权,进入阻塞状态。
- 中断处理:当硬件发生中断时(如键盘输入、定时器触发),操作系统需要处理当前线程的执行,以处理中断。
- 线程优先级变化:当某个线程的优先级发生变化时(如通过调用线程的setPriority()方法),作系统可能会进行上下文切换,将CPU分配给这个高优先级的线程。这种调度策略称为抢占式调度。
4. 什么是线程死锁? 如何避免死锁?
答:线程死锁是指两个或多个线程在执行的过程中,因相互持有对方所需要的资源而处于等待状态,导致所有线程都无法执行继续的情况。具体表现为,每个线程都在等待其他线程释放资源,而每个线程又都持有其他线程所需的资源,从而形成一个等待环路,造成永久阻塞。
死锁的四个必要条件:
- 互斥:至少有一个资源必须是非共享的,即一次只能由一个线程使用。
- 占有且等待:个线程已经持有至少一个资源,同时等待获取其他被其他线程持有的资源。
- 不可剥夺:资源不能被强行剥夺,必须由持有线程显示释放。
- 循环等待:发生死锁时,涉及的所有线程必定存在一个循环等待资源的链。
避免死锁的方法: - 资源有序分配:为资源分配一个全局顺序,所有线程按照这个顺序请求资源,可以避免发生循环等待。
- 避免嵌套锁:尽量避免一个线程同时持有多个锁,减少嵌套锁的使用。
- 限时等待:如无法在指定时间内获取锁,就放弃请求并释放已经持有的锁。
- 死锁检测和恢复:使用JVM提供的工具或第三方库来监控和检测死锁。
- 资源预先分配:确保线程在开始执行前就已经获取到所需的所有资源。
5. 乐观锁和悲观锁了解么?如何实现乐观锁?
答:乐观锁和悲观锁这两种锁策略用于解决并发控制问题。
悲观锁是一种假设最坏情况的锁策略,每次在去访问数据时都会认为别人会修改,所以在每次访问数据时都会上锁,这样别人在拿这个数据就会阻塞直到它拿到锁。Java中悲观锁是通过synchronized关键字或Lock接口来实现。
乐观锁是一种假设最好情况的锁策略,每次在去访问数据时都认为别人不会修改,所以不会上锁,但是在修改的时候会去判断有没有人修改这个数据。Java中通常通过版本号机制或者CAS算法来实现。
实现乐观锁的方式:
- 版本号:在数据库表中添加一个版本号字段,每次更新数据时版本号加1。在更新数据时,将当前版本号作为条件之一进行更新,如果版本号不匹配,则说明数据已经被其他线程修改。
- CAS:使用一个预期值和要更新的变量值进行比较,两值相等才会进行更新
篇幅限制下面就只能给大家展示小册部分内容了。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处即可-免费获取】
https://docs.qq.com/doc/DQXdYWE9LZ2ZHZ1ho
6. 说说 sleep() 方法和 wait() 方法区别和共同点?
答:slepp()和wait()都可以暂停线程的执行。
两者的区别:
- sleep() 是 Thread 类的静态方法,wait() 是 Object类的实例方法。
- sleep() 不会释放锁,wait() 会释放锁进入等待状态、直到被唤醒。
- sleep() 在指定的时间到期后会自动唤醒,wait() 需要 notify()/notifyAll()方法来唤醒。
- sleep() 可以在任何地方使用,wait() 必须在同步块或者同步方法中调用。
7. 讲一下 JMM(Java 内存模型) ?
答:Java内存模型是Java虚拟机规范的一部分,它定义了在多线程环境下,Java程序中变量的访问方式。JMM是一种抽象概念,它屏蔽了不同硬件和操作系统内存访问的差异,为Java程序提供了一致的内存访问效果。
JMM分为主内存和工作内存,主内存是所有线程共享的区域,对应于JVM内存区域中的堆。工作内存是JMM抽象的概念,并非真实存在,它对应于JVM中的栈,线程对变量的所有操作都必须在工作内存中进行,不能直接操作主内存中的数据。
8. Java 内存区域和 JMM 有何区别?
答:Java内存区域侧重于JVM如何管理和划分运行时内存区域。Java内存区域有堆、方法区、虚拟机栈、程序计数器、本地方法栈。
JMM侧重于多线程环境下线程与主存之间的交互关系以及变量的访问规则。
9. Java中如何创建线程?
答:Java中创建线程的方式有很多,如继承Thread类、实现Runnable接口、实现Callable接口、使用ExecutorService线程池等等。
- 继承Thread类:创建一个类来继承Thread类,重写它的run()方法。然后通过创建这个类的实例来创建新的线程,通过调用类实例的start()方法来启动线程。
class MyThread extends Thread { public void run() { System.out.println("线程运行中"); } } public class ThreadExample { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 启动线程 } }AI生成项目java运行
- 实现Runnable接口:创建一个类来实现Runnable接口,实现它的run()方法,然后创建Thread类的实例,将Runnable接口实现类的实例作为构造器参数传递,调用Thread类的实例的start()方法启动线程。
class MyThread extends Thread { public void run() { System.out.println("线程运行中"); } } public class ThreadExample { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 启动线程 } }AI生成项目java运行
- 实现Callable接口:创建一个Callable接口的实现类,并实现call()方法。然后创建Callable实现类的实例,使用FutureTask类来包装Callable对象,使用FutureTask对象作为Thread对象的target创建并启动新线程,调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
class MyCallable implements Callable { public String call() throws Exception { return "线程执行结果"; } } public class CallableExample { public static void main(String[] args) throws Exception { MyCallable callable = new MyCallable(); FutureTask futureTask = new FutureTask<>(callable); new Thread(futureTask).start(); System.out.println(futureTask.get()); // 获取结果 } }AI生成项目java运行
10. 实现Runnable接口和实现Callable接口的区别?
答:他们主要的区别体现在方法签名和返回值、异常处理和提交与执行方式上。
- 方法签名和返回值:Runnable的run()方法有返回值,Callable的call()方法有返回值。
- 异常处理:run()方法不能抛出受检异常,只能处理运行时异常。call()方法可以声明抛出异常。
- 提交和执行:Runnable通过Thread或ExecutorService执行。Callable通过ExcutroService执行,并返回Future对象以获取结果。

浙公网安备 33010602011771号