1.spring 动态代理
Spring框架中的动态代理技术主要用于实现AOP(面向切面编程)功能,它使得在不修改原有业务逻辑代码的基础上,能够通过代理对象对目标方法进行增强或拦截。Spring提供了两种主要的动态代理机制:
-
JDK动态代理:
- JDK动态代理基于Java的
java.lang.reflect.Proxy
类和InvocationHandler
接口实现。 - 当需要代理的目标类实现了至少一个接口时,Spring会选择使用JDK动态代理来创建代理对象。
- 代理对象的方法调用会被转发到InvocationHandler实现类中定义的invoke方法,在invoke方法中可以添加额外的横切逻辑,如事务管理、日志记录等。
- JDK动态代理基于Java的
-
CGLIB动态代理:
- CGLIB是一个强大的高性能的代码生成库,它能够在运行期扩展Java类与实现接口。
- 当目标类没有实现任何接口或者希望代理其final方法时,Spring会采用CGLIB动态代理。
- CGLIB通过生成目标类的子类并在子类中重写父类方法的方式实现代理,并在方法调用前后插入切面逻辑。
在Spring AOP配置中,默认情况下如果目标类有接口,则使用JDK动态代理;若无接口则自动转为CGLIB动态代理。当然,开发者也可以根据需要明确指定使用哪种代理方式。
2、遇到过哪些设计模式?
在学习⼀些框架或中间件的底层源码的时候遇到过⼀些设计模式:
- 代理模式:Mybatis中⽤到JDK动态代理来⽣成Mapper的代理对象,在执⾏代理对象的⽅法时会去执⾏SQL,Spring中AOP、包括@Configuration注解的底层实现也都⽤到了代理模式
- 责任链模式:Tomcat中的Pipeline实现,以及Dubbo中的Filter机制都使⽤了责任链模式
- ⼯⼚模式:Spring中的BeanFactory就是⼀种⼯⼚模式的实现
- 适配器模式:Spring中的Bean销毁的⽣命周期中⽤到了适配器模式,⽤来适配各种Bean销毁逻辑的执⾏⽅式
- 外观模式:Tomcat中的Request和RequestFacade之间体现的就是外观模式
- 模板⽅法模式:Spring中的refresh⽅法中就提供了给⼦类继承重写的⽅法,就⽤到了模板⽅法模式
3 责任链模式
责任链模式(Chain of Responsibility Pattern)是一种行为设计模式,它用于创建一个对象链,并让请求沿着这个链传递直至被处理。这种模式鼓励将多个接收者对象链接起来形成一条链,并且每个接收者都有机会处理请求,这样客户端就无需知道请求的具体处理者是谁。
在责任链模式中:
- 有多个处理者对象,它们通过组合的方式连接成一条链。
- 每个处理者都包含对下一个处理者的引用,使得请求可以沿着链进行传递。
- 当一个请求到来时,首先由链上的第一个处理者对象进行处理。
- 如果当前处理者能够处理该请求,则处理之;否则,它会将请求转发给下一个处理者对象。
- 这种传递过程会一直持续到找到能处理请求的处理者或者到达链的末端为止。
责任链模式的主要优点包括:
- 解耦:发送者与接收者之间不需要直接相互依赖,请求发送者只需将请求发送给链的起始点即可。
- 灵活:可以在运行时动态地修改责任链的结构,即添加或移除处理者对象,来改变请求处理的逻辑。
- 开放封闭原则:系统可以在不修改原有类的基础上增加新的处理者来应对新的需求。
使用场景包括但不限于:
- 权限系统中的角色权限分配和检查;
- 日志记录系统的日志级别过滤;
- 审批流程中多级审批人依次审核;
- GUI事件分发等。
在Java中,责任链模式的实现可以是一个抽象处理者类与一系列具体处理者类的组合。下面是一个简单的日志记录系统实例,其中不同的日志级别对应不同类型的日志处理器,并通过责任链传递日志请求:
Java
1// 抽象处理者接口或抽象类
2public abstract class Logger {
3 protected Logger nextLogger; // 持有下一个日志处理器
4
5 public void setNext(Logger nextLogger) {
6 this.nextLogger = nextLogger;
7 }
8
9 // 抽象方法,处理日志请求
10 public abstract void logMessage(int level, String message);
11
12 // 其他辅助方法...
13}
14
15// 具体处理者1:ERROR级别日志处理器
16public class ErrorLogger extends Logger {
17 @Override
18 public void logMessage(int level, String message) {
19 if (level == Logger.ERROR) {
20 System.out.println("Error: " + message);
21 // 如果设置了下一个处理器,则传递请求给它
22 if (nextLogger != null) {
23 nextLogger.logMessage(level, message);
24 }
25 }
26 }
27}
28
29// 具体处理者2:WARNING级别日志处理器
30public class WarningLogger extends Logger {
31 @Override
32 public void logMessage(int level, String message) {
33 if (level == Logger.WARNING) {
34 System.out.println("Warning: " + message);
35 if (nextLogger != null) {
36 nextLogger.logMessage(level, message);
37 }
38 }
39 }
40}
41
42// 具体处理者3:INFO级别日志处理器
43public class InfoLogger extends Logger {
44 @Override
45 public void logMessage(int level, String message) {
46 if (level == Logger.INFO) {
47 System.out.println("Info: " + message);
48 if (nextLogger != null) {
49 nextLogger.logMessage(level, message);
50 }
51 }
52 }
53}
54
55// 客户端代码示例
56public class Client {
57 public static void main(String[] args) {
58 // 创建一个日志处理器链
59 Logger errorLogger = new ErrorLogger();
60 Logger warningLogger = new WarningLogger();
61 Logger infoLogger = new InfoLogger();
62
63 // 链接处理器
64 infoLogger.setNext(warningLogger);
65 warningLogger.setNext(errorLogger);
66
67 // 发送日志请求
68 infoLogger.logMessage(Logger.INFO, "This is an info message.");
69 infoLogger.logMessage(Logger.WARNING, "This is a warning message.");
70 infoLogger.logMessage(Logger.ERROR, "This is an error message.");
71 }
72}
在这个例子中,当客户端发送一条日志信息时,它首先会经过InfoLogger
,如果级别匹配则输出该日志并继续传递到下一级别(警告和错误)。如果日志级别不匹配,则直接传递给下一个合适的处理器。这样就形成了一个可以根据日志级别动态处理日志的责任链。
4 ⼯⼚模式
工厂模式(Factory Pattern)是一种创建型设计模式,它提供了一种用于创建对象的接口,让子类决定实例化哪一个类。这种模式使代码在不指定具体产品类的情况下创建产品对象成为可能,将实际的产品创建逻辑推迟到运行时。
在Java中,工厂模式通常有以下几种实现形式:
-
简单工厂模式(Simple Factory):
- 定义一个工厂类,该类可以根据传入参数的不同返回不同类型的对象。
1public class ShapeFactory { 2 public static Shape getShape(String type) { 3 if ("CIRCLE".equals(type)) { 4 return new Circle(); 5 } else if ("RECTANGLE".equals(type)) { 6 return new Rectangle(); 7 } else if ("SQUARE".equals(type)) { 8 return new Square(); 9 } 10 throw new IllegalArgumentException("Invalid shape type"); 11 } 12} 13 14// 简单工厂会创建具体的形状类如Circle、Rectangle和Square
-
工厂方法模式(Factory Method):
- 定义一个抽象工厂类,其中包含一个创建产品的抽象方法;由子类决定生成哪种产品对象。
1public abstract class ShapeFactory { 2 abstract Shape createShape(); 3} 4 5public class CircleFactory extends ShapeFactory { 6 @Override 7 Shape createShape() { 8 return new Circle(); 9 } 10} 11 12public class RectangleFactory extends ShapeFactory { 13 @Override 14 Shape createShape() { 15 return new Rectangle(); 16 } 17} 18 19// 工厂方法模式下,每个子类负责创建一种特定的形状对象
-
抽象工厂模式(Abstract Factory):
- 提供一个接口,用于创建相关或依赖对象家族的一系列对象,而不需要指定具体类。
1interface ShapeFactory { 2 Shape createShape(); 3 Color getColor(); 4} 5 6class ShapeAndColorFactory implements ShapeFactory { 7 @Override 8 Shape createShape() { 9 return new Circle(); 10 } 11 12 @Override 13 Color getColor() { 14 return new RedColor(); 15 } 16} 17 18// 抽象工厂模式可以用来创建一系列相关的对象集合,例如这里的Shape和Color对象
工厂模式的主要优点包括:
适配器模式的应用场景包括但不限于:
- 将对象的创建过程封装起来,隐藏了对象的具体创建细节,使得客户端只需关注接口而不是实现细节。
- 便于扩展新的产品类型,只需要新增对应的工厂子类即可,符合开闭原则(Open-Closed Principle, OCP)。
- 有助于解耦客户端与具体产品之间的直接联系,提高了系统的灵活性和可维护性。
-
5 适配器模式
-
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许将一个接口转换为客户希望的另一个接口。这种转换使得原本由于接口不兼容而无法协同工作的类可以一起工作。适配器的主要目的是在不修改原有类的基础上解决接口之间的不匹配问题。
适配器模式有几种实现方式:
-
类适配器:
- 通过继承的方式,适配器类继承自需要被适配的类,并实现目标接口。
1// 假设我们有一个遗留接口和类 2public class LegacyClass { 3 public void legacyMethod() {...} 4} 5 6// 目标接口 7public interface TargetInterface { 8 void targetMethod(); 9} 10 11// 类适配器,继承自LegacyClass并实现TargetInterface 12public class ClassAdapter extends LegacyClass implements TargetInterface { 13 @Override 14 public void targetMethod() { 15 // 在这里调用legacyMethod()并进行必要的转换以满足targetMethod() 16 legacyMethod(); 17 } 18}
-
对象适配器:
- 通过组合而非继承,适配器包含一个对需要被适配的对象实例的引用,并实现目标接口。
1public class ObjectAdapter implements TargetInterface { 2 private final LegacyClass legacy; 3 4 public ObjectAdapter(LegacyClass legacy) { 5 this.legacy = legacy; 6 } 7 8 @Override 9 public void targetMethod() { 10 // 使用内部持有的legacy对象的方法来实现目标接口方法 11 legacy.legacyMethod(); 12 // 可能还包括其他转换逻辑 13 } 14}
- 当您需要使用现有的类库中的类,但其接口不符合您的项目需求时。
- 当您想复用一些已存在的类,但是它们与当前系统的接口不兼容时。
- 需要将一组接口转换为另一组接口时,保持原系统的核心功能不变,同时提供新的接口给客户端使用。
6 外观模式
外观模式(Facade Pattern)是一种结构型设计模式,它为子系统中的一组接口提供了一个统一的、更高级别的接口,使客户端与子系统的交互更加简单。通过定义一个高层接口来隐藏系统的复杂性,并向客户端提供了一个更为简单的接口。
在Java中的实现示例:
Java
1// 子系统类或模块
2public class SubSystemA {
3 public void operationA1() {
4 // 执行操作 A1
5 }
6
7 public void operationA2() {
8 // 执行操作 A2
9 }
10}
11
12public class SubSystemB {
13 public void operationB1() {
14 // 执行操作 B1
15 }
16
17 public void operationB2() {
18 // 执行操作 B2
19 }
20}
21
22// 外观类,简化了对子系统的访问
23public class Facade {
24 private final SubSystemA subSystemA;
25 private final SubSystemB subSystemB;
26
27 public Facade() {
28 this.subSystemA = new SubSystemA();
29 this.subSystemB = new SubSystemB();
30 }
31
32 // 提供一个高层次的接口来调用子系统的方法
33 public void performComplexOperation() {
34 subSystemA.operationA1();
35 subSystemB.operationB1();
36 subSystemA.operationA2();
37 subSystemB.operationB2();
38 }
39}
40
41// 客户端代码
42public class Client {
43 public static void main(String[] args) {
44 Facade facade = new Facade();
45 facade.performComplexOperation();
46 }
47}
在这个例子中,Facade
类充当了外观角色,它封装了子系统SubSystemA
和SubSystemB
的操作。客户端只需调用Facade
提供的performComplexOperation()
方法即可完成一组复杂的操作,无需直接与子系统进行交互,从而降低了系统的耦合度,简化了使用过程。
7 Java死锁如何避免
Java中避免死锁的方法主要有以下几个方面:
-
避免循环等待资源:
- 确保所有的线程按照固定的顺序获取锁。例如,如果线程A需要先获得锁A再获取锁B,那么线程B也应遵循相同的顺序获取锁,而不是随意交替。
-
一次性申请所有资源:
- 在线程开始执行前一次性请求它所需要的全部资源,而不是分步去获取。
-
超时释放锁:
- 使用
tryLock()
方法配合超时机制尝试获取锁,这样即使在一段时间内无法获取到所需的锁,也不会无限期地等待下去,从而避免死锁的发生。
- 使用
-
避免持有锁时进行不必要的操作:
- 在持有锁的代码块中尽量减少操作,尽快释放已获取的锁,以减少死锁发生的可能性。
-
资源有序化:
- 对系统中的所有资源进行排序,并确保线程在申请资源时总是按照这个顺序进行,这样可以防止循环等待条件形成。
-
使用锁层次结构:
- 如果有多个相关的锁,始终按照同一顺序来获取它们,避免嵌套锁时出现死锁。
-
避免死锁检测和恢复策略:
- 通过编写逻辑检查是否即将进入死锁状态,或者采用更复杂的死锁检测算法(如Wait-Die、Wound-Wait等),并在检测到可能的死锁情况时采取措施如回滚事务或释放部分资源以打破死锁循环。
-
合理使用
Thread.join()
:- 当一个线程依赖于另一个线程的结果时,正确使用
join()
方法可以避免死锁的发生,确保在调用join()
之前不会继续请求其他资源。
- 当一个线程依赖于另一个线程的结果时,正确使用
以下是一个简单的示例说明如何使用tryLock()
避免死锁:
Java
1ReentrantLock lock1 = new ReentrantLock();
2ReentrantLock lock2 = new ReentrantLock();
3
4void method() {
5 if (lock1.tryLock()) {
6 try {
7 // 成功获取lock1后尝试获取lock2
8 if (lock2.tryLock()) {
9 try {
10 // 执行临界区操作...
11 } finally {
12 lock2.unlock();
13 }
14 } else {
15 // 如果不能立即获取lock2,则释放lock1
16 lock1.unlock();
17 }
18 } finally {
19 // 在退出该方法前确保释放lock1
20 if (lock1.isHeldByCurrentThread()) {
21 lock1.unlock();
22 }
23 }
24 }
25}
请注意,以上只是基本策略,实际应用中可能会根据具体场景选择不同的解决方法。同时,在设计并发程序时,还需要结合适当的同步机制与合理的业务逻辑处理,全面考虑以降低死锁的风险。
8 深拷⻉和浅拷⻉
深拷贝和浅拷贝是计算机科学中,特别是面向对象编程语言中的概念,主要涉及到对象或数据结构的复制过程。
- 浅拷贝(Shallow Copy):
- 浅拷贝是指复制对象时,只复制对象本身,并不复制它所引用的对象。换言之,当一个对象A包含对其他对象B的引用时,拷贝A得到的新对象A'与原对象A拥有相同的引用,指向同一对象B。
- 在Java中,如果直接使用
Object.clone()
方法或者通过赋值运算符(如=
)进行拷贝,通常实现的是浅拷贝。
例如:
Java
1public class MyClass implements Cloneable {
2 private String name;
3 private AnotherClass another;
4
5 // 其他代码...
6
7 public MyClass clone() throws CloneNotSupportedException {
8 return (MyClass) super.clone();
9 }
10}
11
12MyClass obj1 = new MyClass();
13MyClass obj2 = obj1.clone(); // 这是一个浅拷贝
14
15// 如果AnotherClass不是基本类型而是另一个对象,那么obj1.another和obj2.another将引用同一个对象。
- 深拷贝(Deep Copy):
- 深拷贝不仅复制对象本身,还复制其引用的对象以及引用对象的引用对象等,直到所有的嵌套层次都被复制为止。
- 在Java中,实现深拷贝需要自定义拷贝构造函数或提供序列化反序列化的机制。
例如:
Java
1public class MyClass implements Cloneable {
2 private String name;
3 private AnotherClass another;
4
5 // 其他代码...
6
7 public MyClass(MyClass original) {
8 this.name = new String(original.name); // 对基本类型以外的对象进行深拷贝
9 this.another = new AnotherClass(original.another); // 假设AnotherClass也实现了深拷贝
10 }
11}
12
13MyClass obj1 = new MyClass();
14MyClass obj2 = new MyClass(obj1); // 这是一个深拷贝,所有对象都是独立的副本
15
16// 现在obj1.another和obj2.another分别指向两个不同的AnotherClass对象实例。
深拷贝主要用于避免原始对象和拷贝对象之间的相互影响,确保修改其中一个对象不会影响到另一个对象。而在某些场景下,浅拷贝由于其轻量级的特性可能更受欢迎,具体取决于实际的应用需求。
9 如果你提交任务时,线程池队列已满,这时会发⽣什么
当线程池的任务队列已满时,具体发生的情况取决于线程池的配置和实现:
-
无界队列:
- 若线程池使用的是如
LinkedBlockingQueue
这样的无界队列,则任务会继续添加到队列中等待执行。这意味着线程池永远不会拒绝任务,但可能会导致内存占用持续增加,如果新任务不断提交而没有及时消费,可能造成内存溢出(OOM)。
- 若线程池使用的是如
-
有界队列:
- 如果线程池使用的是一旦达到预设大小就会拒绝新任务的有界队列(如
ArrayBlockingQueue
),则会发生以下情况之一:- 线程数量未达上限:若当前线程池中的工作线程数量小于最大线程数(
maximumPoolSize
),那么线程池会尝试创建新的工作线程来处理队列中的任务。 - 线程数量已达上限:若当前线程数已经等于或超过了最大线程数,且队列已满,这时按照线程池设置的拒绝策略(RejectedExecutionHandler)来处理无法放入队列的任务:
AbortPolicy
(默认):直接抛出RejectedExecutionException
异常。CallerRunsPolicy
:调用者线程自己去执行被拒绝的任务。DiscardPolicy
:默默地丢弃任务,不抛出异常也不执行任务。DiscardOldestPolicy
:从队列中移除最早的未开始执行的任务,然后尝试重新提交当前任务。
- 线程数量未达上限:若当前线程池中的工作线程数量小于最大线程数(
- 如果线程池使用的是一旦达到预设大小就会拒绝新任务的有界队列(如
综上所述,在线程池队列已满的情况下,系统的行为取决于线程池的具体配置,包括队列类型、最大线程数以及拒绝策略等参数。
10 并发编程三要素
并发编程三要素通常指的是:
-
原子性(Atomicity): 原子性是指一个操作或者多个操作在多线程环境下,要么全部执行并且看到其影响,要么都不执行。在并发编程中,确保原子性是为了避免数据竞争和不一致的状态。例如,在Java中,可以通过
synchronized
关键字、ReentrantLock
等锁机制,或者使用CAS(Compare and Swap)无锁算法来保证原子性。 -
一致性(Consistency): 一致性强调的是程序在执行过程中的状态变化是符合预期的,即使在并发环境下也是如此。这意味着任何一次读取都应该能够看到之前所有写入对系统状态改变的总结果。为了保持一致性,开发者需要遵循一些同步规则,比如Java内存模型(JMM)中的happens-before原则,并合理地使用锁和其他并发工具。
-
隔离性(Isolation): 隔离性要求并发执行的事务之间互不影响,如同单线程环境下的串行执行一样。在数据库领域,通过事务隔离级别(如读未提交、读已提交、可重复读、序列化)来实现隔离性;而在普通并发编程环境中,一般通过锁或其他同步机制来防止不同线程间的干扰,使得每个线程都像是在独立运行。
另一个常被提及的并发编程要素是:
- 持久性(Durability): 在数据库领域,持久性意味着一旦事务完成并提交,那么即使发生系统崩溃或电源故障,该事务所做出的更改也会永久保存在数据库中。而在非数据库的并发编程场景下,这可以理解为对共享资源的修改在完成后会立即反映到主存中,不会因硬件故障而丢失。
然而,对于一般的并发编程而言,上述“并发编程三要素”主要关注的是原子性、一致性以及隔离性,以确保在多线程环境下程序的正确执行和数据的安全访问。
CAS
CAS,全称为“Compare and Swap”或“Compare and Set”,是一种无锁算法,在并发编程中用于实现原子性操作。在多线程环境下,当多个线程试图同时修改同一内存位置时,CAS提供了一种无需使用互斥锁(mutex)就能保证数据一致性的方法。
工作原理:
-
CAS包含三个操作数:
- 内存地址V:需要被修改的内存位置。
- 预期原值A:一个期望值,即认为当前内存地址V应该存储的值。
- 新值B:如果当前内存地址V的值等于预期原值A,则将内存地址V的值更新为新值B。
-
当执行CAS操作时,会比较内存地址V的实际值是否与预期原值A相等。
- 如果相等,则将内存地址V的值更新为新值B,并返回成功状态。
- 如果不相等,则说明有其他线程在此期间已经修改了该内存地址的值,此时不会进行任何更新,并返回失败状态。
优点:
- 避免死锁:由于CAS是基于硬件指令级别的支持,它不需要显式地获取和释放锁,从而降低了死锁的可能性。
- 无锁并发:在高并发场景下,CAS可以提高系统的性能,因为它允许更高的并发度,减少了因锁竞争带来的上下文切换和阻塞。
缺点及注意事项:
- ABA问题:如果某个值先被改为B,然后又被改回A,那么CAS操作可能会误判此情况,将其认为是没有发生过修改。某些并发数据结构会通过增加版本号或其他机制来解决ABA问题。
- 循环开销:在高竞争环境下,连续CAS操作失败可能导致自旋重试,消耗CPU资源。
- 只能应用于原子变量:CAS通常用于对原子类型的变量进行操作,而不能直接应用到复杂的数据结构上。
现代操作系统和编程语言(如Java、C++等)提供了对CAS操作的支持,例如在Java中,java.util.concurrent.atomic
包下的原子类就广泛使用了CAS来实现高效且线程安全的操作。
RocketMQ 通过多种机制来保证消息不丢失,主要涉及以下几个方面:
- 刷盘机制。RocketMQ 支持同步刷盘和异步刷盘两种方式。同步刷盘意味着消息在写入磁盘后,需要等待磁盘完成写入才返回成功响应。异步刷盘则允许消息写入磁盘后立即返回成功,但不会等待磁盘完成写入。1
- 消息确认机制。Producer 在发送消息后,需要等待 Broker 的确认响应。如果 Broker 未收到确认,会尝试重新发送消息,直到收到确认。23
- 主从复制机制。RocketMQ 默认采用多主复制模式,即使某个 Broker 宕机,其他 Broker 可以接管其工作,确保消息不会丢失。14
- 消息存储机制。RocketMQ 默认采用双写模式,即将消息同时写入内存和磁盘,然后异步将内存中的消息刷盘到磁盘中,以提高消息的可靠性。
- 重试机制。如果发送消息失败,如网络问题或通信异常,RocketMQ 会自动尝试重试,直到消息成功发送。2356
这些机制共同作用,确保了消息在 RocketMQ 中的可靠传输。
11 sychronized和ReentrantLock的区别
- sychronized是⼀个关键字,ReentrantLock是⼀个类
- sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
- sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
- sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
- sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
- sychronized底层有⼀个锁升级的过程
12、sychronized的⾃旋锁、偏向锁、轻量级锁、重量级锁,分别介绍和联系
- 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就可以直接获取到了
- 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过⾃旋来实现的,并不会阻塞线程
- 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
13 MyBatis 数据转换
在MyBatis框架中,数据转换主要是指类型转换器(TypeHandler)的使用。MyBatis提供了一种机制来处理Java类型与数据库类型的转换,以便在执行SQL操作时将Java对象的数据正确地存入数据库,并从数据库查询结果中正确读取数据到Java对象。
例如:
-
存储过程中的参数和结果映射:当我们将Java对象作为存储过程的输入参数或接收其输出结果时,需要自定义TypeHandler来处理Java类型与数据库特定类型的匹配。
-
基本类型与包装类的转换:MyBatis内置了对Java基本类型及其包装类与数据库对应类型的转换支持,如String与VARCHAR、Integer与INTEGER等。
-
复杂类型转换:对于非基本类型的转换,比如日期、时间、枚举类型、自定义对象等,通常需要自定义TypeHandler实现。例如,可以自定义一个
JsonTypeHandler
来处理JSON字符串与Java对象之间的相互转换。
自定义TypeHandler的过程包括:
- 实现
org.apache.ibatis.type.TypeHandler
接口。 - 在实现类中覆盖
setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType)
方法用于设置预编译语句中的参数值。 - 覆盖
getResult(ResultSet rs, String columnName, JdbcType jdbcType)
或getResult(CallableStatement cs, int columnIndex, JdbcType jdbcType)
方法用于从结果集中获取数据并转换为对应的Java类型。
配置自定义TypeHandler的方式有以下几种:
- 全局配置:在mybatis-config.xml全局配置文件中通过
<typeHandlers>
配置元素添加自定义类型处理器。 - 映射文件内局部配置:在具体的mapper.xml映射文件中使用
<resultMap>
的<typeHandler>
子标签指定针对某个字段使用特定的类型处理器。 - 注解方式:在实体类属性上使用
@TypeHandler
注解关联自定义的TypeHandler。
示例代码(简化版):
Java
1public class JsonTypeHandler<T> implements TypeHandler<T> {
2 @Override
3 public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
4 // 将Java对象转换为JSON字符串并设置到PreparedStatement中
5 if (parameter != null) {
6 String json = JSON.toJSONString(parameter);
7 ps.setString(i, json);
8 } else {
9 // 处理空值情况
10 ps.setNull(i, Types.VARCHAR);
11 }
12 }
13
14 @Override
15 public T getResult(ResultSet rs, String columnName) throws SQLException {
16 // 从ResultSet中读取JSON字符串并反序列化为Java对象
17 String json = rs.getString(columnName);
18 return json != null ? JSON.parseObject(json, type) : null;
19 }
20
21 // ... 其他方法实现 ...
22}
然后在MyBatis配置中注册这个类型处理器,或者在映射文件中指定使用它。
14 jvm
Java虚拟机(Java Virtual Machine,简称JVM)是运行所有Java程序的抽象计算机,它是一个用于实现Java语言字节码指令集的软件。JVM为Java代码提供了运行环境和内存管理机制,负责将编译后的Java字节码转换成机器码并执行。
JVM的主要组成部分包括:
-
类加载器(Class Loader):负责查找和导入二进制格式的类文件(.class)到JVM中,并且提供类的加载、链接和初始化等过程。
-
运行时数据区(Runtime Data Areas):
- 程序计数器(Program Counter Register):存储当前线程正在执行的虚拟机字节码地址。
- 虚拟机栈(VM Stack):每个方法在执行时会创建一个栈帧,存放局部变量表、操作数栈、动态链接信息等,主要用于方法调用和返回。
- 本地方法栈(Native Method Stack):与虚拟机栈类似,但服务于使用JNI(Java Native Interface)调用的本地方法。
- 堆(Heap):存储对象实例,所有线程共享这块区域,垃圾回收器主要在这里工作。
- 方法区(Method Area)/元空间(Metaspace):存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-
执行引擎(Execution Engine):负责解释字节码或通过即时编译器将其转化为机器码执行。
-
垃圾回收器(Garbage Collector, GC):自动管理堆内存,对不再使用的对象进行回收以释放内存资源。
-
即时编译器(Just-In-Time Compiler, JIT):为了提高性能,JVM可以将热点代码(频繁执行的代码)编译成本地机器码。
通过JVM的跨平台特性,使得“一次编写,到处运行”的理念得以实现。不同平台上的JVM能将统一的Java字节码转换为对应平台的机器指令执行,从而实现了Java语言的跨平台性。
15 虚拟机栈 满了的原因
Java虚拟机栈(JVM Stack)满了通常是因为以下几种原因:
-
深度递归:
- 当线程执行的方法调用层次过深,即发生了大量的递归调用时,会不断消耗栈空间来创建新的栈帧。如果递归调用没有适当的退出条件或者循环了,会导致栈的深度超过了JVM设置的最大值,从而引发StackOverflowError。
-
大对象本地变量:
- 如果方法中的局部变量引用了大量数据结构或大型数组等占用内存较多的对象,尤其是在每个递归层级都创建这样的大对象时,也会导致栈空间快速耗尽。
-
Xss参数设置过小:
- JVM启动参数中
-Xss
用于指定每个线程的堆栈大小。如果该值设置得过小,即使正常操作也可能因为空间不足而触发StackOverflowError。
- JVM启动参数中
-
线程数量过多:
- 当系统中并发运行的线程数非常多,且每个线程都在执行需要较大栈空间的任务时,总的栈内存需求可能会超过JVM栈空间总容量。
解决虚拟机栈溢出的问题通常可以通过以下几个途径:
- 检查并优化代码逻辑,避免不必要的深层递归或减少递归层数。
- 调整栈的大小,通过增加
-Xss
参数值来扩大单个线程的栈空间。 - 减少每个线程所持有的大对象引用,合理分配和管理资源。
- 控制并发线程的数量,根据实际情况调整合适的线程池大小。
16 如何排查 cpu 升高 java
排查Java应用程序导致的CPU升高问题,可以按照以下步骤进行:
-
监控与识别:
- 使用操作系统自带工具(如Linux中的
top
、htop
或ps
命令)或第三方监控工具(如VisualVM、JProfiler等)来实时查看系统中哪些Java进程或者线程占用CPU较高。 - 在Linux中使用
top -Hp <pid>
命令可以列出指定Java进程内各个线程的CPU使用率。
- 使用操作系统自带工具(如Linux中的
-
获取线程堆栈信息:
- 通过Java自带的
jstack
工具分析CPU高占用的Java进程,输出其线程堆栈信息。例如: Code
分析1jstack <pid> > thread_dump.txt
thread_dump.txt
文件内容,查找CPU占用高的线程,通常表现为状态为RUNNABLE且循环次数频繁的线程。
- 通过Java自带的
-
分析线程堆栈:
- 根据堆栈信息找出可能导致CPU过高的原因,比如:
- 死锁:检查是否有多个线程互相等待对方持有的锁资源。
- 高频次的IO操作或网络通信阻塞:这可能导致CPU在处理轮询和上下文切换时消耗过高。
- 无尽循环或递归调用:程序逻辑错误导致无限循环或深度递归没有及时退出。
- 高计算密集型任务:算法复杂度过高,导致CPU一直在执行计算操作。
- 线程池设置不合理:线程数量过多,造成大量线程竞争而导致CPU负载过高。
- 根据堆栈信息找出可能导致CPU过高的原因,比如:
-
性能分析工具:
- 使用像JVisualVM这样的图形化工具进一步深入分析CPU热点方法和代码块。
- 对于Java 8及以上版本,也可以利用JDK内置的Java Flight Recorder (JFR) 和 Mission Control工具进行分析。
-
日志排查:
- 查看应用相关的日志,定位是否有异常行为导致CPU上升。
-
优化调整:
- 根据排查结果对代码进行相应优化,如修复死锁、减少不必要的计算、改进并发模型、合理配置线程池大小等。
-
持续监控与调优:
- 定期收集并分析监控数据,以发现长期趋势和潜在问题,逐步优化系统性能。
17 双亲委派模型
双亲委派
双亲委派模型是Java虚拟机(JVM)中类加载器的一个重要概念,它描述了类加载器在加载类时如何协作和委托请求的规则。在Java应用程序运行过程中,每当需要加载一个类型(class),首先会遵循以下步骤:
-
请求发起:当某个类加载器收到加载类的请求时,它并不会自己直接去加载这个类,而是先将加载请求传递给其父类加载器。
-
逐级向上:如果父类加载器存在(除了Bootstrap ClassLoader外,所有类加载器都有其父加载器),则由父加载器尝试加载该类。这个过程会一直延续到顶层的启动类加载器(Bootstrap ClassLoader)。
-
加载执行:只有当父类加载器无法完成加载请求(即找不到指定类)时,当前类加载器才会尝试自己去加载该类。
-
缓存与重复检查:在每个类加载器加载类之前都会检查是否已经加载过该类。如果已加载,则直接从内存中返回,不会再次加载。
-
防止类加载混乱:这种机制保证了Java的核心API类库只会被最上层的启动类加载器加载一次,且子类加载器无法覆盖这些核心类库,从而确保了Java平台的安全性和稳定性。
简而言之,双亲委派模型保证了类加载的有序性、唯一性和安全性,避免了由于多个类加载器加载同一个类而导致的类定义冲突的问题。
18 如何保证缓存的数据一致性
在分布式系统中保证缓存数据一致性是一项重要的任务,以下是一些通用的策略和技术来确保缓存与数据库之间的一致性:
-
直写模式(Write-Through):
- 当更新数据时,首先更新缓存,然后同步更新数据库。当数据写入成功后,缓存立即失效或更新,并等待数据库操作完成后返回确认信息给客户端。
-
回写模式(Write-Behind / Write-Back):
- 先将更改临时存储在缓存中,随后异步批量地更新到数据库。在此过程中,需要实现事务处理以确保数据最终一致,且在某些情况下可能需要维护一个日志系统来处理异常情况。
-
读取/加载时更新缓存(Cache-Aside Pattern):
- 在读取数据时,首先检查缓存。如果缓存未命中,则从数据库加载数据,并同时将数据放入缓存。更新数据时先更新数据库,然后使缓存中的相应条目失效(例如使用
Cache Invalidation
),下次读取时重新从数据库加载。
- 在读取数据时,首先检查缓存。如果缓存未命中,则从数据库加载数据,并同时将数据放入缓存。更新数据时先更新数据库,然后使缓存中的相应条目失效(例如使用
-
版本号控制和条件更新:
- 使用版本号或者时间戳等机制,在更新数据前验证缓存中数据的有效性。只有当缓存中的数据是最新的,才允许进行更新,并同时更新缓存和数据库。
-
消息队列中间件:
- 通过引入消息队列,更新数据库后发送一条消息到消息队列,由消费者服务负责清除或更新缓存。
-
分布式缓存框架的内置一致性保障:
- 如Redis、Memcached等支持分布式锁、事务以及监听数据库binlog变化的特性,结合这些特性可以构建出复杂的缓存更新策略。
-
强一致性方案:
- 对于要求强一致性的场景,可以采用两阶段提交(2PC)、三阶段提交(3PC)等分布式事务技术,但要注意这些方案通常会降低系统的性能和可用性。
-
最终一致性:
- 如果业务允许一定的延时,可以设计成最终一致性的系统,利用事件驱动或定期重试的方式逐步达到所有节点数据一致。
-
订阅数据库变更通知:
- 让缓存系统订阅数据库的变动通知(如MySQL的binlog、MongoDB的Change Streams等),一旦数据库有写操作,立刻触发缓存的更新。
总之,选择哪种方式取决于具体的业务需求、系统架构以及对一致性和性能的要求。在实践中,往往需要结合多种策略来优化数据的一致性和性能表现。
19 springboot 与 spring 的优势
Spring Boot与传统的Spring框架相比,具有以下显著优势:
-
快速启动(Quick Start):
- Spring Boot提供了一种“约定优于配置”的方式来简化应用的初始搭建和开发过程。通过引入starter依赖,可以自动处理大量Maven/Gradle构建配置,使得开发者能够快速开始编写业务代码。
-
自动配置(Auto-configuration):
- Spring Boot包含了对常见第三方库如数据库连接、Web容器、模板引擎等的自动配置支持,它会根据项目中类路径中的jar包进行判断并自动配置Bean,大大减少了手动配置的工作量。
-
内嵌服务器(Embedded Servers):
- Spring Boot默认提供了内嵌的Tomcat、Jetty或Undertow服务器,这意味着一个Spring Boot应用可以直接运行成为一个独立的应用程序,无需额外部署WAR文件到外部应用服务器。
-
命令行界面(CLI)和Actuator:
- Spring Boot CLI允许快速地从命令行创建和运行Spring应用程序。
- Actuator是Spring Boot提供的用于监控和管理生产环境中的应用的强大工具,它可以提供健康检查、metrics信息、审计日志等功能。
-
简化MVC配置:
- 在Spring Boot中,不再需要大量的XML配置或者复杂的Java配置类来定义控制器、视图解析器、静态资源映射等,只需要少量注解就可以完成这些配置。
-
统一的依赖管理:
- Spring Boot通过 starters 简化了项目的依赖管理,确保版本兼容性和最佳实践。
-
易于部署:
- 由于Spring Boot应用通常被打包为单一可执行的JAR或WAR文件,因此非常便于部署到各种环境,包括云平台。
-
开箱即用的特性:
- Spring Boot自带了许多微服务架构相关的特性,比如健康检查、指标收集、配置文件外部化等,使开发人员能够专注于业务逻辑实现。
总结来说,Spring Boot的核心目标就是让基于Spring框架的应用程序的创建和维护变得更加简单和高效。
20 jvm 堆溢出的原因
Java虚拟机(JVM)堆溢出的原因通常与以下几个方面相关:
-
内存分配过大:
- JVM的堆内存空间是有限的,可以通过
-Xms
和-Xmx
参数设置初始堆大小和最大堆大小。如果应用程序创建的对象过多,或者单个对象过大,超过了JVM堆所能提供的最大空间,就会导致堆溢出。
- JVM的堆内存空间是有限的,可以通过
-
内存泄漏:
- 当程序中存在不再使用的对象没有被垃圾回收器回收时,这些对象会持续占用堆内存空间,随着运行时间的增长,可能会耗尽整个堆内存,从而导致溢出。例如,静态集合类(如HashMap、ArrayList等)中的对象引用如果不释放,即使集合内容不再使用,也不会被垃圾回收。
-
长期存活对象过多:
- JVM中有分代垃圾回收机制,年轻代中的对象经历多次GC后仍未被回收,则会被晋升到老年代。如果应用中产生大量生命周期长的对象,且这些对象无法在每次GC后被回收,可能导致老年代空间不足而引发堆溢出。
-
并发环境下大量线程同时创建对象:
- 在高并发场景下,多个线程短时间内创建大量的临时对象,由于新生代空间不足以容纳所有对象,频繁触发GC且不能有效回收,最终可能造成堆内存溢出。
-
大对象直接进入老年代:
- JVM允许配置大对象直接进入老年代的阈值,若超过该阈值的大对象过多,可能导致老年代迅速填满而发生溢出。
-
不合理的堆内存分配:
- 如果分配给年轻代或老年代的空间比例不合适,也可能导致某一代内存空间过早耗尽。比如,年轻代太小,频繁触发YGC但仍然无法满足内存需求;或者老年代太小,年轻代对象快速晋升导致老年代空间不足。
解决堆溢出问题需要结合具体情况分析,通过调整JVM参数、优化代码结构、减少不必要的对象创建以及检查是否存在内存泄露等问题来实现。
21项目中做的优化
在项目中进行优化可能涉及多个层面,以下列举了一些常见的优化方向和措施:
-
性能优化:
- 代码层面:重构冗余或低效的代码逻辑,如减少不必要的循环、递归或者重复计算。
- 数据库查询优化:对SQL语句进行合理化设计,避免全表扫描,利用索引提升查询效率,减少JOIN操作,使用预编译语句防止SQL注入等。
- 缓存策略:合理设置数据缓存,如Redis、Memcached等,将常用且不经常变动的数据存储在内存中以减少数据库访问压力。
- 资源复用:对于昂贵资源(如数据库连接、HTTP连接等)采用连接池技术,提高资源利用率。
-
并发与线程优化:
- 线程池:使用线程池管理并发任务,避免频繁创建销毁线程带来的开销。
- 异步处理:通过异步编程模型处理耗时操作,比如使用Future/Promise模式,或者结合消息队列实现异步解耦。
- 锁优化:尽可能减少锁粒度,使用更高效的锁机制(如读写锁、原子变量),避免死锁问题。
-
内存优化:
- 对象生命周期管理:及时释放不再使用的对象引用,避免内存泄漏。
- 大对象处理:合理分配和管理大对象,避免直接进入老年代导致频繁Full GC。
- JVM调优:合理配置堆大小、新生代与老年代比例、GC收集器类型等参数,监控并分析内存使用情况。
-
网络通信优化:
- 压缩传输:对发送的数据进行压缩后再传输,减小网络带宽消耗。
- 批量处理:合并多次请求为一次,减少网络交互次数。
- 协议选择:根据业务需求选择适合的网络协议,例如HTTP/2、gRPC等能提供多路复用功能的协议。
-
前端性能优化:
- 资源加载:合理分割CSS、JavaScript文件,启用HTTP/2,使用CDN加速静态资源加载。
- 懒加载:图片、组件等内容延迟加载,只在需要时加载。
- 响应式设计:优化页面布局,适应不同设备屏幕尺寸。
-
架构优化:
- 服务拆分:根据业务边界拆分成微服务架构,降低单体应用复杂性。
- 负载均衡:部署多台服务器,并配置负载均衡器,分散系统压力。
- 分布式缓存、存储和计算:根据业务场景合理引入分布式组件,提高系统的可扩展性和可用性。
每种优化措施都需要结合具体项目特点和瓶颈来实施,并通过持续监控和调整达到最佳效果。
22 如何设计高并发的订单系统
设计高并发的订单系统需要综合考虑多个方面的因素,以下是一些关键的设计策略:
-
分布式架构:
- 使用分布式服务化架构,将订单服务、库存服务、支付服务等核心功能拆分为独立的服务,通过微服务或者服务网格进行通信。
- 数据库层面采用分库分表策略,根据订单ID或用户ID进行水平切分,降低单个数据库的压力。
-
异步处理:
- 对于涉及IO操作较重且不需要实时反馈给用户的部分,如创建订单后更新库存、生成账单等,可以采用异步消息队列(如RabbitMQ、Kafka)进行解耦和削峰填谷。
- 用户下单成功后,先生成预订单并扣减库存,然后通过消息队列通知后续服务完成剩余流程,如生成正式订单、发送通知邮件等。
-
缓存与本地内存优化:
- 对高频查询的数据(如商品信息、用户信息)使用缓存(Redis、Memcached),减少数据库访问压力。
- 高并发场景下,可以在内存中维护一份订单状态缓存,提升读取效率,但务必保证数据的一致性。
-
数据库事务控制与隔离级别:
- 设计合理的数据库事务模型,确保在高并发下的数据一致性。例如,对于库存检查与订单创建操作,可以采用乐观锁或悲观锁机制避免超卖。
- 根据业务需求选择合适的事务隔离级别,平衡并发性能与数据安全性。
-
限流与熔断:
- 使用流量控制组件(如Hystrix、Sentinel)对请求进行限流,防止系统被瞬时大流量冲垮。
- 实现熔断机制,在下游服务出现故障时快速失败,保护整个系统的稳定。
-
幂等性设计:
- 由于网络等因素可能导致重复请求,因此系统需要支持幂等性设计,确保同一个订单的多次请求结果一致。
-
弹性伸缩:
- 在云环境下,利用容器编排技术(如Kubernetes)实现资源的动态伸缩,当系统负载增加时自动扩容以应对高并发请求。
-
监控与日志:
- 建立完善的监控体系,包括系统性能指标、数据库状态、中间件健康状况等,及时发现并处理问题。
- 记录详细的日志信息,方便追踪排查问题以及审计。
-
预热与压测:
- 在上线前进行充分的预热和压力测试,模拟真实场景下的高并发请求,找出潜在瓶颈并优化。
综上所述,设计高并发订单系统是一个复杂的过程,需要结合具体的业务场景和技术栈,从架构、算法、数据存储等多个角度出发,采取相应的技术和策略来保障系统的高性能、高可用和可扩展性。
22 Integer s1=new Integer(128) Integer s2=new Integer(128) 相等吗
在Java中,对于Integer s1 = new Integer(128)
和Integer s2 = new Integer(128)
这两个对象实例化过程:
由于Integer对象在-128到127之间的值会缓存(自动装箱时),所以如果创建的是这个范围内的Integer对象,即使使用new关键字创建多个具有相同数值的Integer对象,通过==
运算符比较它们也会返回true,因为它们引用的是同一个缓存中的对象。
但是,对于大于127的值(如这里的128),每次new操作都会生成一个新的Integer对象,因此s1
和s2
虽然数值相同,但它们是两个不同的对象,所以在内存中占据不同的地址。因此,直接使用==
运算符比较Integer s1
和Integer s2
将返回false,因为它们指向的是堆中两个不同的对象。
如果你想比较的是它们所包装的int值是否相等,应该使用equals()
方法:
Java
1if (s1.equals(s2)) {
2 System.out.println("The integer values are equal.");
3}
这段代码将会输出"The integer values are equal.",表示它们所包含的整数值是相等的。
23 要做到实时性 营销系统用户领取优惠券时如何设计支持高并发的库存扣减
要做到实时性,营销系统用户领取优惠券时在设计支持高并发的库存扣减时,可以采用以下策略:
-
分布式缓存+原子操作:
- 使用Redis等内存数据库存储优惠券库存信息,其读写性能远高于传统的关系型数据库。
- 利用Redis的原子操作(如
decrby
命令)直接从缓存中扣减库存,确保高并发下的数据一致性。
-
本地内存队列与异步处理:
- 在应用服务器本地维护一个内存队列,当用户请求领取优惠券时,先将请求放入队列,并立即返回预领取成功给用户,保证用户体验的实时性。
- 后台有一个服务消费这个队列中的任务,执行实际的库存扣减操作和数据库更新。由于是本地内存操作,所以速度非常快。
-
分布式锁+数据库事务:
- 对于有限量的热门优惠券,仍然需要通过分布式锁来控制并发访问。
- 当获取到锁后,在数据库层进行事务处理,包含查询库存、扣减库存和更新用户领取记录三个步骤,确保整个过程的原子性。
-
读写分离:
- 采用数据库读写分离策略,对库存表的查询和更新操作分别指向不同的数据库节点,提高系统的响应速度和可用性。
-
负载均衡与扩容机制:
- 设置负载均衡器(如Nginx、HAProxy),将高并发请求均匀分散到多个应用服务器上。
- 根据实际业务压力动态调整应用服务器的数量,确保系统有足够的处理能力应对高并发场景。
-
API限流保护:
- 在API层面对领取优惠券接口设置适当的限流策略,比如令牌桶算法或漏桶算法,防止瞬时流量过高导致系统崩溃。
-
实时监控报警:
- 建立完善的监控体系,包括但不限于系统负载、数据库状态、缓存命中率等关键指标,一旦出现异常及时报警并自动触发应急预案。
通过上述技术手段,可以在保障实时性的前提下,实现营销系统用户领取优惠券时高并发的库存扣减。同时,需结合具体业务场景和预期峰值流量进行压测和调优,以确保系统在真实环境中的稳定性和可靠性。
可以参考
https://www.cnblogs.com/papering/p/16634825.html
24 线程,进程,协程
线程(Thread):
线程是操作系统能够进行运算调度的最小单位,它包含在进程中。
每个线程都有独立的程序计数器、寄存器和堆栈空间,它们共享进程中的其他资源,如代码段、数据段和打开的文件描述符等。
线程之间的切换是由操作系统的内核完成的,通常涉及到上下文切换的成本,包括保存和恢复CPU寄存器状态、虚拟内存映射等。
多线程可以在单个进程中并发执行不同的任务流,提高系统吞吐量和响应速度。
进程(Process):
进程是操作系统分配资源的基本单位,它包含了运行时程序的全部信息,包括程序代码、数据、打开的文件、子进程、线程等资源。
每个进程都有自己独立的地址空间,不同进程之间不能直接访问彼此的内存区域,需要通过进程间通信机制(IPC)进行交互。
进程间的切换比线程更重,因为除了上下文切换之外,还可能涉及虚拟内存空间的切换以及各种资源的重新分配。
创建新进程通常是为了实现多个独立的应用程序实例或者为了实现进程隔离以保护系统稳定性与安全性。
协程(Coroutine):
协程是一种用户态的轻量级线程,由程序员自行管理其调度,而非由操作系统内核管理。
协程在单个线程中创建,并行性不是真正意义上的并行执行(多核同时执行),而是基于协作式多任务的概念,在一个线程上通过主动让出控制权来实现多个任务的交替执行。
协程之间的切换开销极小,因为它仅需保存和恢复局部上下文,不需要经过复杂的系统调用或硬件中断处理。
在现代编程语言如Go、Python、Lua等中,协程被广泛应用以简化异步编程模型,提升程序性能,特别是在I/O密集型应用中,能够避免由于大量阻塞等待而导致的CPU利用率低下问题。
25 Spring 事务的实现原理
Spring框架通过AOP(面向切面编程)和Transaction Management API实现了声明式事务管理。其核心原理可以分为以下几个关键部分:
-
PlatformTransactionManager接口: Spring提供了多个实现PlatformTransactionManager接口的类,如DataSourceTransactionManager用于JDBC事务管理,HibernateTransactionManager用于Hibernate ORM事务管理等。该接口定义了开启、提交、回滚事务的方法。
-
@Transactional注解: 在服务类或方法上使用
@Transactional
注解,表明这个方法需要在一个数据库事务中执行。当方法被调用时,Spring AOP会拦截这些方法调用,并根据注解的属性(如传播行为、隔离级别、超时时间、只读标志等)进行相应的事务处理。 -
事务代理(Transaction Proxy): Spring通过AOP动态生成一个代理对象,代理对象在调用原方法前后执行事务相关的代码。当方法开始执行时,开启一个新的事务;若方法正常完成,则提交事务;若方法抛出未受检查异常(继承自RuntimeException的异常)或者配置了特定的检查异常,那么事务将被标记为回滚。
-
事务同步(TransactionSynchronization): Spring还提供了事务同步机制,可以在事务成功提交后或者发生回滚时执行一些清理工作,例如清除缓存、发送通知消息等。
-
事务传播行为: 当事务方法内部调用另一个事务方法时,可以通过
@Transactional
注解的propagation属性控制事务如何传播。例如REQUIRED表示如果当前存在事务则加入此事务,否则新建一个事务。 -
数据库连接与事务管理: 对于JDBC而言,Spring会管理数据库连接,并根据事务状态决定何时提交或回滚SQL语句的操作结果。
总之,Spring事务管理的核心在于利用AOP对目标方法进行增强,根据事务注解信息来创建并管理事务上下文,在方法执行过程中根据业务逻辑的执行结果来协调数据库事务的生命周期。
26 nio
NIO(Non-blocking I/O,非阻塞I/O)是Java平台提供的新输入/输出处理方式。在传统的I/O模型中,每个连接都需要一个线程来处理数据的读写操作,当并发量增大时,这种模型会导致大量的线程创建和上下文切换,资源消耗严重。
而在NIO模型中,引入了Selector选择器、Channel通道以及Buffer缓冲区的概念:
-
Channel(通道): Channel是Java NIO中的核心组件,它代表了到实体(如硬件设备、文件或网络套接字)的连接。所有的IO操作都在Channel上进行。
-
Buffer(缓冲区): Buffer是一个用于存储数据的内存块,可以高效地进行批量数据的读写操作,避免频繁的小规模IO操作带来的性能损耗。Java NIO提供了多种类型的Buffer,如ByteBuffer、CharBuffer等。
-
Selector(选择器): Selector允许单个线程管理多个Channel,通过注册感兴趣的事件类型(如读就绪、写就绪),线程可以在多个Channel间轮询检查哪些已经准备好进行指定的操作。这样,在高并发场景下,只需要少量的线程就能处理大量连接的IO请求。
使用Java NIO的优点在于:
- 非阻塞:线程在执行读写操作时不会被阻塞,可以同时处理更多的连接。
- 缓冲区复用:减少内存分配与回收的开销,提高性能。
- 选择器机制:提高了服务器端对多路复用IO的支持,有效利用系统资源。
Java NIO通常应用于高性能服务器开发,例如构建Web服务器、数据库连接池、实时通信应用等。
27 jdk proxy 与 cglib
JDK Proxy 和 CGLib 都是 Java 中常用的动态代理实现技术,它们主要用于在运行时创建一个代理对象,从而对目标对象的方法调用进行增强或拦截。下面简要概述两者的主要特点及区别:
JDK Proxy:
- 官方支持:JDK Proxy 是 Java 标准库(java.lang.reflect.Proxy)的一部分,不需要额外引入第三方库。
- 基于接口:JDK Proxy 动态代理是基于接口的,这意味着它只能为实现了接口的类创建代理对象。
- 实现原理:通过InvocationHandler接口创建一个代理实例,当代理对象的方法被调用时,会转到InvocationHandler的invoke方法中执行,这个过程中可以添加额外的逻辑如前置、后置处理等。
- 性能影响:由于使用反射机制,相对CGLib来说,在大量代理对象生成和方法调用的情况下,性能可能会稍逊。
CGLib:
- 第三方库:CGLib 是一个第三方字节码生成库,它通过对字节码的操作(基于ASM库)在运行时生成一个子类,从而实现动态代理功能。
- 基于继承:CGLib 不依赖于接口,可以直接为任意类生成代理对象,只要该类不是final类即可。
- 实现原理:通过生成被代理类的子类,在子类中覆盖父类方法来插入增强逻辑。
- 性能:因为直接生成字节码并创建子类,所以在性能上相比JDK Proxy可能会更优,特别是在没有接口或者需要代理非接口方法的情况下。
- 兼容性:对于未实现接口的类,以及final类,只能使用CGLib代理。
应用场景对比:
- 如果目标类已经实现了接口,且只需要代理接口声明的方法,那么可以选择JDK Proxy,因为它使用简单且无须引入额外库。
- 如果目标类没有实现任何接口,或者需要代理所有方法(包括非public方法),则需选用CGLib。
在Spring AOP框架中,根据默认配置和类是否实现接口,自动选择使用JDK Proxy还是CGLib进行代理。用户也可以根据需求手动配置代理类型。
28 servlet
Servlet 是 Java 平台上的一个技术,它用于开发 Web 应用程序中的服务器端组件。Servlet 是一种基于 Java 的技术,它可以接收和响应来自客户端(通常是 Web 浏览器)的请求,生成动态内容,并将其发送回客户端。
Servlet 生命周期主要包括以下几个阶段:
-
加载与实例化:
- 当首次接收到对 Servlet 的请求时,Web 容器(如 Tomcat、Jetty 等)负责加载 Servlet 类,并根据需要创建其实例。
-
初始化:
- 创建 Servlet 实例后,容器会调用
init()
方法对其进行初始化,这个方法通常只执行一次,即在 Servlet 的生命周期内仅初始化一次。
- 创建 Servlet 实例后,容器会调用
-
服务请求:
- 每当有客户端请求到达时,容器调用 Servlet 实例的
service()
方法来处理请求。service()
方法根据请求类型(GET、POST 等)调用相应的doGet()
或doPost()
等方法。
- 每当有客户端请求到达时,容器调用 Servlet 实例的
-
销毁:
- 当 Web 应用程序关闭或容器决定需要卸载 Servlet 时,容器会调用 Servlet 实例的
destroy()
方法,允许 Servlet 清理资源,例如关闭数据库连接等。
- 当 Web 应用程序关闭或容器决定需要卸载 Servlet 时,容器会调用 Servlet 实例的
Servlet 通常配置在 web.xml
文件中,也可以通过注解的方式进行配置,在 Servlet 3.0 及以上版本中尤为常见。Servlet 提供了一种强大的机制,使得开发者可以方便地构建功能丰富的、可扩展的 web 应用程序组件。
29 mysql 可以做哪些优化
MySQL 数据库的优化可以从多个角度进行,以下列出了一些关键的优化措施:
-
索引优化:
- 合理创建索引:针对频繁作为查询条件、JOIN连接条件、GROUP BY、ORDER BY涉及的列创建索引,尤其是WHERE子句中常出现的列。
- 避免过度索引:过多的索引会导致写操作变慢,占用更多的存储空间,并可能导致查询优化器的选择困难。
- 使用复合索引:对于多列查询,考虑创建复合索引(组合索引),遵循“最左前缀”原则。
- 注意NULL值的影响:索引不会包含NULL值的列,避免在索引列上使用IS NULL查询。
-
查询优化:
- 避免使用
SELECT *
:明确指定需要的列名,减少不必要的数据传输和处理。 - 使用
EXPLAIN
分析查询计划:理解查询是如何执行的,识别潜在的性能瓶颈。 - 使用
LIMIT
:当只需要少量结果时,结合LIMIT
限制返回记录的数量。 - 避免在索引列上做计算或函数操作:这会导致无法使用索引。
- 避免使用
-
数据表设计与结构调整:
- 选择合适的字段类型:尽量减小字段宽度,合理使用枚举或整数代替字符串等。
- 分区表:对于大型表,考虑分区以分散数据,提高查询效率。
- 数据归档:定期清理或归档历史数据,减少大表的大小。
-
JOIN优化:
- 优化JOIN操作:确保JOIN字段都有索引,尽量减少JOIN的数量,考虑关联表的顺序和大小。
- 避免跨库JOIN:如果可能,将相关表放在同一个数据库中。
-
查询缓存:
- 开启查询缓存:对于重复查询较多的场景,启用查询缓存可以显著提升性能,但也需要注意缓存失效和管理成本。
-
事务与锁优化:
- 适当的事务隔离级别:选择最适合业务场景的事务隔离级别,避免死锁和过多锁定。
- 减少锁的粒度:合理设计表结构和事务,减少并发冲突。
-
其他优化手段:
- 正确使用存储过程和触发器:在合适的情况下,可以减少网络往返延迟和提高整体性能。
- 保持数据库统计信息准确:定期更新表统计信息,帮助优化器制定更合理的执行计划。
- 优化硬件资源配置:合理分配内存、CPU、磁盘I/O资源,必要时采用SSD硬盘、增加内存等方式提高性能。
-
配置调整:
- 调整MySQL配置参数:如InnoDB缓冲池大小、线程池大小、临时表大小等,以适应实际工作负载。
-
监控与调优工具:
- 使用性能监视工具,如MySQL Performance Schema或Percona Toolkit,持续监控数据库性能指标,找出性能瓶颈并进行针对性优化。
总之,MySQL数据库优化是一个综合的过程,需要从数据库设计、SQL查询优化、硬件环境、数据库参数配置等多个层面同时进行考量和改进。