1.快速失败(fail-fast)
设计的目的是为了避免在遍历时对集合进行并发修改,从而引发潜在的不可预料的错误。
通过迭代器遍历集合时修改集合: 如果你使用Iterator
遍历集合,然后直接使用集合的修改方法(如add()
或remove()
),集合的结构会发生变化,而迭代器并没有同步感知到这些变化,就会抛出ConcurrentModificationException
。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
list.remove(element); // 修改了集合结构,抛出 ConcurrentModificationException
}
}
通过增强型 for 循环遍历集合时修改集合: 增强型 for
循环实际上是基于迭代器的语法糖,因此在遍历过程中如果修改了集合,同样会抛出 ConcurrentModificationException
。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String element : list) {
if (element.equals("B")) {
list.remove(element); // 抛出 ConcurrentModificationException
}
}
如何避免异常
使用迭代器的remove()
方法: 如果需要在迭代过程中删除元素,可以使用迭代器自带的remove()
方法,它会安全地修改集合,并更新迭代器的状态,不会抛出异常。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
if (element.equals("B")) {
iterator.remove(); // 使用迭代器的 remove() 方法,安全移除元素
}
}
避免在遍历时修改集合: 如果你必须在遍历时修改集合,可以考虑先将要删除的元素收集到一个临时列表中,遍历完成后再删除它们。
List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
List<String> toRemove = new ArrayList<>();
for (String element : list) {
if (element.equals("B")) {
toRemove.add(element); // 将要删除的元素暂存
}
}
list.removeAll(toRemove); // 之后统一删除
使用并发集合类: 在多线程环境中,如果多个线程同时对集合进行操作,推荐使用并发集合类,如CopyOnWriteArrayList
或ConcurrentHashMap
,它们支持线程安全的修改。
List<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
for (String element : list) {
if (element.equals("B")) {
list.remove(element); // 不会抛出异常,因为 CopyOnWriteArrayList 是线程安全的
}
}
2.HashMap和HashTable的区别
HashMap
:非线程安全,允许null键和null值,效率较高。Hashtable
:线程安全,不允许null键和null值,效率相对较低。
3.HashMap的数据结构,put的流程?
HashMap存放的是键值对,数据结构是数组+链表+红黑树,当链表的长度超过8时且HashMap的容量超过64时,链表会自动转成红黑树进行保存。
put流程:通过键来计算哈希值,看哈希值对应的数组位置是否为空,如果为空,则直接插入,如果不为空,则遍历链表,看链表中是否有元素与键值相等,如果相等,则对键值进行更新。如果不存在相等的键值,则在链表末尾处添加该键值对。如果链表长度大于8,数组长度大于64,则链表转成红黑树。当元素达到数组长度75%时,直接扩容两倍。
4.StringBuilder和StringBuffer的区别
5.synchronized
关键字的用法和实现原理?
用法:synchronized
可以用于方法或代码块,确保在多线程环境下,某些代码块在同一时间只有一个线程可以执行。
synchronized (this) {
// 线程安全的代码
}
实现原理:synchronized
会通过获取对象的监视器锁(Monitor Lock)来实现同步,锁住代码块或方法,保证临界区内的线程安全。
6.什么是volatile关键字?它有什么作用?
volatile
关键字是Java中的一个修饰符,用于保证多线程环境下共享变量的可见性。当一个变量被声明为volatile
时,它告诉Java编译器和运行时环境,任何对该变量的读写操作都要直接从主内存中进行,而不是从线程的本地缓存中读取。
写入:如果不使用volatile关键字,线程会将修改的值保存到缓存中,如果使用了volatile关键字,线程会将修改的值刷新到主内存中。保证多线程环境下共享变量的可见性。
读取:如果不使用volatile关键字,线程会从缓存中读取该变量,因为一般线程会将修改的值保存到缓存中。如果使用了volatile关键字,线程就会从主内存中读取修改后的变量值。
volatile
保证了变量的可见性,即一个线程对该变量的修改能立即被其他线程看到。volatile
防止了指令重排序,确保变量的读写顺序在并发环境中是安全的。- 但
volatile
不保证操作的原子性,对于涉及多个步骤的操作仍然可能需要同步或者使用更高级的并发工具类。
7.JVM的内存模型是怎样的?堆内存和栈内存的区别?
- JVM内存模型包括:方法区、堆、虚拟机栈、本地方法栈和程序计数器。
- 堆内存:用于存储所有的对象,堆是线程共享的。
- 栈内存:用于存储局部变量、方法调用等,栈是线程私有的。
8.JVM中的垃圾回收机制是怎样的?
- Eden Space(伊甸园)、
- Survivor Space(幸存者区)、
- Old Gen(老年代)
1.JAVA对象分配的过程?
创建一个对象实例时,JVM首先会在堆内存分配内存空间,大部分情况下,新对象首先会被分配到新生代的Eden区,新生代分为三个区域:一个Eden区和两个Survivor区。新对象通常会分配在Eden区,当Eden满了之后会进行Minor GC,在GC的过程中存活的对象会在两个Survivor区中进行转移和交换。经过多次GC仍然存活的对象会被晋升成老年代,老年代对象主要用来存储长期存活的对象和大对象。
2.新生代和老年代分配的目的?
(1)新生代对象使用复制算法,简单高效,可以快速完成内存回收,减少系统暂停时间。因为大部分对象都是短暂存在的,因此这种策略能够有效处理大量短暂对象的分配和回收。
(2)可以针对不同生命周期的对象采用不同的回收策略,新生代频繁回收,老年代较少地回收,这样的话可以较少Full GC的频率,提升系统的整体性能。
3.垃圾回收算法主要包括:
- 标记-清除算法:遍历所有可达对象标记为存活状态,然后遍历堆内存,把没有被标记的对象视为垃圾进行清理。优点:简单,不需要额外的内存空间。缺点:产生大量内存碎片,效率很低。
- 标记-压缩算法:先标记所有可达对象,把存活对象向一端移动,然后直接清理边界外的内存区域,从而消除碎片。优点:解决标记-清除算法带来的内存碎片问题,相对复制算法减少了内存空间。缺点是复杂度比较高,执行效率比较低,特别是压缩过程中需要移动对象,可能会引起应用程序暂停的时间比较长。
- 复制算法:把内存分为两个相等的区域,每次只使用其中一个区域,当这个区域满了以后,把存活对象复制到另一个区域并清理掉原区域的所有对象。优点:不会产生内存碎片。缺点:额外占用内存空间(内存加倍了),对象频繁复制导致效率问题。
4.详细介绍一下Serial Parallel CMS 和 G1垃圾回收器的主要特点
Serial GC:串行垃圾回收器,适用于单核处理器或者对响应时间要求不高的场景。
Parallel:基于多线程并行垃圾回收,适合高吞吐量的服务器应用或者cpu核心数多的服务器环境。
CMS:并行垃圾回收,但是它把垃圾回收分成四个阶段,尽可能地减少了STW的时间,比较适用于高交互性的应用,比如web服务器,以及对停顿时间有严格要求但是对吞吐量要求相对宽松的场景。
G1:把堆内存划分了多个大小相等的区域,每个区域都可以独立作为年轻代和老年代的一部分,通过并行和并发实现垃圾回收,从而减少停顿时间,另外它还能根据目标停顿时间来动态调整垃圾回收策略,来满足不同应用需求。适合低延迟可预测垃圾回收停顿时间的应用,比如大规模分布式系统、在线交易系统。
9.JVM优化
1.为什么要优化?Eden区过小,young gc会频繁;Eden区过大,young gc时间变长;survior太小,对象过早进入老年代;survior过大造成内存的浪费。
2.如果使用合理的JVM参数配置,大多数情况下是不需要调优的。(2)还是存在少量场景需要调优,可以对JVM核心指标配置监控告警,当出现波动时人为介入分析评估。
3.调优案例:
CMS内存碎片化导致FGC问题
问题现象:C端核心业务在高峰期服务器发生FGC,导致部分请求超时报错,影响用户体验
原因分析:CMS使用标记清除算法,不再进行任何压缩和整理工作,意味着老年代随着应用的运行变得碎片化,碎片过多会影响大对象的分配,虽然老年代还有很大的剩余空间,但是没有连续的空间来分配大对象。长期如此,最终可能会导致FGC的发生。
优化策略:业务低峰期显示触发FGC,优化内存碎片并压缩堆,降低在业务高峰期发生FGC的概率
System.gc(),没有开始-XX:+DisableExplicitC jmap -histo:live pid
优化效果:业务高峰期基本没有出现FGC
10.谈谈对AOP的理解
把公共代码抽象出来成一个切面,然后注入到目标对象中去。通过动态代理的方式,将需要注入切面的对象进行代理,再进行调用的时候,将公共的逻辑直接添加进去,而不需要修改原有的业务的逻辑代码,只需要在原来的业务逻辑基础之上做一些增强功能即可。
切面就是单独用来写日志逻辑的类,类有@aspect注解就是切面,这个类使用切点@Pointcut去指定一个自定义的注解,所有加这个注解的方法在执行前都会先调用有@Pointcut的方法
11.谈谈对IOC的理解
控制反转:在之前是由程序员来控制对象,现在由IOC容器来控制对象。控制实现的过程中所需要的对象和依赖的对象
DI:依赖注入,把对应的属性的值注入到具体的对象中,@Autowired, populateBean完成属性值的注入
容器:存储对象,使用map结构来存储,在spring中一般存在三级缓存,singletonObjects存放完整的bean对象,整个bean的生命周期,从创建到使用到销毁的过程全部都是由容器来管理
Map
结构用于存储和管理单例Bean,具体来说:
-
singletonObjects:这个
Map
存放的是已经创建好的单例Bean对象。当一个单例Bean被创建并初始化完成后,Spring容器会将其放入这个Map
中,以键值对的形式存储,其中键是Bean的名称,值是Bean实例。这样,当需要使用这个Bean时,Spring容器可以直接从这个Map
中获取,而不需要重新创建。 -
earlySingletonObjects:这个
Map
是二级缓存,用于存放早期的单例Bean对象。在Bean创建过程中,如果Bean实现了BeanFactoryAware
接口或有其他原因需要提前引用这个Bean,Spring容器会将尚未完全初始化的Bean放入这个Map
中。 -
singletonFactories:这个
Map
是三级缓存,用于存放单例Bean的工厂对象。在Bean创建过程中,Spring容器会先创建一个工厂对象,然后通过工厂对象来创建Bean。这个Map
中存放的就是这些工厂对象。
12.反射
反射(Reflection)是Java语言中的一种机制,允许程序在运行时动态地检查和操作类、接口、方法、属性等信息。通过反射,程序可以在运行时获取类的结构信息,并能够动态地创建对象、调用方法、访问字段等。反射通常用于框架、库开发和动态代理等场景。
反射的作用
- 动态获取类的信息:可以在运行时获取类的全限定名、构造方法、字段、方法等信息。
- 动态调用方法:在运行时通过反射调用对象的方法,而不需要在编译时确定具体调用的内容。
- 动态访问字段:可以访问对象的私有、受保护或公有字段,并且可以对其进行读取或修改。
- 动态创建对象:不需要提前知道类的名称,可以在运行时根据名称动态地创建类的实例。
反射的使用
Java中的反射机制主要通过java.lang.reflect
包来实现。以下是几个常用的反射类和方法:
Class<?>
:表示一个类的类对象,可以通过它获取类的元数据。Field
:表示类中的字段(成员变量)。Method
:表示类中的方法。Constructor<?>
:表示类的构造函数。
反射的具体例子
1. 获取类的Class
对象
可以通过三种方式获取某个类的Class
对象:
- 通过类的类名:
Class.forName("类的全限定名")
- 通过对象:
对象.getClass()
- 通过类名.class:
类名.class
// 通过类名获取
Class<?> clazz1 = Class.forName("com.example.MyClass");
// 通过对象获取
MyClass obj = new MyClass();
Class<?> clazz2 = obj.getClass();
// 通过类名.class获取
Class<?> clazz3 = MyClass.class;
反射的缺点
- 性能开销大:反射涉及到大量的动态类型检查和方法调用,性能相对于直接调用稍差。
- 安全性问题:反射允许访问私有字段和方法,可能会破坏封装性。
- 编译时检查失效:使用反射时,很多错误会推迟到运行时才会暴露。
13.http请求(put post区别)
-
POST:用于向服务器发送数据,用于创建资源,或者向已存在的资源发送数据。POST 请求通常会导致服务器状态的变化或触发某些动作。
- 语义上是“添加”(append)数据到资源上。
- 常用于提交表单数据、上传文件、处理非幂等操作(即操作可能有不同的结果,每次执行结果不同)。
-
PUT:用于在服务器上创建或替换资源。PUT 请求是幂等的,也就是说,重复的 PUT 请求应产生相同的结果。
- 语义上是“更新”或者“替换”资源。如果资源不存在,则创建资源。
- 常用于更新某个已知的资源,例如通过
PUT /users/123
更新 ID 为 123 的用户信息。
GET:
- 功能:用于请求访问指定的资源。
- 特点:请求参数通常附加在 URL 中,数据不会被修改,适合获取数据。可以被缓存。
14.开启线程的方法 线程的状态
一、开启线程的方法
1. 继承 Thread
类
通过继承 Thread
类并重写其中的 run()
方法来定义线程。
步骤:
- 创建一个继承自
Thread
类的类。 - 重写
run()
方法,将线程执行的代码放入其中。 - 创建线程对象并调用
start()
方法启动线程。
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的任务
System.out.println("Thread is running...");
}
}
public class TestThread {
public static void main(String[] args) {
MyThread t1 = new MyThread(); // 创建线程对象
t1.start(); // 启动线程
}
}
2. 实现 Runnable
接口
通过实现 Runnable
接口并将其传递给 Thread
类的构造函数。
步骤:
- 创建一个实现了
Runnable
接口的类。 - 实现
run()
方法,将线程执行的代码放入其中。 - 将该
Runnable
对象传递给Thread
构造器并调用start()
启动线程。
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的任务
System.out.println("Runnable is running...");
}
}
public class TestRunnable {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable); // 创建线程对象
thread.start(); // 启动线程
}
}
3. 使用匿名内部类
你可以通过匿名内部类的方式直接创建线程。
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous Runnable is running...");
}
});
thread.start(); // 启动线程
}
}
4. 使用 Lambda 表达式
在 Java 8 及以上版本中,使用 Lambda 表达式简化 Runnable
接口的实现。
public class TestThread {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Lambda Runnable is running...");
});
thread.start(); // 启动线程
}
}
二、查看线程的状态
Java 提供了一些方法和枚举来检查线程的状态。可以通过调用 Thread
类的方法来获取线程的当前状态。
1. 线程状态枚举(Thread.State
)
Thread.State
是枚举类型,它定义了线程的六种状态:
- NEW:线程对象已经创建,但尚未调用
start()
方法。 - RUNNABLE:线程正在 Java 虚拟机中运行。
- BLOCKED:线程被阻塞,正在等待监视器锁(同步锁)。
- WAITING:线程无限期等待另一个线程执行特定操作。
- TIMED_WAITING:线程在等待,超时后会被唤醒。
- TERMINATED:线程已完成执行。
2. 获取线程状态的方法
使用 Thread
类中的 getState()
方法来查看线程的状态。
15.Runnable和Callable的区别?
1. Runnable
详细说明:
Runnable
接口定义了一个run()
方法,用于封装并发任务。它不返回任何结果,也不会抛出任何受检异常。public class MyRunnable implements Runnable { @Override public void run() { System.out.println("Runnable is running"); } } public class Main { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程,执行run()方法 } }
2. Callable
详细说明:
Callable
接口定义了一个call()
方法,可以返回任务的执行结果,并且可以抛出异常。通常与ExecutorService
结合使用。- 返回的结果可以通过
Future
对象来获取。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Callable result";
}
}
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get()); // 获取 Callable 的返回值
executor.shutdown();
}
}
16.线程池的工作原理?
(1)初始化线程,使用Executors
或ThreadPoolExecutor
创建,设定参数(核心线程数,最大线程数,消息对列,空闲线程存活时间,时间单位,线程工厂,拒绝策略)
(2)线程池中有一组线程,来一个任务会使用空闲线程来执行任务。
(3)如果运行的线程数已经达到核心线程数时,新来的任务就进入任务队列中等待。
(4)当等待的任务数超出任务队列的长度时且线程数还没有到达最大线程数,线程池就要创建新的线程(非核心线程)来处理新加的任务。
(5)当创建的线程数已经超出最大线程时,新来的任务会被拒绝,线程池采用拒绝策略,可能会抛出异常、丢弃任务、运行任务等。
(6)当非核心线程在没有任务时,超出最长存活时间就会被销毁。
d. 任务执行:
- 线程从任务队列中获取任务并执行。
- 执行完毕后,线程返回线程池,准备接收新的任务。
e. 线程的销毁:
- 如果空闲线程的数量超过核心线程数,并且超过了指定的空闲存活时间,则会被销毁。
- 当线程池关闭时,未完成的任务将根据关闭策略(如
shutdown()
或shutdownNow()
)进行处理。
import java.util.concurrent.*;
//超过3启动等待队列,超过6创建新线程,超过8抛出异常
public class ThreadTest {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(3,5,1L, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
for(int i = 0; i < 8; i++){
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + "办理业务");
});
}
}
}
17.List Set Map的区别?
- List:适合需要按顺序访问元素的场景,如保存用户列表、待办事项等。
- Set:适合需要确保元素唯一的场景,如存储用户的邮箱地址、商品的ID等。
- Map:适合存储键值对关系的场景,如存储用户信息(用户名和对应的用户对象)、产品ID与产品详情的关系等。
18.B+树和红黑树的区别,哪个效果更好,为什么?
索引:帮助MySQL高效获取数据的排好序的数据结构,如二叉树,红黑树,Hash表,B-Tree
红黑树是一种二叉搜索树,当左右子树高度不平衡是会进行调整
B+树是一种多路搜索树,
- 非叶子节点不存储数据,只存储索引,可以放更多的索引(前一层存放的是下一层索引的第一个值)
- 叶子节点包含所有索引字段
- 叶子节点用指针链接,提高区间访问的性能
B+树的效果更好,因为高度更低,查询效率更高。
19.常见的字符串操作
public class StringExample {
public static void main(String[] args) {
String str = " Hello, World! ";
// 1. 字符串长度
System.out.println("Length: " + str.length());
// 2. 去除空格
System.out.println("Trimmed: '" + str.trim() + "'");
// 3. 转换为大写
System.out.println("Uppercase: " + str.toUpperCase());
// 4. 子字符串
System.out.println("Substring: " + str.substring(7));
// 5. 替换字符
System.out.println("Replaced: " + str.replace("World", "Java"));
// 6. 分割字符串
String[] parts = str.split(", ");
for (String part : parts) {
System.out.println("Part: " + part);
}
// 7. 格式化字符串
String formatted = String.format("Name: %s, Age: %d", "John", 30);
System.out.println("Formatted: " + formatted);
}
}
20.JAVA中Array和list之间的转换
Array->list: Arrays.asList()
// 创建一个数组
String[] array = {"apple", "banana", "cherry"};
// 使用 Arrays.asList() 方法将数组转换为 List
List<String> list = Arrays.asList(array);
list->Array: toArray()
Object[] objectArray = list.toArray();
21.面向对象和面向过程的区别
22.类的加载机制?
-
加载 (Loading):
- 从
.class
文件中读取字节码,并将其加载到内存中。加载器会根据类的名称和路径查找对应的.class
文件。
- 从
-
链接 (Linking):
- 验证 (Verification):检查加载的字节码是否符合 Java 虚拟机规范,确保安全性。
- 准备 (Preparation):为类变量分配内存,并设置其默认值。
- 解析 (Resolution):将常量池中的符号引用转换为直接引用,即确定实际的内存地址。
-
初始化 (Initialization):
- 执行类的静态初始化块和静态变量的初始化,设置变量的实际值。这是类加载的最后一步。
Java 中的类加载器主要包括以下几种:
-
引导类加载器 (Bootstrap ClassLoader):
- 负责加载 Java 核心类库,如
java.lang.*
、java.util.*
等。
- 负责加载 Java 核心类库,如
-
扩展类加载器 (Extension ClassLoader):
- 负责加载 JRE 的扩展库(通常在
lib/ext
目录中)。
- 负责加载 JRE 的扩展库(通常在
-
应用类加载器 (Application ClassLoader):
- 负责加载用户类路径(classpath)下的类,包括用户自定义的类。
3. 类加载的双亲委派机制:
- 在类加载过程中,类加载器遵循双亲委派模型,即一个类加载器在尝试加载类时,会首先将请求委派给它的父类加载器。如果父加载器无法加载该类,才会由当前加载器进行加载。这种机制可以防止类的重复加载和冲突。
23.获取一个class对象的方式?
可以通过三种方式获取某个类的Class
对象:
- 通过类的类名:
Class.forName("类的全限定名")
- 通过对象:
对象.getClass()
- 通过类名.class:
类名.class
// 通过类名获取
Class<?> clazz1 = Class.forName("com.example.MyClass");
// 通过对象获取
MyClass obj = new MyClass();
Class<?> clazz2 = obj.getClass();
// 通过类名.class获取
Class<?> clazz3 = MyClass.class;
24.如何获得类的属性?
1. 通过getter
方法获取属性
这是最常见的方式,通常会在类中为每个属性定义相应的getter
方法,供外部代码调用。
public class Person {
private String name;
private int age;
// Getter methods
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.getName(); // 获取属性 name
person.getAge(); // 获取属性 age
}
}
2. 通过反射机制获取属性
Java的反射机制允许我们在运行时获取对象的属性,而不需要直接调用getter方法。通过反射,可以动态地获取和修改类的属性,即使这些属性是私有的。
import java.lang.reflect.Field;
public class Person {
private String name = "John";
private int age = 30;
}
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Person person = new Person();
// 获取属性
Field nameField = person.getClass().getDeclaredField("name");
nameField.setAccessible(true); // 设置访问权限
String name = (String) nameField.get(person);
Field ageField = person.getClass().getDeclaredField("age");
ageField.setAccessible(true);
int age = (int) ageField.get(person);
System.out.println("Name: " + name + ", Age: " + age);
}
}
3. 通过java.beans.Introspector
获取属性
Java中还提供了Introspector
类,可以通过内省机制来获取对象的属性信息。它主要用于Java Beans,提供了对类的属性、方法的描述。
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class Person {
private String name = "John";
private int age = 30;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
for (PropertyDescriptor pd : Introspector.getBeanInfo(Person.class).getPropertyDescriptors()) {
if (pd.getReadMethod() != null && !"class".equals(pd.getName())) {
System.out.println(pd.getName() + ": " + pd.getReadMethod().invoke(person));
}
}
}
}
// 使用 StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
String result = sb.toString();
// 使用 StringBuffer
StringBuffer sbf = new StringBuffer();
sbf.append("Hello");
sbf.append(" ");
sbf.append("World");
String result2 = sbf.toString();
25.怎么获得类的实例?
26.springboot的多环境配置文件是怎么区分的
在 Spring Boot 中,多环境配置文件通常通过不同的配置文件和 spring.profiles.active
属性来区分。
1. 创建配置文件
在 src/main/resources
目录下,你可以创建不同的配置文件,例如:
application.properties
(默认配置)application-dev.properties
(开发环境)application-test.properties
(测试环境)application-prod.properties
(生产环境)
2. 配置 application.properties
在 application.properties
中,可以指定活动的配置文件,例如:
spring.profiles.active=dev
3. 运行时指定活动配置
你可以在启动应用程序时通过命令行参数指定活动的配置文件。例如:
java -jar your-app.jar --spring.profiles.active=prod
4. 使用环境变量
你也可以通过设置环境变量来指定活动的配置文件:
export SPRING_PROFILES_ACTIVE=dev
5. 访问配置属性
在代码中,可以使用 @Value
注解或 @ConfigurationProperties
注解来访问不同环境下的配置属性:
@Value("${some.property}")
private String someProperty;
6. 配置优先级
application.properties
是基础配置。- 环境特定的配置文件(如
application-dev.properties
)会覆盖基础配置。 - 可以根据需要添加其他特定于环境的配置。
27.Spring框架中的单例Bean是线程安全的吗?
Spring中的Bean对象默认是单例的,框架并没有对bean进行多线程的封装管理
如果Bean是有状态的,则线程安全需要开发人员自己来保证,最简单的方法就是将Bean的作用域从Singleton改成prototype,这样每次请求Bean对象相当于是创建新对象来保证线程的安全
有状态表示有数据存储功能,无状态不会存储数据。
28.Bean Factory和Factory Bean有什么区别?
相同点:都是用来创建bean对象的
不同点:使用Bean Factory创建对象需要遵循生命周期,比较复杂,如果只是想要简单自定义一个对象,并交给Spring管理,则可以实现Factory Bean接口。
29.OMM出现的场景
-
堆内存溢出(Java heap space):常见于创建了大量对象且没有及时回收的场景。例如,运行时间过长的程序中,老年代对象一直没有被清理,就会导致堆内存溢出。
-
内存泄漏:程序中的对象没有被正确释放,导致内存不断被占用。例如,未释放的资源、未清理的缓存,或者长时间运行的应用中积累的不再使用的对象。
-
频繁的 Full GC:频繁触发全局垃圾回收(Full GC)可能会导致大量的暂停时间,影响程序性能。如果 Full GC 后仍然无法回收足够的内存,也可能引起内存溢出。
-
递归导致堆栈溢出:深度递归调用使用了大量的栈内存,导致线程的栈空间耗尽。例如,没有正确实现的递归算法或者递归深度超出预期,可能会引起
StackOverflowError
,它是 OOM 的一种特殊形式。
30.HTTP和HTTPS的区别
31.HTTPS建立连接的过程
1.非对称加密得到会话密钥
(1)浏览器发送(TLS版本、加密套件列表、非对称加密套件列表、第1随机数)-->服务端
(2)服务端保存浏览器传过来的随机数,选择相应的套件,并且生成一个服务端随机数。发送(TLS版本,加密套件,第2随机数,证书)-->浏览器。
(3)浏览器收到证书并对证书进行验证,取出证书里服务端的公钥进行保存,生成预主密钥(第3随机数),发送(公钥对预主密钥加密后的)-->服务端。
(4)服务端协商没问题后加密开始,服务端收到加密的预主密钥之后,会用自己的私钥进行解密,得到预主密钥,只有客户端和服务端知道预主密钥,除非私钥被泄露。客户端预主密钥+第1随机数+第2随机数=会话密钥。
(5)服务端也用预主密钥+第1随机数+第2随机数=会话秘钥,各自得到的会话秘钥是相同的。
2.只用用会话密钥进行对称加密
32.对称加密算法有哪些?非对称加密算法有哪些?
对称加密:即加密的密钥和解密的密钥相同,如果有第三方知道加密规则就很容易破解。
非对称加密:公钥加密:服务端拥有成对的私钥和公钥,客户端拥有公钥。客户端使用公钥进行加密,服务端使用私钥进行解密。服务器端使用私钥进行加密,客户端使用公钥解密。但是公钥不能对公钥进行解密,私钥不能对私钥解码,安全性更高。
33.TCP和UDP的区别
34.Redis缓存雪崩,穿透,击穿
缓存击穿:大量用户请求访问热点key,当热点key突然失效,导致请求访问数据库。
解决方案:分布式锁,互斥锁(击穿redis请求数据库是上锁,只有一个线程能抢到锁,操作数据库,查询到数据后,再把数据写到缓存中去)
雪崩是大量的key同时失效,击穿是热点key失效
雪崩是大量商品总共的访问请求次数,击穿是大量用户对同一商品的请求次数
缓存雪崩:缓存同一时间大量失效,导致请求直接访问数据库,导致数据库挂掉
解决方法:(1)设置缓存随机失效时间,使其不在同一时间失效。(2)redis是集群部署,把热点的key平均分布在不同的节点上。(3)不设置缓存失效时间。
缓存穿透:用户请求缓存中没有的数据,导致请求去访问数据库,但是数据库也没有(请求的数据缓存和数据库中都没有)
解决方案: (1)不管数据库查询到的结果是空还是别的值,都放到缓存中,下次同一个请求发来时,就不会穿透缓存。(2)拉黑ip(3)对参数合法性进行校验,如果不合法就过滤掉。(4)使用布隆过滤器。
35.如何保证数据的一致性和可靠性?
读写并发会导致数据不一致
对于数据进行修改操作,有两种方式:
(1)一种是先对数据库进行修改,在删除缓存,这样一般不会出现数据不一致的情况,除非删除缓存操作失败。
(2)先删除缓存,再对数据库进行修改。这会出现数据不一致的情况。如下图所示线程2在线程1删除缓存但还没有更新数据库时,进行数据查询操作并把旧数据放到redis中,这样就会导致数据不一致。此时的解决方案是延迟双删,也就是在执行完更新数据库操作后,延迟等到线程2将将旧数据放到缓存中后对缓存进行再次删除。
对于(1)中删除缓存失败情况,可以使用异步方式,发送异步消息到消息队列中,系统监听mq,一旦监听到某一个redis key删除失败,就执行重试删除
36.Redis的主从同步?读写分离怎么实现的?
在主节点写,从节点能够拿到数据。但不能在从节点上写数据。
redis-cli -p 7002 //连接7002服务器
SLAVEOF 192.168.150.101 7001 //将7002设置成7001的从节点
redis-cli -p 7003 //连接7002服务器
SLAVEOF 192.168.150.101 7001 //将7003设置成7001的从节点
INFO replication //查看主从结构
1.全量同步
(1)slave节点请求增量同步
(2)master检查从节点的replid,发现不一致,拒绝增量同步,执行全量同步
(3)master将完整数据成RDB文件,发送RDB文件到slave
(4)slave清除本地缓存,加载RDB文件
(5)master将RDB期间的命令都保存在repl_bakog中,并持续将log发送给slave
(6)slave执行接收到的命令,保持与master之间的同步
2.增量同步
(1)slave发送同步请求,带上replid和offset(记录了从repl_baglog里读取到哪个位置了)
(2)master判断replid是否一致,发现一致,回复continue
(3)master在repl_baklog里面获得offset后面的数据,发送offset后的命令
(4)slave执行命令
3.面试问题
(1)全量同步和增量同步之间的区别?
全量同步:master将完整的内存数据生成RDB,将RDB文件发送slave,后续命令则记录在repl_baklog,逐个发送给slave。增量同步:slave提交自己的offset到master,master从repl_balog中读取offset之后的命令给slave
(2)什么时候执行全量同步?
slave节点第一次连接到master节点时
slave节点断开太久,repl_bakog中的offset已经被覆盖
(3)什么时候执行增量同步?
slave节点断开又恢复,并且在repl_baklog中能找到offset时
(4)Redis主从节点是长连接还是短连接?
长连接
(5)怎么判断redis某个节点是否正常工作?
Redis是通过ping-pong检测机制,如果有一半以上节点去ping一个节点时没有pong响应则集群认为这个节点挂掉了。
a.每隔1秒发送一次ping命令,如果超过一定时间没有响应则认为是主观下线 b.如果大多数sential都认为实例时主观下线,则判定服务下线
(6)过期key如何处理?
当主节点处理了一个key或者淘汰算法淘汰了一个key,则主节点模拟一条del命令发送给从节点,从节点接收该命令后,执行删除key操作。
(7)replication buffer和repl backlog buffer的区别是什么?
使用时期不同:replication buffer在全量阶段和增量阶段都出现了,主节点会给每个新连接的从节点分配一个replication buffer。repl backlog buffer在增量同步中出现,一个主节点只有一个repl backlog buffer。
两者缓存区满了之后出现的场景不同:replication buffer满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制。repl backlog buffer满了,由于是环形结构,会导致覆盖起始位置数据。
(8)哨兵的作用是什么?(高并发读场景)
a.监控: 不断检查master和slave是否按预期工作
b.自动故障恢复:当master发生故障,哨兵会选择一个slave作为master,故障恢复后也以master为主
c.通知:哨兵充当redis客户端的服务发现来源,当集群发生故障时,会将最新的消息推送给redis客户端
(9)故障转移步骤有哪些?
a.首先选定一个slave作为新的master(根据offset和优先级),执行slaveof no one
b.让其他所有的节点都执行slaveof 新master
c.修复故障节点配置,添加slaveof 新master
(10)JAVA中配置哨兵模式
(11)分片集群结构
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题要解决:
海量数据存储问题
高并发写的问题
使用分片集群可以解决上述问题,分片集群的特征:
集群中有多个master,每个master保存不同数据,解决并发写
每个master有多个slave节点
master之间通过ping检测彼此健康状态
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
37.三级缓存解决循环依赖
循环依赖是指一个或者多个Bean实例之间会存在直接或者间接的依赖关系,构成循环调用。
(1)循环依赖有以下三种
(2)三级缓存:用来存放不同类型的Bean
第一级缓存:完全初始化好的Bean实例,这些实例可以直接使用
第二级缓存:原始的Bean对象,实例化以后还没有设置属性值的Bean实例,没有被依赖注入
第三级缓存:Bean工厂对象,生成原始的Bean对象,放入到二级缓存中
(3)缓存的放置时间和删除时间
三级缓存:createBeanInstance之后:addSingletonFactory
二级缓存:第一次从三级缓存确定对象是代理对象还是普通对象的时候,同时删除三级缓存getSingleton
一级缓存:生成完整对象之后放到一级缓存,删除二三级缓存:addSingleton
(3)核心思想:Bean的实例化和Bean里面的依赖注入进行分离。采用一级缓存存储完整的Bean实例,采用二级缓存存储不完整的Bean实例,通过不完整的Bean实例作为突破口,解决循环依赖问题。第三级缓存主要是解决代理对象的循环依赖问题。
(4)Spring只能解决单实例存在的循环引用问题,以下情况需要人为干预
38.Spring事件监听机制
39.设计模式
(1)工厂模式:在各种BeanFactory以及ApplicationContext创建中都用到了
(2)模板模式:在各种BeanFactory以及ApplicationContext实现中都用到了
(3)代理模式:Spring AOP利用了AspectJ AOP实现的! AspectJ AOP的底层用了动态代理
(4)策略模式:加载资源文件的方式,使用不同的方法,如:ClassPathResource,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的接口Resoure;在Aop的实现中,采用了两种不同的方式,JDK动态代理和CGLIB代理
(5)单例模式:创建Bean的时候
(6)观察者模式:Spring中的ApplicationEvent,ApplicaytionListener,ApplicationEventPublisher
(7)适配器模式:MethodBeforeAdviceAdapter,ThrowsAdviceAdapter,AfterReturningAdapter
(8)装饰者模式:源码中类型带有Wrapper或者Decorator的都是
常见的设计模式是单例模式
单例模式:在整个运行时域,一个类只有一个实例对象
为什么使用单例模式:有的类型的实例对象的创建和销毁对资源来说消耗不大,而有些可以复用的类的对象因频繁创建和销毁对资源消耗很大。(例如创建数据库连接对象)
如何实现单例模式:线程安全、懒加载、可以反射
public class Singleton{
private Singleton(){} //构造器私有,其他类
//无法通过new Singleton来构造实例,只能通过getInstance方法,
//在getInstance方法中首先判断instance
//是否被构造过,如果构造过,则直接返回对象,否则通过new Singleton来创建对象
private static Singleton instance = null; //初始化对象为null
public static Singleton getInstance(){
if (instance == null){
instancre = new Singleton();
}
return instance;
}
}
实例对象在第一次调用时才真正构建的,而不是程序启动时就构建好等待调用,滞后构建(符合懒加载)
不是线程安全的(在if语句处,可能有多个线程同时进入,导致实例化多次)
解决方式1:加上synchronized
public class Singleton{
private Singleton(){} //构造器私有
private static Singleton instance = null; //初始化对象为null
public static synchronized Singleton getInstance(){
if (instance == null){
instancre = new Singleton();
}
return instance;
}
}
缺点:每次构造对象时都需要同步操作,性能低
解决方案:编译时构造,运行时调用
public class Singleton{
private static Singleton instance = new Singleton(); //编译时构造
private Singleton(){}
public static synchronized Singleton getInstance(){ //运行时调用
return instance;
}
}
缺点:饿汉模式,需要改成懒加载
解决方案:只需要在构建的时候加锁,调用时不需要加锁
public class Singleton{
private static Singleton instance;
private Singleton(){} //构造器私有
public static Singleton getInstance(){ // 1
if (instance == null){ // 2
synchronized (Singleton.class){ // 3
instance = new Singleton(); // 4
}
}
return instance;
}
}
缺点:在多个线程执行语句2之后,虽然只有一个线程能抢到锁去执行语句3,但是可能会有其他线程已经进入if代码块在等待,一旦线程a执行完,线程b就会立即获取锁,这样对象就会被创建多次。
解决方案:加判空(双检锁),只要有一个被创建了,后面就不会再创建
public class Singleton{
private volatile static Singleton instance;
private Singleton(){} //构造器私有
public static Singleton getInstance(){ // 1
if (instance == null){
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton(); // 2
}
}
}
return instance;
}
}
在定义实例的时候需要加上volatile修饰符,否则会因为虚拟机指令重排的问题导致多次实例化,在执行2的时候有三个原子操作:(1)memory = allocate //分配内存 (2)ctorinstance(memory) //初始化对象 (3)instance = memory //对象指向内存地址。 经过重排之后可能会出现(1)->(3)->(2)执行顺序,当a线程执行到(3)时,b线程执行到if (instance == null)时,就会出现多次实例化。volatile修饰的变量不会出现指令重排问题。
简洁写法
public class Singleton{
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
利用静态内部类SingletonHolder,在程序启动的时候不会加载,只有在第一次调用的时候才会加载。
40.什么是Spring事务,事务的传播行为?事务的失效场景? Spring事务的隔离级别?Spring事务的隔离级别?
1.Spring事务:
Spring事务是Spring框架提供的一种机制,用于保证一组数组库操作的原子性、一致性、隔离性和持久性。
ACID 特性:
- 原子性:事务中的操作要么全部完成,要么全部不执行。
- 一致性:事务执行前后,数据库状态的一致性得以保持。(就是说如果某个操作如果没执行到,就需要进行事务回滚)
- 隔离性:多个事务并发执行时,彼此之间不会互相影响。
- 持久性:一旦事务提交,结果是永久性的,即使系统崩溃也能保持。
2.事务的传播行为
A调用了B的处理逻辑
把创建新事物比喻成写作业
REQUIRED:A写作业,B就抄作业;A不写作业,B就得自己写作业
SUPPORTS:A写作业,B就抄作业;A不写作业,B就不写作业
MANDATORY:A写作业,B就抄作业;A不写作业,B就打A一顿(抛出异常)
REQUIRED_NEW:不管A写不写作业,B都是要自己写作业的
NOT_SUPPORTED:不管A写不写,B都不写
NEVER:B不写作业,如果A写作业,打A一顿(抛出异常)
NESTED:如果A写作业,则嵌套在A中执行,如果A不写作业,则跟REQUIRED一样
其他问题:
REQUIRED_NEW和NESTED的区别:
REQUIRED_NEW新事物和原事务不是父子关系,因此原事务回滚时,新事务不回滚
NESTED新事务和原事务是父子关系关系,原事务回滚时,新事务也回滚
REQUIRED和NESTED的区别:
REQUIRED在调用方存在事务的情况下,调用方和被调用方共用一个事务,所以被调用出现异常的时候,调用方不管有没有catch都会事务回滚。
NESTED调用方和被调用方是父事务和子事务的关系,所以被调用方发生异常的时候,调用方catch异常,只有子事务回滚,父事务不会回滚
3.事务失效场景
(1)权限问题:如果方法的权限为private,会导致事务失效,spring要求代理的方法必须是public的。
(2)final修饰:spring的事务使用aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。如果使用final修饰方法,那么在代理类中就无法使用该重写方法,而添加事务功能。使用static也没法通过动态代理变成事务方法。
(3)方法内部调用:非事务方法调用事务方法,非事务方法直接调用了this对象方法,没有通过aop生成代理对象。
(4)没有被spring管理。没有加上@Controller @Service @Component @Repository等注解来创建bean实例。
(5)多线程调用:多线程调用时,线程间的数据是隔离的,多线程调用一个方法的话,会产生多个ThreadLocal,从而产生多个数据库连接,数据库链接不一样,导致事务不一样,所以事务就失效了。
(6)表不支持事务:myisam不支持,改成innodb
(7)未开启事务
(8)传播特性设置成Never:不支持事务,如果有事务则抛出异常,只有REQUIRED,REQUIRED_NEW,NESTED会创建新事物
(9)使用try catch捕获了异常:开发者自己捕获了异常,但没有抛出,把异常吞掉了,spring认为程序是正常的,所以没有回滚。
4.Spring事务的隔离级别
(1)事务隔离级别:用来解决并发事务所产生的一些问题
read uncommitted:读未提交,出现脏读的问题
read committed:读已提交,解决脏读问题(行锁,读不加上锁)
repeatable read:可重复读,解决不可重读问题(行锁,读写都加上锁)(MySQL默认级别)
serializable:可串行化,解决幻影读问题(表锁)
数据库和spring代码的隔离级别不同:以Spring的配置为主
(2)存在的问题:
脏读:事务1在修改数据的过程中,事务2读取事务1,事务1修改后并未提交,进行了事务回滚,导致事务2出现了脏数据。读已提交就可以解决这个问题,加上行锁,只对写加上锁,写的时候不允许其他事务来进行操作。
不可重复读(针对的某一条数据或者某几条数据):事务2在事务1的两次查询中对数据进行了更改,导致在同一个事务中两次查询的结果不一致。可重复读可以解决这个问题,为该字段对应行加上锁(行锁),可以使在事务1执行期间中不允许其他事务对该字段进行更新。
幻影读(针对整张表)
事务1在查询的过程中,事务2对表中的数据进行了修改,导致事务1前后两次查询不一致。所以使用serializable级别,对整张表加一个锁,在执行的过程中,禁止其他事务对该表进行修改。(表锁)
MVCC是事务隔离级别的无锁的实现方式,用于提高事务的并发性能
5.Spring事务的实现方式原理是什么?
事务操作是AOP的一个核心体现,当一个方法添加@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当使用这个代理对象的方法的时候,如果有事务处理,那么会先开启事务,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,事务管理器会在业务逻辑执行完毕后自动提交事务。如果出现异常,直接进行回滚操作,用户也可控制哪些异常进行回滚。
41.Spring、Springmvc、Springboot的区别
(1)总结
Spring:框架,容器,生态
Springmvc:以容器化技术完成了MVC框架
Springboot:
(1)实现了自动配置
(2)起步依赖:定义了对其他库的传递依赖,这些东西加在一起支持某项功能。
(3)辅助功能:提供了嵌入式服务器、外部配置等。
将tomcat.jar内置了进来,通过main方法启动容器,达到一键开发部署(只需要将当前的运行的项目打成一个jar包,放到服务器里,然后运行java -jar jar包名即可运行,不需要部属一个额外的web服务器 )
提供start POMs来简化Maven配置和减少版本冲突所带来的问题
对Spring和第三方库提供默认配置,也可以修改默认值,简化框架配置
无序配置XML--JavaConfig,无代码生成,开箱即用
(2)详细区别
Spring:是一个IOC容器,用来管理Bean,使用依赖注入实现控制反转,可以很方便地整合各种框架,提供AOP机制弥补OOP的代码重复问题、更方便将不同方法中共同处理抽取成切面、自动注入给方法执行,比如日志、异常。
Springmvc:是Spring对web框架的一个解决方案,提供了一个总的前端控制器Servlet,用来接受请求,然后定义了一套路由策略(url到handle的映射)及适配执行handle,将handle结果使用视图解析技术生成视图展现给前端。
Springboot:是spring提供的一个快速开发工具包,让程序员能更方便、更快速的开发spring+springmvc应用。简化了配置(约定了默认配置),整合了一系列的解决方案(starter机制)、redis、mongnadb、es,可以开箱即用。
42.MVC模式
模型:处理数据验证、逻辑和持久性
视图:处理怎样显示信息
控制器:在模型和视图之间传递数据
43.Spring的核心是什么?Spring的优势是什么?
1.核心
Spring是一个开源框架,使得开发变得简洁优雅;是一个IOC和AOP的容器框架
IOC:控制反转(原来对象由自己创建,现在由容器控制)
AOP:面向切面编程(业务无关的功能,如日志,事务配置,可通过切面切入到业务逻辑中,而不需要一个个改)
容器:包含并管理应用对象的生命周期
2.优势
(1)低侵入式,代码污染度极低
(2)独立于各种应用服务器,基于Spring框架的应用,可以真正实现Write Once, Run Anywhere。
(3)IOC容器降低了业务对象替换的复杂性,提高了组件的解耦
(4)Spring AOP支持将一些通用任务如安全、事务、日志等进行集中式处理,从而提供了更好的复用。
(5)ORM和DAO提供了与第三方持久层框架的良好整合,简化了底层的数据库访问
(6)高度开放性,开发者可以自由选用spring框架的部分或全部
44.Spring支持的bean作用域有哪些?Spring bean的生命周期?
1.bean的作用域
(1)singleton:该属性定义Bean时,IOC容器仅创建一个Bean实例,IOC每次返回的都是同一个Bean实例。
(2)prototype:该属性定义Bean时,IOC容器可以创多个Bean实例,每次返回的都是新实例。
(3)request:该属性仅对HTTP请求产生作用,该属性定义Bean时,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext环境。
(4)session:仅用于HTTP Session,同一个Session共享一个Bean实例,不同的seesion使用不同的实例
(5)global-session:该属性仅用于HTTP Session,同Session作用域不同的是,所有session共享一个Bean实例。
2.bean的生命周期
(1)实例化Bean对象 :通过反射的方式,此时只是在堆中开辟内存空间,属性都是默认值
(2)设置对象属性:根据XML配置文件和注解,给对象中的属性设置值
(3)检查Aware相关接口并设置相关依赖:如果对象中需要引用容器内部的对象,那么需要调用aware接口的子类方法来进行统一的设置(假设有一个bean需要访问Spring容器中的DataSource
对象,可以通过实现BeanFactoryAware
接口,然后在setBeanFactory
方法中获取DataSource
对象。)
(4)BeanPostProcessor前置处理:在Bean的初始化之前的操作
- 检查Bean的某些条件或状态。
- 修改Bean的属性值。
- 根据特定条件动态更换Bean的实现。
- 增加额外的逻辑,比如日志记录、安全检查等。
(5)检查是否是InitializingBean以决定是否调用afterPropertiesSet方法:afterPropertiesSet在属性设置完成后,对属性进行一些操作,如打开网络连接、初始化资源、执行复杂的启动逻辑等。
(6)检查是否配置有自定义的init-method方法:如果当前bean对象定义了初始化方法,那么在此处调用初始化方法
(7)BeanPostProcessor后置处理:在Bean对象初始化之前的操作
- 对Bean进行进一步的配置或增强。
- 应用AOP相关的逻辑,比如创建代理对象。
- 执行Bean初始化后的一些清理或验证工作。
- 为Bean添加额外的功能,比如性能监控。
(8)注册必要的Destruction相关回调接口:为了方便对象的销毁,在此处调用注销的回调接口,方便对象进行销毁操作
(9)是否实现DisposableBean接口
(10)是否配置自定的destory方法
45.如何实现一个IOC容器?
IOC意味着将设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制
(1)先准备一个基本的容器对象,包含一些map结构的集合,用来方便后续过程中存储具体的对象。
(2)进行配置文件的读取工作或者注解工作,将需要创建的bean对象都封装成BeanDefinition对象存储在容器中。
(3)容器将封装好的BeanDefinition对象对通过反射的方式进行实例化,完成对象的实例化
(4)进行对象的初始化操作,也就是给类中的对应属性值进行设置,也就是进行依赖注入,完成整个对象的创建,变成一个完整的bean对象,存储在容器的某个map结构中
(5)通过容器对象来获取对象,进行对象的获取和逻辑处理工作
(6)提供销毁操作,当对象不用或者容器关闭的时候,将无用的对象进行销毁
46.Spring注解有哪些?分别有什么作用?
数据库相关
47.char和varchar的区别
48.数据库的sql失效一般有哪些场景
1.场景
https://www.51cto.com/article/702691.html
“模型数空运最快”
“模”:模糊查询like"%**"
“型”:数据类型不一致(想查的值与参数的类型不一致)
explain select * from t_user where id_no = 1002; // id_no为varchar,1002为int
“数”:对索引使用内部函数
explain select * from t_user where SUBSTR(id_no,1,3) = '100'; //模型要进行全表扫描,拿到值进行截取后再比较
“空”:索引中有空值
SELECT * FROM employees WHERE email IS NULL;
“运”:索引列进行四则运算
explain select * from t_user where id + 1 = 2 ; //数据库先遍历全表,进行运算之后,再进行比较
//优化方式
-- 内存计算,得知要查询的id为1
explain select * from t_user where id = 1 ;
-- 参数侧计算
explain select * from t_user where id = 2 - 1 ;
“最”:复合索引不按索引最左开始查找
“快”:全表查找比索引查找速度快
一些解释:
SQL 失效通常指的是在数据库中,原本可以使用的 SQL 查询突然变得无法执行或效率降低。以下是一些常见的场景:
1.联合索引不满足最左匹配原则
第一条是符合最左匹配原则,第二条不符合最左匹配原则。
索引成立的条件,叶子节点和根节点都是有序的,可以通过二分查找法来查找对应的值。
二分查找的前提是有序,如果去掉第一个索引的要求,第二个索引在B+数上就是无序的,没法找到符合要求的值,无法进行二分查找,因此索引失效,只能进行全表扫描。
2.范围查找的右边索引会失效
EXPLAIN SELECT * from test_user where a > 1 and b =1
b是无序的,所以索引失效
如何将a>1改成a=1,此时b是有序的,所以索引不会失效。也就是说在a相同的情况下,b才有序。
3.like查询失效
EXPLAIN SELECT * from test_user where a like "1%" //索引有效
EXPLAIN SELECT * from test_user where a like "%1%" //索引失效 查找任意位置有1的数据
EXPLAIN SELECT * from test_user where a like "%1" //索引失效
除了第一位有顺序,后面是没有顺序的,所以在没有顺序的二叉树上进行搜索,索引会失效。
49.联表查询有哪些方式?
1.内连接(INNER JOIN):返回两个表中匹配的部分,如果一个表在另一个表中没有匹配值,则不返回。
SELECT a.*, b.*
FROM table_a a
INNER JOIN table_b b ON a.common_field = b.common_field;
2.外连接
(1)左外连接(LEFT JOIN):返回左表的全部记录,右表中匹配的记录返回,不匹配的填为NULL
SELECT a.*, b.*
FROM table_a a
LEFT JOIN table_b b ON a.common_field = b.common_field;
(2)右外连接(RIGHT JOIN):返回右表的全部记录,左表中匹配的记录返回,不匹配的填NULL
SELECT a.*, b.*
FROM table_a a
RIGHT JOIN table_b b ON a.common_field = b.common_field;
(2)全连接(FULL OUTER JOIN):返回两个表的全部记录,如果某个表没有匹配记录,则返回NULL
SELECT a.*, b.*
FROM table_a a
FULL OUTER JOIN table_b b ON a.common_field = b.common_field;
50.MyBatis的优点?
1.在 MyBatis 中,SQL 语句可以通过 XML 文件进行配置,如下所示:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<select id="selectUserById" resultType="com.example.domain.User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
2.MyBatis 支持动态 SQL,例如 <if>
标签,使得 SQL 语句可以根据条件构建:
<select id="selectUsers" resultType="com.example.domain.User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
3.MyBatis 可以配置缓存以提高性能:
<mapper namespace="com.example.mapper.UserMapper">
<cache/>
<!-- Cache configuration can be done here -->
</mapper>
4.可以进行事务管理
@Transactional
public void updateUser(User user) {
userMapper.update(user);
// 其他数据库操作...
}
5.MyBatis 允许自定义复杂的结果映射
<select id="selectUserWithRoles" resultType="com.example.domain.User">
SELECT u.*, r.role_name
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE u.id = #{id}
</select>
这个查询将用户信息和角色信息映射到 User
对象中,展示了 MyBatis 在处理复杂查询时的灵活性。
51.Mybatis中#和$的区别?
在绑定参数时,#会为用户的输入添加引号,防止SQL注入攻击。$不会添加引号,存在SQL注入攻击的风险。
<select id="selectUserByName" resultType="User">
SELECT * FROM users WHERE name = #{name}
</select>
SELECT * FROM users WHERE name = 'John Doe'
<select id="selectUserById" resultType="User">
SELECT * FROM users WHERE id = ${id}
</select>
//用户输入的 id 是 1 OR 1=1
SELECT * FROM users WHERE id = 1 OR 1=1
52.UNION和UNION ALL的区别?
-- 使用 UNION (会去除重复的行),时间慢
SELECT name, department FROM employees
UNION
SELECT name, department FROM managers;
-- 使用 UNION ALL (不会去除重复的行),时间快
SELECT name, department FROM employees
UNION ALL
SELECT name, department FROM managers;
53.MySQL主从同步?主从同步的时候有几个线程处理主从同步?
54.MySQL有几种引擎?
55.数据库删除有几种实现方式?
Delete:删除表中的一行或者多行,不释放空间
Truncate:删除表里的所有数据,释放空间
Drop:删除表的内容和定义,释放空间,如果想要添加数据就得重建表格
删除数据的速度:Drop>Truncate>Delete
delete删除后会记录到事务中,可以进行回滚,Truncate不能进行回滚
delete不释放空间,删除后id仍然从删除处增加;Truncate释放空间,删除后,id从0开始增加