文章目录
-
目录
文章目录
提问问题
问题1
问题2
问题3
问题4
问题5
问题6
问题7
问题8
问题9
问题10
问题11
问题12
问题13
问题14
问题15
问题16
问题17
问题18
问题19
写在最后
提问问题
项⽬的⽤户数据怎么存储的,存储在哪⾥,⽤的什么数据结构?索引的设计思想
四种引⽤,强引⽤、软引⽤、弱引⽤、虚引⽤分别介绍
String、Stringbuilder、Stringbuffer的区别,String s = "abc"和String s = new String("abd")⼀样吗
序列化时,某些字段不想被序列化,怎么做?⽤什么注解
线程⽣命周期
创建线程的⽅式
常⽤的线程池有哪些,使用场景分别是?
Sleep和Wait的区别
Java中加锁的⽅式有哪些?可重⼊锁是什么意思?谁是可重⼊锁?
介绍JVM内存结构,哪些是线程私有的?哪些会发⽣OOM?
Java类加载过程介绍
双亲委派机制是什么?tomcat中的是这样的吗?
如何判断垃圾是否该被回收?介绍常⽤的垃圾回收算法
什么时候触发完全回收full GC
Spring启动过程、分哪些步骤
Spring @Autowired和@Resource有什么区别?
Spring bean作⽤域介绍
AOP简单介绍,AOP的⼏个主要核⼼概念是什么?AOP中⼀些常⽤的注解的作⽤
Redis缓存雪崩和缓存穿透,介绍,如何解决这些问题?
问题1
用户数据存储常见:
关系型数据库(RDBMS):
- 如MySQL、PostgreSQL、Oracle等,使用表格来组织数据。每个表由多行(记录)和多列(字段)组成。
- 数据结构:通常使用B树或其变种B+树作为索引结构,以优化查询性能。
- 索引设计:主要目的是加快数据检索速度,减少磁盘I/O操作。设计索引时会考虑查询频率高的列、唯一性、组合索引等因素。
非关系型数据库(NoSQL):
- 如MongoDB、Cassandra、Redis等,根据不同的使用场景提供灵活的数据模型。
- 数据结构:
- 文档型数据库(如MongoDB)使用JSON或BSON格式存储文档,索引可建立在文档内的字段上。
- 键值存储(如Redis)使用键值对来存储数据,索引即为键。
- 列族存储(如Cassandra)将数据分组存储,索引可建立在列族的列上。
- 索引设计:根据数据的访问模式和查询需求来设计索引。例如,MongoDB中可以为文档中的某个字段创建索引以加速查询。
在设计用户数据存储时,需要考虑数据的一致性、完整性、安全性以及备份恢复等因素。
索引的设计思想
主要是为了提高数据检索效率,减少不必要的数据扫描和排序操作,从而提升应用的性能。但也要注意过度索引可能会导致维护成本增加和写操作性能下降。
问题2
在Java中,引用类型决定了垃圾收集器如何处理对象。每种引用类型都有其特定的用途,选择合适的引用类型可以帮助开发者更好地管理内存,避免内存泄漏,并实现特定的功能需求。
以下是四种不同类型的引用及其特点:
强引用(Strong Reference):
强引用是最常见的引用类型。只要强引用还存在,对象就不会被垃圾收集器回收。
对象的生命周期完全依赖于强引用,只要至少存在一个强引用指向该对象,它就不会被垃圾回收。
Object obj = new Object(); //这里的obj就是一个强引用。
软引用(Soft Reference):
软引用允许其所引用的对象被垃圾收集器回收,以回收内存。
当JVM内存不足时,软引用指向的对象会被回收。
软引用经常用在实现内存敏感的缓存中,比如LRU缓存。
SoftReference<Object> softRef = new SoftReference<>(new Object());
弱引用(Weak Reference):
弱引用比软引用更弱,无论当前内存是否足够,只要进行垃圾回收,弱引用指向的对象就一定会被回收。
弱引用主要用于实现不影响对象存活期的引用情况。
弱引用常用于实现对象的身份识别,比如在HashMap中用作键时,避免内存泄漏。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
虚引用(Phantom Reference):
虚引用是最弱的引用类型。一个对象引用被设置为虚引用后,它仍然可以被垃圾回收。
虚引用必须和引用队列(ReferenceQueue)一起使用。当垃圾收集器决定回收对象时,如果对象上有虚引用,垃圾收集器会在回收对象前,将这个虚引用加入到与之关联的引用队列中。
虚引用主要用于跟踪对象被垃圾回收的活动,通常很少使用。
ReferenceQueue<Object> refQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), refQueue);
问题3
String、StringBuilder和StringBuffer都是Java中用于处理字符串的类,但它们在性能和用途上有所不同:
String:
String类的内容是不可变的。
当创建一个新的String对象时,如果该对象的内容已经存在于堆内存中,则不会创建新的对象,而是引用已有的对象。
因为String对象不可变,所以每次对String进行修改实际上都会生成一个新的String对象,这会导致额外的内存分配和垃圾回收,影响性能。
StringBuilder:
StringBuilder类的内容是可变的。
StringBuilder内部使用字符数组来存储字符串,可以在不产生新对象的情况下直接修改字符串内容。
StringBuilder是非线程安全的,适用于单线程环境下的字符串操作。
StringBuffer:
StringBuffer类的内容也是可变的,与StringBuilder类似。
StringBuffer内部同样使用字符数组来存储字符串。
StringBuffer是线程安全的,它的方法都是同步的,可以在多线程环境下安全使用,但因此在性能上略低于StringBuilder。
关于两个声明方式:
String s = "abc";
String s = new String("abc");
第一行代码中的s
是对字面量"abc"
的引用。如果堆内存中已存在相同内容的String对象,则不会创建新对象,而是引用已有的对象。
第二行代码中的s
是通过new
关键字创建了一个新的String对象,即便堆内存中已存在相同内容的对象,也会创建一个新的对象。第二种方式总是会在堆内存中创建一个新的String对象。
问题4
可以使用`@Transient`注解标记该字段。带有`@Transient`注解的字段会被忽略,从而不会被序列化到输出的流中。
import java.io.Serializable;public class Example implements Serializable {private static final long serialVersionUID = 1L;private String fieldToSerialize;@Transientprivate String fieldNotToSerialize;
}
在上述代码中,`fieldToSerialize`会被序列化,而`fieldNotToSerialize`由于被`@Transient`注解标记,将不会被序列化。
问题5
线程的生命周期通常包括以下几个状态:
新建(New):
线程对象被创建后,尚未开始执行。此时线程已经存在,但还未调用start()方法。
就绪(Runnable):
调用了线程的start()方法后,线程进入就绪状态,准备执行。此时线程已经分配了CPU时间,但还未实际运行。如果有其他线程正在使用CPU,那么这个线程将等待CPU时间片,在操作系统的调度下获得执行。
运行(Running):
线程获得CPU时间并开始执行run()方法中的代码。此时线程处于活跃状态,执行着程序计数器指向的指令。
阻塞(Blocked):
当线程因为某些原因放弃CPU使用权时,会进入阻塞状态。阻塞的原因可能有三种:
- 等待阻塞:执行wait()方法,线程等待其他线程执行notify()或notifyAll()。
- 同步阻塞:线程在获取synchronized锁失败时,会进入同步阻塞状态。
- 其他阻塞:线程执行sleep()或join()方法,或者发送了请求打断信号而暂停执行。
等待(Waiting):
一个处于阻塞状态的线程,只有在收到通知或中断信号,或者其等待的超时时间到达时,才会返回到可运行状态。
超时等待(Timed Waiting):
与等待状态类似,但线程是在指定的时间内等待。例如,通过Thread.sleep(long millis)或Object.wait(long timeout)方法。超时时间到达后,线程会返回可运行状态。
终止(Terminated):
线程的run()方法执行完毕,或者因异常退出了run()方法,线程结束生命周期。
线程一旦终止,就不能再次启动。如果需要再次执行,必须创建一个新的线程对象。
问题6
在Java中创建线程主要有以下几种方式:
1.继承Thread类:
通过创建一个继承自
Thread
类的子类并重写其run()
方法来实现线程的功能。然后通过该类的实例调用start()
方法启动线程。
class MyThread extends Thread {public void run() {// 线程要执行的任务}
}MyThread t = new MyThread();
t.start();
2.实现Runnable接口:
创建一个实现了
Runnable
接口的类,并重写其run()
方法。然后创建该类的实例,并将该实例作为参数传递给Thread
类的构造函数,创建Thread
对象,最后通过start()
方法启动线程。
class MyRunnable implements Runnable {public void run() {// 线程要执行的任务}
}Thread t = new Thread(new MyRunnable());
t.start();
3.实现Callable接口:
类似于实现
Runnable
接口,但是Callable
接口允许线程返回结果,并且可以抛出异常。实现Callable
接口后,使用FutureTask
类包装Callable
对象,并传入ExecutorService
来执行。
class MyCallable implements Callable<Integer> {public Integer call() throws Exception {// 线程要执行的任务,返回结果return 123;}
}ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
executor.shutdown();
// 获取线程执行结果
try {Integer result = future.get();
} catch (InterruptedException | ExecutionException e) {e.printStackTrace();
}
4.使用线程池:
通过
ExecutorService
接口的实现类,比如Executors
工厂类提供的线程池方法来创建线程。线程池可以有效管理线程资源,避免创建过多线程导致的资源浪费。
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.execute(new Runnable() {public void run() {// 线程要执行的任务}
});
executor.shutdown();
5.使用Lambda表达式:
在Java 8及以上版本中,可以利用Lambda表达式简化实现
Runnable
接口的代码,特别是当Runnable
接口的实现非常简单时。
Thread t = new Thread(() -> {// 线程要执行的任务
});
t.start();
通常情况下,推荐使用实现
Runnable
接口或Callable
接口的方式,因为这样可以避免单继承的限制,并且可以使线程的逻辑与线程的创建和管理分离,提高代码的可读性和可维护性。
问题7
在Java中,常用的线程池主要包括以下几种,它们分别适用于不同的使用场景:
-
FixedThreadPool(固定大小线程池):
- 使用
Executors.newFixedThreadPool(int nThreads)
创建。 - 特点:线程数固定,不会随着任务的增减而改变。
- 适用场景:适合任务量相对稳定且数量不多的场景,可以避免频繁创建销毁线程带来的性能损耗。
- 使用
-
CachedThreadPool(缓存线程池):
- 使用
Executors.newCachedThreadPool()
创建。 - 特点:线程数不固定,空闲线程最多存活60秒后会被自动终止,任务量大时会创建新线程,任务量减少时会回收线程。
- 适用场景:适合任务量波动较大,且CPU资源充足的场景,可以根据实际负载动态调整线程数量。
- 使用
-
SingleThreadExecutor(单线程池):
- 使用
Executors.newSingleThreadExecutor()
创建。 - 特点:只有一个工作线程,保证任务按顺序执行,不会并发执行。
- 适用场景:适合需要串行执行任务的场景,如读写文件操作等,可以保证线程安全。
- 使用
-
ScheduledThreadPool(定时线程池):
- 使用
Executors.newScheduledThreadPool(int corePoolSize)
创建。 - 特点:除了具有普通线程池的特性外,还支持定时和周期性任务的执行。
- 适用场景:适合需要定期执行任务的场景,如定时清理缓存、定时检查更新等。
- 使用
-
WorkStealingPool(工作窃取线程池):
- 使用
Executors.newWorkStealingPool()
创建。 - 特点:采用工作窃取算法,适用于大量计算任务,能够平衡负载,提高CPU利用率。
- 适用场景:适合CPU密集型任务的场景,可以充分利用多核处理器的优势。
- 使用
问题8
sleep()
和 wait()
都是Java中用于线程控制的方法,但它们有以下主要区别:
-
所属类不同:
sleep()
是Thread
类的静态方法,可以直接通过Thread.sleep(millis)
调用。wait()
是Object
类的实例方法,必须在同步方法或同步块中调用,以确保对共享资源的访问是线程安全的。 -
锁的处理不同:
sleep()
不会释放当前对象的锁,即使线程在sleep()
方法期间,其他线程仍然无法调用该对象的同步方法或同步块。wait()
会释放当前对象的锁,使得其他线程可以进入同步方法或同步块,获取该对象的锁并执行。 -
唤醒机制不同:
sleep()
会在指定的时间后自动唤醒线程。wait()
需要其他线程显式地调用同一对象上的notify()
或notifyAll()
方法来唤醒等待的线程。 -
使用目的不同:
sleep()
通常用于短暂地暂停执行,例如在轮询循环中暂停一段时间,或者模拟延迟。wait()
通常用于线程之间的协作,当一个线程需要等待某个条件成立时,它会调用wait()
方法进入等待状态,直到另一个线程改变条件并调用notify()
或notifyAll()
方法。 -
异常处理不同:
sleep()
抛出的是InterruptedException
,表示在等待过程中线程被中断。wait()
同样抛出InterruptedException
,表示在等待过程中线程被中断,并且还可能抛出IllegalMonitorStateException
,表示线程没有获得对象的锁就调用了wait()
方法。
问题9
Java中的加锁方式主要有以下几种:
synchronized关键字:
同步方法:在方法声明前添加synchronized
关键字,锁定当前对象。同步块:在代码块前添加synchronized
关键字,锁定当前对象。ReentrantLock类:
提供比synchronized
更灵活的锁机制,如尝试非阻塞地获取锁、可中断地等待锁、公平锁等。ReadWriteLock接口:
由ReentrantReadWriteLock
类实现,提供读锁(共享锁)和写锁(独占锁)。允许多个线程同时持有读锁,但写锁是互斥的。StampedLock类(Java 8引入):
提供了三种锁模式:读锁、写锁和乐观读锁。支持尝试非阻塞地获取锁,并提供了锁的超时版本。Condition接口:
与ReentrantLock
配合使用,提供了更加灵活的线程间通信机制。
可重入锁(Reentrant Lock)
是指同一个线程可以多次获取同一把锁,每次获取锁后都会增加锁的持有计数,直到锁被释放次数与获取次数相等,锁才真正被释放。
Java中的
ReentrantLock
和synchronized
关键字都是可重入锁。
问题10
Java虚拟机(JVM)的内存结构可以分为几个主要区域:
方法区 (Method Area):
- 存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 在HotSpot VM中,方法区又称为永久代(PermGen),但是在Java 8及以后版本中,已经被元空间(Metaspace)取代。
堆 (Heap):
- 所有线程共享的一块内存区域,用于存储所有创建的对象实例。
- 是垃圾收集器管理的主要区域,也是最常发生内存溢出(OutOfMemoryError)的地方。
虚拟机栈 (Java Virtual Machine Stack):
- 线程私有的内存区域,每个方法执行时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError
。本地方法栈 (Native Method Stack):
- 线程私有的内存区域,为虚拟机使用到的Native方法服务。
- 类似于虚拟机栈,每个线程也会创建一个本地方法栈,其生命周期与线程相同,当线程请求的栈深度大于虚拟机所允许的最大深度时,将抛出
StackOverflowError
。程序计数器 (Program Counter Register):
- 线程私有的内存区域,保存了当前线程所执行的字节码的行号指示器。
- 线程切换时,程序计数器也会随之切换,但不会发生
OutOfMemoryError
。
在JVM中,可能发生OOM的区域主要有堆和方法区(元空间)。
堆内存溢出通常是因为申请的对象超出了堆的最大容量,而方法区(元空间)内存溢出则可能是由于类和方法的数量过多导致的。
对于虚拟机栈和本地方法栈,通常情况下,它们的大小是由虚拟机启动参数决定的,如果分配给这些区域的内存过小,可能会导致
StackOverflowError
,但并不是OOM。
问题11
Java类加载是一个复杂的过程,主要分为以下几个步骤:
加载(Loading):
- 通过类的全限定名获取类的字节流,这个字节流可以来自多种不同的数据源,比如本地的.class文件、JAR文件、网络资源等。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类各种数据的访问入口。连接(Linking):
- 验证:确保Class文件的字节流中包含的信息符合虚拟机规范,没有安全方面的问题。
- 准备:为类的静态变量分配内存,并设置类变量的默认初始值。
- 解析:将常量池内的符号引用转换为直接引用。
初始化(Initialization):
- 执行类构造器
<clinit>()
方法的过程,该方法不需要显式编写,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生。- 在多线程环境下,类的初始化是线程安全的,Java虚拟机会保证一个类被完全加载和初始化之后,才允许其他线程去初始化这个类。
类加载过程中,JVM采用双亲委派模型进行类的加载,这意味着当一个类加载器收到类加载的请求时,它首先会委托给父类加载器去尝试加载这个类,如果父类加载器无法加载,子类加载器才会自己尝试加载。这个机制保证了Java的核心类库只会被加载一次,并且提高了安全性。
问题12
双亲委派机制
当一个类加载器被请求加载一个类时,它首先不会自己去尝试加载这个类,而是将这个请求委托给其父类加载器去完成。如果父类加载器不能加载该类,子加载器才会尝试自己去加载。
这种机制的好处在于,它保证了Java类库的唯一性,确保了标准类库只会被加载一次,避免了类的冲突。此外,它还提供了安全性,因为父加载器通常加载更为信任的类,如Java核心API类。只有在父加载器无法找到或者加载该类时,才会让子加载器去尝试加载。
在Tomcat中,类加载机制与标准的Java类加载器一样,Tomcat对双亲委派模型进行了一些扩展和调整,仍然遵循了双亲委派模型的基本原则。Tomcat使用了一种叫做“分层类加载”的机制,它将类加载器分为几个层次:
- Bootstrap ClassLoader:负责加载Java核心库,如rt.jar等。
- Common ClassLoader:负责加载Tomcat的核心库,如servlet API等。
- Catalina ClassLoader:负责加载Tomcat容器的核心组件。
- Shared ClassLoader:负责加载Web应用共享的库。
- Webapp ClassLoader:针对每个Web应用单独存在,负责加载该Web应用的类库。
问题13
在Java虚拟机(JVM)中,判断垃圾是否应该被回收通常依赖于可达性分析算法(Reachability Analysis),即从根集合(Root Set)开始,沿着应用程序中的引用链向上搜索,直到遇到不可达的对象。
根集合通常包括以下几种类型的对象:
- 活跃线程中的局部变量表。
- 活跃线程的输入参数。
- 活动方法的返回值。
- 类静态变量。
如果一个对象不再被任何根集合中的对象所引用,那么这个对象就被认为是垃圾,可以被回收。
常用的垃圾回收算法包括:
标记-清除(Mark-Sweep):
- 首先标记所有活动对象。
- 清除所有未标记的对象。
- 缺点是容易产生内存碎片。
复制(Copying):
- 将内存分为两个相等的区域,每次只使用其中一个。
- 标记活动对象,并将它们复制到另一个区域。
- 清理未使用的区域。
- 优点是实现简单,不容易产生内存碎片,但牺牲了一半的内存作为临时空间。
标记-整理(Mark-Compact):
- 标记活动对象。
- 移动所有活动对象到内存的一端,从而整理出连续的空闲内存空间。
- 优点是减少了内存碎片,但实现比复制算法复杂。
分代收集(Generational Collection):
- 根据对象存活的周期将内存分为几代,如新生代(Young Generation)、老年代(Old Generation)和永久代(PermGen,已在Java 8中被元空间Metaspace取代)。
- 大多数新创建的对象都在新生代中存活时间短暂。
- 老年代中的对象存活时间较长。
- 分代收集算法主要在新生代中使用复制算法,在老年代中使用标记-清除或标记-整理算法。
增量/并行/并发收集(Incremental/Parallel/Concurrent Collection):
- 为了减少垃圾回收时的停顿时间,这些算法尝试在后台并发地进行垃圾回收工作。
- 增量回收将垃圾回收任务分成多个小的部分,逐步执行。
- 并行回收使用多个垃圾回收线程来同时处理垃圾回收任务。
- 并发回收则允许应用程序和垃圾回收器并发运行,减少了因垃圾回收而导致的停顿时间。
问题14
在Java虚拟机(JVM)中,完全回收(Full GC)通常在以下情况下触发:
老年代空间不足:当老年代(Tenured Generation)的空间不足时,无法再为新对象分配空间,这时会触发Full GC来清理老年代以及整个堆内存,并尝试回收更多的空间。
方法区(Metaspace,在Java 8之前为Permanent Generation)空间不足:如果JVM中的方法区(用于存储类元数据、常量、静态变量等)满了,也会触发Full GC。在Java 8及以后版本中,如果Metaspace区域满了,同样会触发Full GC。
系统请求:通过调用
System.gc()
方法可以向JVM建议执行垃圾回收,但这并不保证Full GC一定会被触发,这只是一个提示,实际执行取决于JVM的实现。配置的GC策略:某些垃圾收集器(比如CMS收集器)可能在特定的回收阶段结束后,为了整理内存碎片或进行老年代回收而触发Full GC。
并发模式下的Full GC:在并发垃圾收集器(如CMS或G1)中,可能在并发阶段未能及时回收足够的内存,或者并发回收阶段完成后仍需执行一次完整的回收来清理剩余的垃圾,这时也会触发Full GC。
异常情况:例如内存泄露,导致内存不断增加,最终耗尽,也会触发Full GC。
值得注意的是,频繁的Full GC通常是一个不好的信号,可能意味着存在内存泄漏或者JVM的垃圾收集器配置不当。理想情况下,Full GC应该尽量少发生,以避免对应用程序性能造成显著影响。
问题15
Spring框架的启动过程可以分为以下几个步骤:
加载Spring应用上下文:Spring应用的启动是从加载Spring应用上下文开始的。应用上下文负责初始化和管理Spring容器中的所有bean。
注册Spring容器:根据配置信息(XML配置、注解配置或基于Java的配置),容器会注册所有的bean定义。这包括类的扫描、bean的创建和配置等。
实例化Bean:Spring容器根据定义的bean定义信息,实例化所有的bean。
装配Bean:Spring容器将通过依赖注入(DI)机制,将bean之间的依赖关系装配起来。这可能涉及属性设置、构造器调用等。
初始化Bean:在所有的属性都被注入之后,Spring容器会调用bean的初始化方法,比如
InitializingBean
接口的afterPropertiesSet()
方法,或者自定义的初始化方法。注册Bean后处理器:Spring允许用户注册Bean后处理器,这些处理器会在bean初始化之后、容器完成初始化之前被调用,用于进一步处理bean。
启动监听器:Spring容器提供了启动监听器机制,允许用户在容器启动时和关闭时执行特定的操作。
准备环境:Spring Boot等现代Spring框架会进一步提供环境准备功能,如设置数据源、配置属性源、加载外部配置等。
应用准备就绪:完成上述所有步骤后,Spring应用就准备好了,可以对外提供服务。
对于Spring Boot应用来说
启动过程还包括了自动配置的加载和引导(如执行SpringApplicationRunListener的事件回调),以及可能的应用准备和应用就绪事件的触发。
问题16
@Autowired
和@Resource
是Spring框架中两个不同的依赖注入注解,它们用于自动装配bean之间的依赖关系,但是它们之间有一些关键的区别:
来源:
@Autowired
是Spring自己的注解,用于自动装配bean。@Resource
是Java标准的注解,属于JSR-250,也可以用于自动装配bean,但它不仅仅限于Spring。注入方式:
@Autowired
默认按类型进行自动装配,如果有多个相同类型的bean存在,可以通过@Qualifier
指定具体的bean名称。@Resource
默认按名称进行自动装配,如果没有指定名称,它会按类型来查找bean。如果需要按类型装配,可以使用@Resource(type = YourClass.class)
。注解作用域:
@Autowired
可以作用于字段、构造函数、setter方法等。@Resource
主要用于字段和setter方法。必须性:
@Autowired
注解的使用是可选的,如果没有找到匹配的bean,会抛出异常。@Resource
注解的使用是必需的,如果没有找到匹配的bean,它不会抛出异常,而是会将null注入到字段中。处理未找到bean的方式:
@Autowired
默认情况下,如果找不到对应类型的bean,会抛出异常(NoSuchBeanDefinitionException)。@Resource
默认情况下,如果找不到对应名称的bean,会注入null值,除非设置autowireMode
为Resource.AUTWIRE_REQUIRED
(类似于@Autowired
的行为)。
问题17
在Spring框架中,bean的作用域定义了bean的生命周期以及它们在应用程序中的可见性。
Spring提供了多种不同的作用域,每种作用域都适用于不同的场景。以下是Spring中常见的六种bean作用域:
singleton:默认作用域。Spring容器中只创建一个共享的bean实例,无论你请求多少次,都会得到同一个实例。
prototype:每次请求时都会创建一个新的bean实例。也就是说,每次调用
getBean()
方法时,都会获得一个全新的对象。request:每个HTTP请求都会有一个与之关联的bean实例。这个bean只在当前的HTTP请求内有效,请求结束后,bean会被销毁。
session:在一个HTTP Session中,bean的生命周期和Session的生命周期一致。这意味着,在同一个Session中,无论请求多少次,都会得到相同的bean实例。
application:在一个ServletContext范围内,只创建一个bean实例。这个作用域适用于那些需要在整个web应用中共享的资源。
websocket:在单个WebSocket会话中,只创建一个bean实例。这个作用域适用于需要在单个WebSocket会话中共享的资源。
问题18
AOP(面向切面编程)是一种编程范式,旨在通过将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,来提高模块化。横切关注点是那些影响多个类和模块的功能,例如日志记录、安全性、事务管理等。在AOP中,这些横切关注点被封装成独立的模块,称为“切面”(Aspect)。
AOP的几个主要核心概念包括:
切点(Pointcut):定义了哪些连接点(Join Point)需要被切面关注,即哪些方法需要被拦截。
通知(Advice):也称为增强(Advising),它定义了在切点匹配的连接点上执行的操作。常见的通知类型包括前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。
目标对象(Target Object):被一个或多个切面所通知的对象。
切面(Aspect):包含通知和切点的模块。它将横切关注点封装起来,对业务逻辑的影响最小化。
织入(Weaving):是将切面连接到其他应用类型或对象上,并创建一个被通知的目标对象的过程。织入可以在编译时、加载时或运行时完成。
在Spring AOP中,常用的注解包括:
@Aspect
:标记一个类作为一个切面。
@Pointcut
:定义一个切点表达式。
@Before
:定义前置通知,在目标方法执行前运行。
@After
:定义后置通知,在目标方法执行后运行。
@AfterReturning
:定义返回通知,在目标方法正常执行后返回结果时运行。
@AfterThrowing
:定义异常通知,在目标方法抛出异常时运行。
@Around
:定义环绕通知,它包围了目标方法的执行,可以在方法执行前后做额外的处理。
@EnableAspectJAutoProxy
:开启基于注解的自动代理,通常在配置类上使用。
问题19
Redis缓存雪崩和缓存穿透是两种常见的缓存失效问题,它们会导致大量的请求直接落到数据库上,可能引起数据库压力过大甚至宕机。
缓存雪崩:
缓存雪崩是指在某一个时间段内,缓存集中过期,导致大量的并发请求直接访问数据库。这通常发生在以下几种情况:
- 缓存中的多个键在同一时间过期;
- 系统负载突然增大,导致缓存的命中率下降;
- 缓存服务器宕机,所有的缓存数据丢失。
缓存穿透:
缓存穿透是指查询的数据在缓存中不存在,同时在数据库中也不存在(例如查询的是一个不存在的ID),导致每次请求都需要访问数据库,从而绕过缓存层,给数据库带来压力。
解决方案:
缓存雪崩:
- 随机过期时间:不要将所有的缓存键设置为同一时间过期,可以设置一个随机的过期时间,避免大量缓存同时失效。
- 热点数据永不过期:对于热点数据,可以设置永不过期,或者设置更长的过期时间。
- 设置热点数据的备份:对于非常重要的热点数据,可以设置多份拷贝,即使一部分缓存失效,也不会影响数据的访问。
- 使用互斥锁或分布式锁:在缓存失效后,通过锁机制控制并发量,避免大量并发请求同时查询数据库。
缓存穿透:
- 布隆过滤器(Bloom Filter):在写入缓存之前,先使用布隆过滤器判断数据是否存在,如果不存在则不写入缓存,直接返回,减少对数据库的访问。布隆过滤器虽然有一定的误报率,但可以极大地减少无效请求。
- 缓存空对象:当查询的数据不存在时,将一个空对象缓存起来,设置一个较短的过期时间,这样后续相同的查询请求就可以直接从缓存中获取空对象,避免访问数据库。
- 缓存NoSQL:使用一些支持范围查询的NoSQL数据库作为缓存,例如Cassandra、Redis的有序集合(Sorted Set)等,可以有效地解决部分缓存穿透问题。
- 背景刷新:对于可能存在缓存穿透风险的数据,可以通过后台任务定期刷新缓存,而不是等待用户请求时才去查询数据库。
写在最后
PS:以上是网络上收集的一些常见的问题以及自己对答案搜索整理;一次整理大概在10-25个问题之间,适合对自己的知识的查缺补漏
面试一般根据岗位要求或者简历上写的来进行扩展提问,也有些是直接问公司常用到的相关方面的技术问题,无论怎么准备都祝大家能拿到心怡的offer!