Java八股文详细文档.3(基于黑马、ChatGPT、DeepSeek)
通过B站黑马程序员的八股文教学,自己也二刷了,结合ChatGpt、deepSeek总结了一下,Java八股文详细文档.3(框架篇和并发编程篇)
Java八股文详细文档.1(包含JVM篇、数据库篇和常见集合篇):
https://blog.csdn.net/weixin_73144915/article/details/145535602?spm=1001.2014.3001.5502
Java八股文详细文档.2(包含数据库篇和消息中间件篇):
https://blog.csdn.net/weixin_73144915/article/details/145601349?spm=1001.2014.3001.5501
Java八股文面试重点完整篇
https://blog.csdn.net/weixin_73144915/article/details/145657974?spm=1001.2014.3001.5501
六、框架篇
1.Spring框架中的单例bean是线程安全的吗?(重点)
这是线程不安全的。一般来说,在spring中的bean都是注入无状态(不能修改)的对象,就没有线程安全问题,但如果在bean中定义了可修改的成员变量,是需要考虑线程安全问题的。
我们可以加锁,使用synchronized关键字进行同步,保证同一时刻只有一个线程能访问该方法;也可以通过 @Lookup 注解设置bean为多例,每次请求都会创建一个bean实例,多个线程操作互不影响;
2.能介绍一下动态代理吗
动态代理是指在程序运行时通过反射机制自动生成代理对象,并对目标对象的方法调用进行拦截和处理,他能在不修改目标类的情况下,增强目标类的功能(类增强),Java提供了两种方式来实现动态代理:
JDK动态代理:基于接口的动态代理,只能代理实现了接口的类;
CGLIB动态代理:基于类的动态代理,生成目标类的子类来实现代理;
动态代理提供了高度的灵活性和扩展性,但也带来了性能和复杂性,广泛应用于AOP、远程代理、虚拟代理等场景。
3.什么是AOP,项目中有用到AOP吗?(重点)
AOP称为面向切面编程,它用来将那些跟业务无关,但对多个对象产生影响的公共行为和逻辑,抽取封装为一个可重用的模块,这个模块被命名为“切面”,作用是减少系统中的重复代码,降低模块之间的耦合度,同时提高可维护性;
而我在项目中也经常用到AOP,比如有很多数据库表都有一些公共字段(createTime, updateTime, createUser, updateUser),每次对这些表进行插入或修改操作都要更新这些公共字段,所以可以利用AOP封装一个模块,根据当前时间、用户ID对属性赋值,在mapper中添加注解就实现了公共字段的自动填充功能。
而AOP在一些业务场景用的比较多的比如:记录操作日志、缓存处理等等。
4.spring事务失效的场景有哪些?
事务底层是基于AOP实现的,而AOP是通过代理对象实现的。
(1)没有用public 修饰,代理只会拦截公共方法;
(2)同一个类中调用自己的方法,这时调用不会经过代理对象。通过将自调用的方法提取到另一个服务类解决;
(3)事务注解标记在接口上而不是实现类上;
(4)自己捕获异常没有抛出,事务通知无法知悉。通过在catch中将异常抛出;
(5)捕获到检查异常,而spring默认只会回滚非检查异常。通过配置@Transactional(rollbackFor = Exception.class)设置回滚所有异常。
5.spring的bean的生命周期(重点)
(1)创建Bean:Spring容器启动时,根据XML配置文件或注解扫描来获取Bean的定义信息,调用构造函数来创建Bean实例;
(2)依赖注入:Spring容器根据Bean的配置,自动注入所有依赖的属性;
(3)调用初始化方法:Spring容器为每个Bean调用初始化方法;
(4)使用Bean:在容器中,Bean可以随时被获取和使用;
(5)容器关闭:当容器关闭时,Spring会调用Bean的销毁方法来销毁bean
6.能说一下Spring中的循环引用吗
循环依赖就是两个或两个以上的bean互相持有对方,最终形成闭环。循环依赖在spring中是允许的,spring框架利用三级缓存能解决大部分的循环依赖。
一级缓存:单例池,存放已经初始化的bean对象;二级缓存:缓存早期的bean对象(生命周期还没走完);三级缓存:缓存的是ObjectFacotry,表示对象工厂,用来创建某个对象。对于互相依赖(A依赖于B,B依赖于A)可以使用@Lazy 注解进行懒加载来实现。
7.能说一下SpringMVC的执行流程吗?(重点)
(1)用户发送HTTP请求到前端控制器(所有的请求都会先交给控制器处理)
(2)前端控制器收到请求后调用处理器;
(3)处理器根据请求的URL和请求类型查找匹配的处理器(Controller/Handler)
,返回处理器执行链给控制器;
(4)控制器接着调用处理器适配器;
(5)适配器经过适配调用具体的处理器,返回ModelAndView对象给控制器;
(6)控制器将ModelAndView传给视图解析器;
(7)视图解析器解析后返回具体的View(视图)给控制器;
(8)控制器根据View进行视图渲染;
8.Spring boot的自动配置原理能说一下吗?(重点)
SprintBoot项目中的启动类上有一个注解@SpringBootApplication,这个注解封装了三个关键的注解,分别如下:
(1)@SpringBootConfiguration注解:让SpringBoot的启动类作为一个配置类进行处理,本质上它是@Configuration注解的一个补充,用于表示SpringBoot特定的配置类;
(2)@EnableAutoConfiguration注解:用来启动Spring Boot的自动配置机制,减少了开发者的配置工作。它会根据应用的环境、类路径、配置文件等动态加载相应的bean,将其导入到Spring容器中;
(3)@ComponentScan注解:用来扫描指定路径的所有组件(@Component、@Service、@Repository注解标注的类),并将它们注册为Spring管理的bean,如果没有显示的指定扫描路径,默认会扫描跟启动类相同的包;
9.Spring中的常用注解有哪些?
10.Mybatis的执行流程?
(1)首先读取Mybatis配置文件
(2)创建会话工厂SqlSessionFactory,会话工厂创建SqlSession实例(包含了执行SQL语句的所用方法)
(3)操作数据库的接口,Executor执行器,同时负责查询缓存的维护
(4)通过MapperStatement对象处理传入参数并执行SQL语句
(5)将查询结果映射为Java对象并返回
(6)Mybatis还提供了事务的管理,支持自动提交和手动控制事务
(7)最后关闭SqlSession对象,释放数据库连接
七、并发编程篇
1.进程和线程的区别是什么?
(1)进程是正在运行程序的实例,进程包含了多个线程,每个线程执行不同的任务;
(2)不同的进程使用不同的内存空间,而当前进程的所有线程共享内存空间;
(3)线程更加轻量,线程上下文切换一般以进程低(上下文切换指的是从一个线程切换到另一个线程)
2.并发和并行有什么区别?
(1)并发是指多个任务在同一时间段内交替执行,通过快速切换任务看起来像是“同时执行”,可以在单核处理器上实现;
(2)并行是指多个任务在同一时刻同时执行,通常依赖于多核CPU实现;
3.创建线程的方式有哪些?(重点)
(1)继承Thread类并重写run( ) 方法;
(2)实现Runnable接口并实现run方法;
(3)实现Callable接口并实现call( )方法。两个接口的区别为:Runnable接口的run方法没有返回值,而Callable接口是个泛型,它的call方法有返回值,可以利用Futrue来获取结果;Callable接口的call方法允许抛出异常,而Runable接口的run方法的异常只能在内部处理(try-catch),不能向上抛。
(4)创建线程池,使用submit方法提交任务,shutdown方法关闭线程池,他避免了频繁的创建和销毁线程的开销,可以复用线程,适用于高并发的场景;
4.run( )方法和start( ) 方法有什么区别?
(1)start方法是用来启动线程的,通过该线程调用run方法执行定义好的逻辑代码,start方法只能调用一次;
(2)run方法封装了线程执行的逻辑代码,可以被调用多次;
5.线程有哪些状态,这些状态是怎么转化的?(重点)
线程的状态新建、可运行、阻塞、等待、时间等待和终止六个状态;
(1)创建线程对象的时候是新建状态;
(2)调用了start方法就转化为了可以执行状态;
(3)线程获取了CPU的执行权并执行完毕就转化为终止状态;
(4)在可执行状态的过程中,如果没有CPU执行权,可能会切换到其他状态:
- 如果没有获取锁就进入阻塞状态,获得锁后切换为可执行状态;
- 如果线程调用了wait( )方法进入等待状态,通过其他线程调用notify( )方法唤醒后切换为可执行状态;
- 如果线程调用了wait方法并设置睡眠时间就进入了时间等待状态,到时间后切换为可执行状态;
6.在Java中的wait和sleep方法有什么不同?
wait和sleep方法都是用来暂停线程的执行,但他们有很大的区别如下:
(1)方法调用不同:wait方法是Object类中的一个方法,每个Java对象都继承自Object类,因此每个对象都可以调用wait方法;而sleep是Thread类中的一个静态方法,只能通过Thread类来调用;
(2)醒来机制不同:wait()方法用于让当前线程进入等待状态,直到有其他线程调用该对象的notify()或notifyAll()方法,wait(long)和sleep(long)方法用于使当前线程暂停执行指定的时间,然后恢复运行;
(3)锁特性不同(重点):wait方法的调用必须先获取wait对象的锁,而sleep没有这个限制;wait方法执行后会释放对象锁,而sleep如果在synchronized代码块中执行,并不会释放对象锁;
7. 怎么停止一个正在运行的线程?
在Java中,不恰当的线程停止可能会导致不一致的状态或资源泄露,下面列举几种常见的停止线程的方法:
(1)Thread.stop( ) 方法会强制停止线程,不管线程是否在进行关键操作,可能会导致线程在不安全的情况下被中断,进而破坏数据一致性,所以这个方法不推荐使用(已作废);
(2)使用退出标志位,通过定义一个控制线程停止的标志变量来让线程自动退出,这种方法需要线程在执行过程中定期检查标志,来判断是否需要停止执行,这方法适用于没有阻塞操作(sleep、wait等)的线程;
(3)使用interrupt方法,这个方法不会直接停止线程,而是通知线程中断,当打断阻塞的线程,它会抛出InterruptedException异常,打断正常的线程会根据状态来标记是否退出线程;
8.synchronized关键字的底层原理是什么?
Synchronized 是采用互斥的方式让同一时刻至多只有一个线程持有对象锁,它的底层原理是监视器锁monitor实现线程的同步控制。每一个对象都有一个关联的监视器,可以理解为一个锁。当一个线程进入同步代码块时,他会获得这个对象的监视器锁,当线程退出同步代码块时,它会释放该锁;
Monitor内部有三个属性,分别是owner,entrylist,waitset,其中owner是关联的获得锁的线程,并且只能关联一个线程;entry list关联的时处于阻塞状态的线程;waitset关联的是处于等待状态的线程;
9.Monitor实现的锁属于重量级锁,你了解过锁升级吗?
10.你能谈一下JMM吗
(1)JMM(Java Memory Model)是Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性;
(2)JMM把内存分为两部分,一个是私有线程的工作区域(工作内存),另一个是所有线程的共享区域(主内存);
(3)线程跟线程之间是相互隔离的,线程跟线程交互需要通过主内存;
11.CAS能说一下吗?
CAS(Compare And Swap)意思是先比较再交换,操作步骤为先比较内存位置的值是否与预期值匹配,如果匹配就更新为新值,否则不操作。这是一种在并发编程中实现无锁操作的原子指令,用于确保多线程环境下数据更新的安全性。
它不需要锁,能避免线程阻塞和死锁问题,常用于乐观锁和自旋锁,但不能解决ABA问题(解决方案是添加版本号),长时间自旋导致CPU资源浪费(设置自旋次数)
12.乐观锁和悲观锁的区别?(重点)
(1)乐观锁就是认为数据冲突可能发生也可能不发生,操作时不会直接加锁;而悲观锁会认为数据冲突一定会发生,操作时先加锁在访问;
(2)乐观锁没有锁竞争,性能比较高,但大量重试可能会导致CPU资源浪费;而悲观锁频繁加锁/释放锁,开销大;
(3)乐观锁难以解决ABA问题,而悲观锁可能引起死锁问题
13.请谈一下你对volatile的理解?
(1)保证了线程间可见性:用volatile修饰共享变量,能够防止编译器对其进行优化,让一个线程对共享变量的修改对其他线程可见;
(2)禁止进行指令重排序:用vola修饰共享变量会再读、写共享变量时加入不同的屏障,组织其他读写操作越过屏障,从而达到阻止重排序的效果;
14.能说一下AQS吗?
(1)AQS(AbstractQueuedSynchronizer) 是 Java 多线程中的抽象队列同步器,这是一种锁机制,它是作为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的;
(2)AOS内部维护了一个先进先出的双向队列,队列中存储的是排队的线程;
(3)在AOS内部有一个属性state,这个status相对于一个资源,默认是0(无锁状态),如果队列中有一个线程修改status为1,则当前线程相对于获取了资源;
(4)在对status修改的时候使用的是CAS操作,保证了多线程情况下的原子性;
15.说一下ReentrantLock的实现原理?
(1)ReentranLock是支持可重入的锁,调用lock方法获取锁之后,可在调用lock,是不会阻塞的;
(2)ReentranLock主要利用CAS+AOS队列(重点讲)实现的;
(3)它支持公平锁和非公平锁,在提供的无参构造器默认是非公平锁,也可以在有参构造器传参设置为公平锁;
16.synchronized和Lock有什么区别?(重点)
(1)语法层面:synchronized是关键字,源码在JVM中的本地方法区,用C++实现的;而Lock是接口,源码有JDK提供,用Java语言实现的;
(2)功能层面:二者都属于悲观锁,都具备基本的互斥、同步、锁冲入功能。但Lock提供了很多synchronized不具备的功能,比如公平锁,可打断,可超时,多条件变量;Lock有适合不同场景的实现,如ReentrantLock,ReentranReadWriteLock;
(3)性能方面:在没有竞争请款下,synchronized做了很多优化,如偏向锁,轻量级锁;如果竞争比较激烈,Lock的实现通常会提供更好的性能;
17.死锁产生的原因是什么?(重点)
死锁是指多个线程(或进程)因循环等待资源而永久处于阻塞的状态,其产生需要同时满足以下四个条件:
(1)互斥:资源一次只能被一个线程独立占用;
(2)保持和请求:线程持有至少一个资源,同时请求被其他线程持有的资源;
(3)不可剥夺:资源只能由持有者主动释放,不可被强势剥夺;
(4)循环等待:存在一个线程等待链,每个线程都在等待下一个线程所持有的资源;
说白一点,就是比如线程1持有锁A并等待锁B,线程2持有锁B并等待锁A,双方处于僵持状态导致死锁。
18.能说一下ConcurrentHashMap吗(重点)
(1)顶层数据结构:JDK1.7底层采用分段的数组+链表实现;JDK1.8跟HashMap底层结构一样,采用数据+链表/红黑树;
(2)加锁的方式:JDK1.7采用的是Segment分段锁,底层是ReentrantLock; JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑树首结点,目的是防止多个线程同时执行put( ) 方法,可能同时定位到同一个桶的空位置,导致后写入的数据覆盖掉前写入的数据问题。
19.导致并发程序出现问题的原因是什么?
(1)原子性破坏:一个操作被线程调度器打断,非原子性操作暴露在中间状态,可以通过synchronized、ReentrantLock锁解决;
(2)可见性问题:线程对共享变量的修改未能及时同步到主内存,其他线程看到的是过时数据,可以通过volitile 、锁来解决;
(3)有序性破坏:编译器和CPU的指令排序导致代码执行顺序与预期不符,可以通过volatile 解决;
20.线程池的执行流程知道吗?(重点)
线程池是管理并复用线程的机制,核心目标是通过减少线程创建/销毁的开销来提升系统性能,执行流程如下:
(1)先调用execute 或submit方法提交任务;
(2)判断核心线程数是否已满,未满就添加到工作线程集合中并执行,若核心线程已满就将任务添加到阻塞队列;
(3)接着判断阻塞队列是否已满,未满在任务队列等待执行;
(4)如果阻塞队列已满,会判断当前线程数是否大于最大线程数,是的话就根据拒绝策略处理,否则将创建非核心线程(临时线程)执行任务;
(5)另外,如果核心或临时线程完成任务会检查阻塞队列中是否有需要执行的线程,如果有,则使用临时线程执行任务;
21.你知道有哪些阻塞队列吗?
(1)ArrayBlockingQueueP:基于数组结构的有界阻塞队列,FIFO
(2)LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO
(3)DelayedWorkQueue:是一个优先级队列,保证每次出队的任务都是当前队列中执行时间最靠前的;
(4)SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待其他元素移出队列;
22.如何确定核心线程数量?
(1)如果是高并发、任务执行时间短的情况,核心线程数 = CPU核数+1,减少线程上下文切换;
(2)并发不高、任务执行时间长的情况:
- IO密集型的任务,核心线程数 = CPU核数*2+1
- 计算密集型的任务核心线程数 = CPU核数+1
(3)并发高、业务执行时间长的情况,解决的关键不在于线程池而在于整体架构的设计,看业务某些数据能否做缓存是第一步,增加服务器是第二步
23. 线程池的种类有哪些?
(1)FixedThreadPool(固定大小线程池),线程池中的线程数是固定的(核心线程数等于最大线程数),超出的线程会在任务队列中等待,直到有线程空闲。适用于处理大量短时间任务,且希望保持一定数量的工作线程进行处理;
(2)SingleThreadExecutor(单线程化线程池),线程池只有一个线程,保证所有的任务按照指定顺序(FIFO)执行,适用场景如日志记录,任务调度;
(3)CachedThreadPool(缓存线程池),线程池的线程数量没有固定的上限(核心线程数为0,最大线程数为Integer.MAX_VALUE),根据需要创建新的线程,如果线程空闲超过60秒(默认),线程就会被回收。适用于执行大量短生命周期的任务(短时间的I/O密集型任务);
(4)ScheduledThreadPool(定时任务线程池),线程数量是固定的,由核心线程数量决定,提供了定时任务调度的能力,支持定时任务、延迟任务、周期任务执行。适用于需要定时执行任务的场景;
24.为什么不推荐用Executors创建线程池?
(1)固定大小线程池和单线程化线程池设置的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
(2)缓存线程池创建的最大线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM;
(3)因此推荐使用通过ThreadPoolExecutor的方式创建线程池,可以自己根据需要设置核心线程数,最大线程数,阻塞队列长度。
25.如何在某个方法中限制并发线程的数量?
在多线程中提供了一个工具类Semaphore信号量,通过Semaphore来限制某个方法中的并发线程数。首先创建Semaphore对象,初始化并发下最大线程数,调用acquire( ) 可以请求一个信号量,总信号量就会减一,如果没有信号量,该线程就会被阻塞直到有其他线程释放;调用release()释放一个信号量,总信号量加一;
26.谈一下你对ThreadLoca的理解?(重点)
ThreadLocal是Java中用于实现线程本地存储的类,它允许每个线程拥有独立的变量副本,从而在多线程环境中实现数据隔离,避免多线程同时访问共享变量时出现的竞态条件和同步问题。
底层实现:ThreadLocal依赖于每个线程内部的ThreadLocalMap,调用set方法就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中。调用get方法,就是以ThreadLocal自己作为key,到当前线程查找关联的资源。调用remove方法,就是以ThreadLocal自己作为key,移除当前线程查找关联的资源。
内存泄漏问题:ThreadLocalMap中的key是弱引用,但value是强引用,ke会被GC释放内存,但value关联的内存不会被释放,因此建议主动remove释放key和value