面试题大总结2
多线程#
1.一个线程的生命周期有几种状态,是怎么流转的#
一个线程的生命周期通常包括以下五种状态:
1.新建状态(New): 当线程对象被创建时,线程处于新建状态。此时,线程已经被分配了内存空间,但还未开始执行。
2.就绪状态(Runnable): 当线程对象调用 start() 方法后,线程进入就绪状态。此时,线程等待操作系统的调度分配 CPU 资源。就绪状态的线程是可以运行的,但可能由于没有获得 CPU 时间片而暂时未运行。
3.运行状态(Running): 当线程获得 CPU 时间片后,线程开始执行 run() 方法中的代码,这时线程处于运行状态。
4.阻塞状态(Blocked/Waiting/Timed Waiting): 线程在某些情况下会进入阻塞状态,例如等待某个资源或锁的释放、调用 sleep() 方法暂时休眠等。根据具体情况,阻塞状态还可以细分为: 阻塞(Blocked):等待获取锁或资源。 等待(Waiting):等待另一个线程的通知。 超时等待(Timed Waiting):等待一段时间后自动返回就绪状态,如调用 sleep(time)。
5.终止状态(死亡状态)(Terminated): 当线程的 run() 方法执行完毕或者线程因异常退出时,线程进入终止状态。这时线程生命周期结束,不能再次启动。
线程状态的流转:
新建状态 → 就绪状态:调用 start() 方法。
就绪状态 → 运行状态:线程被调度获取 CPU 时间片。
运行状态 → 阻塞状态:线程主动调用 sleep()、wait() 或者等待某个资源。
阻塞状态 → 就绪状态:等待条件满足、超时或者被中断。
运行状态 → 终止状态:线程执行完毕或发生异常退出。 这些状态和它们之间的流转构成了线程的完整生命周期。
2.volatile关键字的作用 #
1.可见性: 当一个变量被声明为 volatile 时,意味着对该变量的读写操作会直接操作主内存,而不是从线程的本地缓存中读取。这确保了当一个线程修改了 volatile 变量的值时,其他线程能立即看到这个修改。
2.防止指令重排序优化: 使用 volatile 关键字也可以防止指令重排序优化。在一些情况下,编译器和 CPU 出于性能优化的目的,可能会重新排列指令的执行顺序,这在多线程环境中可能导致不可预期的行为。volatile 保证了对这个变量的操作不会被重排序,从而确保了线程的正确执行顺序。
需要注意的是,volatile 关键字并不能保证操作的原子性。对于一些复杂的操作,比如 i++(自增操作),还是需要使用其他同步机制(如 synchronized 块)来确保线程安全。
3.线程池的七大参数以及工作原理 #
七大参数:
corePoolSize(核心线程数): 线程池中始终保持活动的核心线程数,即使它们处于空闲状态。只有当需要执行新任务时,线程池才会创建新的线程,直到线程池中的线程数达到 corePoolSize。
maximumPoolSize(最大线程数): 线程池中允许创建的最大线程数。当队列满了,且活动线程数小于 maximumPoolSize 时,线程池会创建新的线程来执行任务。
keepAliveTime(线程空闲时间): 当线程池中的线程数超过 corePoolSize 时,多余的空闲线程在终止前能保持空闲的最大时间。
unit(时间单位): 这是 keepAliveTime 参数的时间单位,如秒、毫秒等。
workQueue(任务队列): 用于存放等待执行的任务的阻塞队列。当线程池中的所有核心线程都在执行任务时,新任务会被放入此队列中等待执行。常见的队列有: ArrayBlockingQueue:一个有界的数组阻塞队列。 LinkedBlockingQueue:一个无界的链表阻塞队列。 SynchronousQueue:一个直接提交的阻塞队列。
threadFactory(线程工厂): 用于创建新线程的工厂类,可以通过自定义 ThreadFactory 为线程池中的线程指定名字、优先级等。
handler(拒绝策略): 当任务无法被线程池执行时,线程池会调用这个拒绝策略来处理此任务。常见的拒绝策略包括: AbortPolicy:抛出 RejectedExecutionException,这是默认策略。 CallerRunsPolicy:由调用线程处理该任务。 DiscardPolicy:直接丢弃任务。 DiscardOldestPolicy:丢弃队列中最旧的未处理任务。
工作原理:
任务提交:当一个任务提交到线程池时,线程池首先判断当前线程数是否小corePoolSize,如果是,则创建一个新线程来执行任务。
任务队列:如果当前线程数达到 corePoolSize,且所有核心线程都在忙碌,则任务被放入 workQueue 中等待。
扩展线程:如果任务队列已满且线程数小于 maximumPoolSize,线程池会创建新线程来执行任务。
拒绝任务:当线程数已经达到 maximumPoolSize,且任务队列也已满时,线程池会根据指定的拒绝策略来处理新提交的任务。
空闲线程处理:当线程池中的线程空闲时间超过 keepAliveTime 且当前线程数大于 corePoolSize 时,空闲线程会被终止,直到线程数降到 corePoolSize。
通过这七个参数,ThreadPoolExecutor 提供了一个灵活的机制来管理线程池的大小、任务的处理方式以及资源的有效利用。
4.线程池启动方法的submit以及execute的不同 #
submit和execute是线程池的两种任务提交方法,它们的主要区别在于任务的返回值处理。
execute(Runnable task):提交一个无返回值的任务,无法获取任务的执行结果,也无法捕获任务中的异常。
submit(Callable<T> task):提交一个有返回值的任务,可以获取任务的执行结果,并且可以捕获任务中的异常。它返回一个Future对象,可以通过它来检查任务是否完成或获取结果。
5.什么是活锁,饥饿,无锁,死锁 #
这些都是并发编程中涉及的不同问题或情况:
活锁(Livelock): 活锁发生在两个或多个线程不断地尝试互相避免死锁,但由于他们的持续调整状态,导致所有线程都无法前进。虽然线程不会被阻塞,但它们也无法完成预期的任务。活锁的一个典型例子是两个线程互相让步以避免冲突,但最终它们都未能取得进展。
饥饿(Starvation): 饥饿发生在一个或多个线程因系统资源总是被其他线程占用而无法获得足够的资源以完成任务的情况。这可能是由于资源分配策略不公平,某些线程总是被优先处理,导致其他线程无法获得所需的资源。
无锁(Lock-free): 无锁编程是一种并发控制的技术,其中数据结构和操作不会使用传统的锁机制来保证线程安全。相反,无锁数据结构通过原子操作和一致性保证来确保多线程环境中的数据一致性。无锁编程的优势在于减少了上下文切换和锁竞争,提升了性能。
死锁(Deadlock): 死锁发生在两个或多个线程相互等待对方释放资源的情况,导致这些线程都无法继续执行。例如,线程A持有资源1并等待资源2,而线程B持有资源2并等待资源1,这样就形成了一个循环依赖,导致所有相关线程都被阻塞。
6.什么是原子性,可见性,有序性 #
这三个概念是理解Java内存模型和并发编程中的关键要素:
1. 原子性(Atomicity): - 原子性指操作的不可分割性,即操作要么完全执行,要么完全不执行。它确保了操作在多线程环境中不会被中断或干扰。例如,Java中的`AtomicInteger`类提供了原子性保证,避免了线程间的干扰。
2. 可见性(Visibility): - 可见性保证了一个线程对共享变量的修改能够被其他线程看到。在没有正确同步的情况下,一个线程对变量的修改可能不会立即被其他线程感知。Java的`volatile`关键字和同步机制可以保证变量的可见性。
3. 有序性(Ordering): - 有序性确保程序执行的顺序与代码中指定的顺序一致。编译器和处理器可能对指令进行重排,这可能导致实际执行顺序与代码顺序不一致。Java中的`synchronized`和`volatile`关键字帮助维护有序性,确保操作的执行顺序符合预期。
设计模式面试题 #
1.设计模式的分类分为几种,每一种说出一两个#
设计模式通常分为三大类:
1. 创建型模式:
- 单例模式(Singleton):确保一个类只有一个实例,并提供全局访问点。
- 工厂方法模式(Factory Method):定义一个创建对象的接口,让子类决定实例化哪一个类。
2. 结构型模式:
- 适配器模式(Adapter):将一个类的接口转换成客户端所期望的另一个接口。
- 装饰器模式(Decorator):动态地给对象添加额外的职责,而不影响其他对象。
3. 行为型模式:
- 观察者模式(Observer):定义对象之间一对多的依赖关系,使得当一个对象改变状态时,所有依赖于它的对象都得到通知并自动更新。
- 策略模式(Strategy):定义一系列算法,将每一个算法封装起来,并使它们可以互换。
2.设计模式的六大原则简称(solid)原则 #
设计模式的六大原则是指导软件开发中设计模式应用的基本准则,旨在提高代码的可维护性、可扩展性和灵活性。以下是六大原则的简要说明:
单一职责原则 (Single Responsibility Principle, SRP)
- 定义:一个类应该只有一个引起它变化的原因,即一个类只负责一项职责。
- 解释:如果一个类承担了多种职责,那么在需求变更时,该类的多个职责可能同时变化,从而影响其稳定性。因此,遵循单一职责原则可以降低类的复杂性,提高代码的可维护性。
开闭原则 (Open/Closed Principle, OCP)
- 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。
- 解释:在不修改现有代码的情况下,通过扩展系统的行为来添加新功能。这可以通过继承、接口和抽象类来实现,有助于保持系统的稳定性和灵活性。
里氏替换原则 (Liskov Substitution Principle, LSP)
- 定义:子类对象必须能够替换父类对象,并且程序行为保持不变。
- 解释:子类在继承父类时,不能违背父类的行为约定。这确保了继承体系的正确性,避免了在使用多态时产生意外行为。
依赖倒置原则 (Dependency Inversion Principle, DIP)
- 定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。
- 解释:通过依赖抽象(如接口或抽象类)而不是具体实现类,来降低模块之间的耦合性,提高系统的灵活性和可维护性。
接口隔离原则 (Interface Segregation Principle, ISP)
- 定义:不应该强迫客户端依赖于它们不需要的接口;即,使用多个专门的接口,而不是一个大的接口。
- 解释:一个接口应该只包含客户端所需的方法,这样可以避免接口的臃肿,使得类间的依赖关系更加清晰。
合成复用原则 (Composite/Aggregate Reuse Principle, CARP)
- 定义:尽量使用对象组合/聚合,而不是继承来实现功能复用。
- 解释:继承是强耦合的,子类会继承父类的实现,可能导致代码的灵活性下降。而通过组合/聚合,可以在运行时决定功能的组合方式,从而使系统更加灵活。
这六大原则为设计模式的应用提供了理论基础,有助于开发出更具弹性和可维护性的代码。
将六大原则的英文首字母拼在一起就是SOLID(稳定的),所以也称之为SOLID原则。
只有满足了这六大原则,才能设计出稳定的软件架构,但它们只是原则,有时还是需要学会灵活应变,千万不要生搬硬套,否则只会把简单问题复杂化,切记!
3.单例设计模式如何防止反射漏洞攻击 #
在 Java 中,单例设计模式可以通过反射的方式绕过私有构造方法,从而创建多个实例,破坏单例模式。为了防止反射漏洞攻击,可以采取以下几种方法:
1. 在构造方法中防止多次实例化
在单例类的构造方法中,可以通过检查是否已经实例化来防止反射创建新的实例。具体做法是在构造方法内加入一个判断,如果实例已经存在,则抛出异常。这样,即使通过反射调用构造方法,也无法创建新的实例。
2. 使用枚举(Enum)实现单例
使用枚举类来实现单例模式是防止反射攻击的最简单、最安全的方法。Java 枚举类型在 JVM 中有特殊的处理机制,能够天然防止反射和序列化漏洞。因此,使用枚举可以确保单例模式的唯一性。
通过枚举实现的单例不仅防止了反射创建实例,还能防止序列化破坏单例,因为枚举类在序列化时 JVM 会确保返回相同的实例。
3. 使用
final
关键字和双重检测锁定虽然主要防范措施是在构造函数中,但结合
final
关键字和双重检测锁定(Double-Check Locking)也可以进一步强化单例的线程安全性。综上所述,为了防止反射攻击,可以通过在构造方法中检查实例是否已经存在,抛出异常来阻止通过反射创建多个实例;或者使用枚举类实现单例,这样可以避免反射和序列化带来的问题。
4.工厂模式分为几种? #
工厂模式是一种创建型设计模式,主要用于创建对象的实例。根据不同的需求和场景,工厂模式可以分为以下几种:
1. 简单工厂模式(Simple Factory)
也称为静态工厂方法模式。简单工厂模式通过一个工厂类根据传入的参数决定创建哪一个具体类的实例。简单工厂模式通常不被认为是设计模式中的正式模式,而是一个特殊实现。
特点:
- 工厂类包含一个静态方法,用于创建实例。
- 客户端只需要知道传递的参数,而不需要知道具体类的名字。
2. 工厂方法模式(Factory Method)
工厂方法模式为创建对象提供了一个接口,具体的对象由子类决定。工厂方法模式将对象的创建推迟到子类中进行。
特点:
- 定义一个创建对象的接口,但由子类决定要实例化的类。
- 更加符合开闭原则(对扩展开放,对修改关闭)。
3. 抽象工厂模式(Abstract Factory)
抽象工厂模式提供了一个接口,用于创建一系列相关或依赖的对象,而不需要指定它们的具体类。它用于生产“产品族”,即一组相关的或互相依赖的对象。
特点:
- 适用于需要创建一系列相关的对象时使用。
- 抽象工厂为一系列产品提供了统一的接口,具体的实现由子类决定。
总结:
- 简单工厂模式 适合创建单一产品实例,客户端只需传递参数即可。
- 工厂方法模式 提供了更灵活的对象创建方式,适合需要扩展性较强的场景。
- 抽象工厂模式 适合创建一组相关或依赖的对象,确保产品族的一致性。
每种工厂模式都有其适用的场景,可以根据项目的需求选择合适的模式。
MySQL#
1.一张表,里面有id自增主键,当insert了17条记录之后删除了15,16,17条记录,再把mysql重启,再插入一条数据请问这条新数据id是18还是15?(从搜索引擎角度解释)#
MySQL8.0以下版本:
- 表类型为InnoDB引擎,这条记录的ID是15。因为InnoDB只把自增主键的最大ID记录到内存中,所以重启MySQL或者对表进行OPTIMIZE操作,都会使得最大ID丢失
- 表类型为MylSAM引擎,这条记录的ID是18。因为MyISAM会把自增主键的最大ID记录到数据文件里面,重启MySQL之后,自增主键的最大ID也不会丢失
MySQL8.0以及以上版本
- 这条记录的ID是18,因为这个版本保证ID的值是在redo日志中的,重启之后是可以恢复的
2.如何区分float和double#
在 MySQL 中,
FLOAT
和DOUBLE
是用于存储浮点数的两种数据类型。它们的主要区别在于存储的精度和范围。以下是两者的详细比较:1. 精度(Precision)
FLOAT:单精度浮点数,通常使用 4 个字节(32 位)来存储。它的有效精度大约为 7 位十进制数字。这意味着它能够表示的数值范围更广,但精度较低,适合存储对精度要求不高的数据。
DOUBLE:双精度浮点数,通常使用 8 个字节(64 位)来存储。它的有效精度大约为 15-16 位十进制数字。相比
FLOAT
,DOUBLE
提供了更高的精度,适合需要存储更精确数据的场景。2. 存储空间
- FLOAT:占用 4 字节(32 位),由于精度较低,所需存储空间也较少。
- DOUBLE:占用 8 字节(64 位),由于提供了更高的精度,所需存储空间较多。
3. 数值范围
- FLOAT:大约在
-3.402823466E+38
到3.402823466E+38
之间。- DOUBLE:大约在
-1.7976931348623157E+308
到1.7976931348623157E+308
之间。4. 应用场景
FLOAT:适用于存储对精度要求不高但需要节省存储空间的数据。例如一些科学计算、统计数据或者传感器数据等。
DOUBLE:适用于需要更高精度的数值计算场景,例如金融计算、科学研究中的精确数据存储等。
总结
FLOAT
:占用空间小,精度低,适合存储对精度要求不高的数据。DOUBLE
:占用空间大,精度高,适合存储需要高精度的数值。在选择使用
FLOAT
还是DOUBLE
时,需要根据数据的精度需求和存储空间的考虑来决定。
3.如何区分CHAR_LENGTH()和LENGTH()#
在 MySQL 中,
CHAR_LENGTH()
和LENGTH()
都用于获取字符串的长度,但它们的计算方式不同,侧重的方面也不同。以下是对这两个函数的详细解释:1.
CHAR_LENGTH()
- 功能:
CHAR_LENGTH()
返回字符串中字符的个数。- 测量单位:按字符数计算长度。
- 适用场景:适用于多字节字符集(如 UTF-8),因为它按字符而不是字节来计算长度。
2.
LENGTH()
- 功能:
LENGTH()
返回字符串的字节长度。- 测量单位:按字节数计算长度。
- 适用场景:适用于需要知道字符串实际占用的存储空间时,因为它按字节来计算。
在 ASCII 编码中,
CHAR_LENGTH()
和LENGTH()
通常会返回相同的结果,因为每个字符占用 1 个字节。但在多字节字符集(如 UTF-8)中,这两个函数的返回值可能不同。
总结
CHAR_LENGTH()
:按字符计算字符串的长度,适合处理多字节字符集,返回的是字符的数量。LENGTH()
:按字节计算字符串的长度,适合需要知道字符串占用的实际字节数的情况,返回的是字节的数量。使用这两个函数时,应根据具体需求选择合适的函数来获取字符串的长度。
4.请描述一下mysql中InnoDB支持的四种事务隔离级别名称,以及逐级之间的区别#
在 MySQL 中,InnoDB 存储引擎支持四种事务隔离级别。事务隔离级别决定了一个事务可以看到其他事务的修改程度,以及防止特定类型的数据不一致性的问题。这四种事务隔离级别按照从低到高的顺序排列如下:
1. 读未提交(Read Uncommitted)
- 特点:在这个级别下,一个事务可以读取到其他事务尚未提交的数据(称为“脏读”)。
- 问题:可能会出现脏读,即一个事务读取到另一个事务未提交的数据,而这些数据可能会被回滚,从而导致读取的数据不一致。
- 适用场景:几乎不使用,因为它提供的隔离级别太低,很容易导致数据不一致性。
2. 读已提交(Read Committed)
- 特点:在这个级别下,一个事务只能读取到其他事务已经提交的数据。
- 问题:避免了脏读,但可能会出现不可重复读,即一个事务在不同的时间点读取同一行数据时,可能得到不同的结果(因为其他事务可能在这个过程中提交了更新)。
- 适用场景:常见于大多数数据库,如 Oracle 默认使用此隔离级别。适合读操作多、写操作少的场景。
3. 可重复读(Repeatable Read)
- 特点:在这个级别下,事务在开始时看到的数据视图在整个事务过程中保持不变,即使其他事务进行了提交修改。这是 MySQL InnoDB 的默认隔离级别。
- 问题:避免了脏读和不可重复读,但可能会出现幻读问题,即在同一事务中多次查询时,结果集中可能会出现或消失一些记录(因为其他事务可能插入了新记录)。
- 适用场景:适用于读写操作混合且要求数据一致性高的场景。MySQL 通过使用“间隙锁”(Gap Lock)解决幻读问题。
4. 可串行化(Serializable)
- 特点:在这个级别下,所有的读操作都被强制加锁,事务之间完全串行化执行,确保没有并发操作引起的任何数据不一致性问题。
- 问题:避免了脏读、不可重复读和幻读,是最严格的隔离级别,但代价是性能可能会显著下降,因为事务之间无法并发执行。
- 适用场景:适用于需要严格数据一致性且可以接受低并发量的场景。
逐级区别总结:
- 从低到高的隔离级别:隔离级别越低,允许的并发程度越高,但数据不一致性问题的风险也越高;隔离级别越高,数据一致性越好,但并发性能下降。
- 脏读:
Read Uncommitted
允许脏读,其他级别禁止。- 不可重复读:
Read Committed
允许不可重复读,Repeatable Read
和Serializable
禁止。- 幻读:
Repeatable Read
允许幻读(MySQL 通过间隙锁解决),Serializable
禁止。选择合适的事务隔离级别要权衡数据一致性和系统性能之间的关系,根据实际应用场景来决定。
5.请说出mysql类型中ENUM(枚举)的用法#
在 MySQL 中,
ENUM
类型用于定义一组预定义的字符串值,并将这些值作为列的可选项。ENUM
类似于枚举类型,在需要限制字段值的范围且这些值为少量字符串的情况下,使用ENUM
非常方便。
ENUM
的用法1. 定义
ENUM
列
ENUM
类型在定义列时需要指定一组预定义的字符串值。示例如下:在这个例子中,
status
列的值只能是'active'
、'inactive'
或'banned'
之一。如果尝试插入一个不在枚举列表中的值,MySQL 会抛出错误。
2. 插入数据
使用
ENUM
类型插入数据时,只能插入定义在ENUM
列中的值:如果尝试插入一个不在枚举列表中的值,例如:
MySQL 会抛出错误,因为
'pending'
不是status
列的有效值。3. 查询和过滤
可以使用
ENUM
类型的列进行查询和过滤:这将返回所有
status
为'active'
的用户。4. 默认值
可以为
ENUM
列指定一个默认值:在这个例子中,如果插入一条记录时没有指定
order_status
,默认值将是'pending'
。5. 数值表示
ENUM
类型在 MySQL 中实际上存储为整数,每个枚举值对应一个整数,从 1 开始。例如,'active'
对应 1,'inactive'
对应 2,依此类推。你可以通过查询整数来了解ENUM
列的数值:这个查询会返回
status
列对应的整数值。
ENUM
的优点
- 节省存储空间:
ENUM
使用整数存储值,通常比直接存储字符串更节省空间。- 数据完整性:通过限制列值为预定义的字符串集合,
ENUM
可以防止插入无效数据。- 易读性:使用
ENUM
可以使数据库表结构更加自描述和易读。
ENUM
的缺点
- 可扩展性差:如果需要增加新的枚举值,必须修改表结构,可能影响数据库性能。
- 查询复杂性:数值映射的存在可能导致查询的复杂性增加,尤其在需要理解内部表示时。
总结
ENUM
类型非常适合用于限定字段值为一组预定义字符串的场景,如用户状态、订单状态等。但在使用时要注意其可扩展性问题,如果预定义值可能频繁变化,可能需要考虑使用其他更灵活的类型。
6.Char和VarChar的区别#
CHAR
和VARCHAR
是两种常见的字符串数据类型,它们在存储方式和性能上有所不同。CHAR
- 固定长度:
CHAR
类型的字符串是固定长度的。如果定义了CHAR(10)
,无论存储的字符串长度是多少,都会占用10个字符的空间。如果实际存储的字符串长度不足10个字符,系统会在末尾自动填充空格来补齐长度。- 存储效率:由于是固定长度,
CHAR
在存储时效率较高,适用于存储长度固定的字符串,如国家代码、邮政编码等。- 检索速度:检索速度相对较快,因为长度固定,读取时更直接。
VARCHAR
- 可变长度:
VARCHAR
类型的字符串是可变长度的。VARCHAR(10)
可以存储1到10个字符的字符串,实际占用的空间与存储字符串的长度相等,外加一个或两个字节用于记录字符串的长度。- 存储效率:由于是可变长度,所以在存储时会根据字符串的实际长度来分配存储空间,适合存储长度不确定的字符串,如用户名、电子邮件地址等。
- 检索速度:由于长度不固定,检索时需要计算实际的字符串长度,可能比
CHAR
稍慢。总结
CHAR
适用于固定长度的字符串,效率高但可能浪费空间。VARCHAR
适用于长度不确定的字符串,节省空间但检索稍慢。选择时可以根据具体需求来决定使用哪种类型。
7.mysql一张表能添加多少个索引列#
在 MySQL 中,一张表可以添加多个索引列,但具体数量会受到数据库版本和配置的限制。
主要限制因素:
MySQL版本:
- 在 MySQL 8.0 版本及以上,一个表最多可以有 64 个索引(不算主键)。
- 在 MySQL 5.7 及以下版本,一个表最多可以有 16 个索引。
组合索引的列数:
- 对于单个组合索引,最多可以包含 16 个列。如果超出此限制,将无法创建组合索引。
注意事项:
- 虽然可以创建多个索引,但要合理设计索引数量。过多的索引会增加写操作的开销(如插入、更新、删除),并占用额外的存储空间。
- 索引的选择应基于查询需求,尽量避免不必要的重复索引。
所以,在设计数据库表时,建议在确保查询效率的前提下,合理使用索引。
8.mysql innoDB支持的锁#
InnoDB 是 MySQL 中常用的存储引擎之一,支持多种类型的锁机制,以确保数据的一致性和并发控制。以下是 InnoDB 支持的几种主要锁类型:
1. 表级锁(Table Locks)
意向锁(Intention Locks):在执行行级锁之前,InnoDB 会先为表加上意向锁。这种锁不阻塞任何类型的 DML 操作,但用于表明将要对某些行加锁。
意向共享锁(IS):表示事务打算对某些行加共享锁。
意向排他锁(IX):表示事务打算对某些行加排他锁。
2. 行级锁(Row Locks)
共享锁(S 锁,Shared Lock):又称读锁。允许多个事务同时读取一行数据,但不允许任何事务对该行进行修改。
排他锁(X 锁,Exclusive Lock):又称写锁。只允许持有该锁的事务对行进行读取和修改,其他事务在释放锁之前无法对该行加任何锁。
3. 间隙锁(Gap Locks)
InnoDB 在使用
Next-Key Locking
时,会加上间隙锁。间隙锁锁定的是索引记录之间的“间隙”,用于防止其他事务在间隙中插入数据,避免幻读现象。Next-Key 锁:是一种组合锁,锁定当前索引记录和它的前一个间隙,避免新数据插入到这一范围内。
4. 临键锁(Record Locks)
这种锁仅锁定索引记录本身,而不涉及记录之间的间隙。适用于明确操作单行数据的情况。
5. 自增锁(AUTO-INC Locks)
这是 InnoDB 为了处理
AUTO_INCREMENT
类型的字段而引入的一种特殊表级锁。自增锁会在插入时对表进行短暂的加锁,确保自增列的值不会因为并发插入而产生冲突。6. 外键约束锁(Foreign Key Constraints Locks)
当涉及外键约束时,InnoDB 会对涉及的父表或子表加上相应的锁,以确保外键的完整性。
7. 插入意向锁(Insert Intention Locks)
当多个事务在同一个间隙中插入数据时,会使用插入意向锁。这种锁允许多个事务并发插入而不互相阻塞,前提是插入位置不同。
总结
InnoDB 的锁机制非常复杂和灵活,能够支持高并发情况下的数据一致性需求。了解这些锁类型对于优化 MySQL 的性能和解决锁相关问题至关重要。
9.什么是普通索引?唯一索引一定比普通索引快吗? #
普通索引(Normal Index)
普通索引,也称为非唯一索引,是 MySQL 中最常见的一种索引类型。普通索引允许列中的值重复,这意味着多个记录可以有相同的索引值。它主要用于提高查询的速度。
特点:
允许重复值:普通索引不会强制唯一性约束,这意味着索引列中的值可以重复。
提高查询速度:虽然普通索引可以加快查询速度,但它在查询性能上不如唯一索引,因为数据库引擎在普通索引中查找特定值时,可能需要扫描多个匹配的索引项。
唯一索引(Unique Index)
唯一索引是一种特殊的索引,它强制索引列中的所有值都是唯一的。即使尝试插入重复的值,也会导致插入操作失败。
特点:
唯一性约束:唯一索引确保列中的每个值都是唯一的,适合用来对那些需要唯一性的字段建立索引,如用户ID、邮箱地址等。
更快的查询:在理论上,唯一索引的查询速度通常会比普通索引更快一些,因为数据库引擎可以更快地确定某个值是否存在,并且一旦找到匹配的值就可以停止搜索。
唯一索引一定比普通索引快吗?
不一定。虽然唯一索引在查找特定值时可能会更快,但这并不意味着它在所有情况下都比普通索引快。
原因包括:
查询类型不同:如果查询需要扫描大量记录,或者涉及范围查询(如
BETWEEN
或LIKE
操作),唯一索引和普通索引的性能差异可能很小,甚至可能相同。数据分布和查询模式:如果数据分布不均匀,或者查询的模式并不依赖于唯一性(如查询重复数据的场景),那么普通索引的性能可能并不逊色于唯一索引。
存储引擎的优化:现代数据库引擎在处理索引时做了大量优化,因此唯一索引和普通索引之间的性能差异在实际使用中可能并不显著。
总结
普通索引适合不需要唯一性约束的字段,允许重复值。
唯一索引保证列值唯一,通常在查找特定值时更快,但不一定在所有情况下都比普通索引快。
索引选择应根据具体的业务需求和查询模式来决定。
10.设计数据库的范式 #
数据库设计中的范式(Normalization)是一种组织数据的原则,旨在减少数据冗余和提高数据一致性。范式通过分解表结构来确保每张表只包含与特定主题相关的数据。以下是常见的数据库范式:
1. 第一范式(1NF, First Normal Form)
定义:表中的每一列都是原子的,即每个列的数据类型都应该是不可再分的。所有列的值都是单一值,不允许有重复的列,也不允许有重复的行。
实现:确保表中的每一列都是单一值(如不允许数组、列表等复合数据类型)。
例子:如果一个表中有一列
电话
,它包含多个电话号码,那么需要将这些电话号码拆分到多个行中,每行一个电话号码,或者将其拆分为多个列。2. 第二范式(2NF, Second Normal Form)
定义:在满足第一范式的基础上,表中的非主属性必须完全依赖于主键(即没有部分依赖)。这里的“主键”可以是单一字段,也可以是组合字段。
实现:如果表的主键是由多个字段组成的,确保表中的每个非主属性都依赖于整个主键,而不是主键的一部分。
例子:假设有一个订单表,其主键由
订单ID
和商品ID
组成。如果表中有一个列商品名称
,它只依赖于商品ID
,这就违反了第二范式。需要将商品名称
移到一个单独的商品表中。3. 第三范式(3NF, Third Normal Form)
定义:在满足第二范式的基础上,表中的非主属性不依赖于其他非主属性(即没有传递依赖)。
实现:确保表中的每个非主属性都只依赖于主键,而不依赖于其他非主属性。
例子:如果在一个学生表中,存在
学号
、班级ID
和班级名称
,其中班级名称
依赖于班级ID
,而班级ID
又依赖于学号
,这就违反了第三范式。应该将班级名称
移到一个独立的班级表中。4. BCNF范式(Boyce-Codd Normal Form)
定义:BCNF 是第三范式的一个加强版。在 BCNF 中,每个表中的决定因素(candidate key)都必须是候选键。这意味着表中的每一个非主属性都必须依赖于一个候选键,而不仅仅是主键。
实现:通过识别表中的候选键,确保非主属性完全依赖于这些候选键,而不是部分依赖或传递依赖。
例子:假设有一张表包含
教师ID
、课程ID
和教师姓名
,其中一个教师可以教授多个课程。这时,课程ID
和教师ID
的组合可能是候选键,但教师姓名
应该依赖于教师ID
,而不是课程ID
。5. 第四范式(4NF, Fourth Normal Form)
定义:在满足 BCNF 的基础上,消除表中的多值依赖(Multivalued Dependencies)。即,一个表中的某个列不应该依赖于另一个列的多个值。
实现:通过将表拆分成更细粒度的表,确保每个表只包含一组互相独立的数据关系。
例子:假设有一个学生选课表,记录学生可以选择多个课程,并且每门课程可能由多个教师教授。为了满足第四范式,需要将学生和课程的关系,以及课程和教师的关系分开存储。
6. 第五范式(5NF, Fifth Normal Form)
定义:在满足第四范式的基础上,消除冗余数据,以确保数据的所有信息都可以从单独的关系中重构,而不引入额外的数据。
实现:通过进一步拆分表结构,确保每个表中的数据无法再分而不引起数据丢失或冗余。
例子:假设有一张表存储客户、产品和销售地区的关系,如果要确保无论从哪个维度(客户、产品、地区)进行组合查询都不会产生冗余数据,就需要进一步将表拆分成多个关系表。
总结
范式化是设计高效数据库结构的关键,但并非总是越高的范式越好。在实际应用中,有时为了性能优化,可能需要对范式化进行适当的妥协和反范式化。因此,设计数据库时需要根据具体需求,权衡数据一致性和查询性能之间的关系。
11.一千万数据的表如何分页查询#
在面对具有大量数据的表(如有一千万条记录的表)时,进行分页查询时需要特别注意性能优化,以避免影响数据库的响应速度。以下是几种常用的分页查询方法及其优化策略:
1. 基本分页查询
- 使用
LIMIT OFFSET
:问题:当
offset
值很大时(如几百万或几千万),查询性能会明显下降,因为数据库需要扫描和跳过大量记录。2. 优化的分页查询
使用索引优化:
- 确保分页查询中使用的字段有适当的索引。例如,如果按照
id
进行分页,可以确保id
字段有索引。这种方法通过记住上一页的最后一个
id
,避免了大offset
的问题。
- 延迟关联(Deferred Join):
- 先从索引中获取分页的
id
列表,然后再进行主查询:优点:减少了排序和跳过的记录量,但适合于较小的页面大小。
3. 使用子查询
- 改进版分页:
- 在大数据量的情况下,直接通过索引字段和子查询来分页可以提升性能。
原理:通过子查询获取当前页的起始位置,然后从该位置开始查询,减少了
OFFSET
带来的性能开销。4. 物化视图或预计算表
- 创建预计算表:
- 对于经常进行的大数据量分页查询,可以考虑使用物化视图或创建一个中间表,预先存储查询的部分结果,定期更新,提升查询速度。
5. 缓存结果
- 使用缓存:
- 对于频繁访问的分页数据,可以考虑将结果缓存到内存数据库如 Redis 中,从而避免频繁查询数据库。
6. 键集分页(Keyset Pagination)
- 基于键集的分页:
- 使用唯一索引列(如主键)进行分页,而不是使用
OFFSET
,这种方法的性能更加稳定。优点:随着页面增大,查询性能几乎不受影响。
总结
LIMIT OFFSET
方法适合于小规模数据分页,对于大数据量要谨慎使用。- 优先考虑基于索引字段的分页策略,如通过
id
进行分页。- 对于极大数据量的分页,可以通过缓存、预计算表或物化视图来提升性能。
- 使用合适的索引和查询计划,减少数据库的计算和扫描开销,是分页查询优化的核心。
根据实际情况选择合适的分页查询策略,特别是在数据量巨大时,可以显著提高查询效率和用户体验。
12.有一张订单表,如果数据越存越多,怎么处理? #
当订单表的数据量随着时间的推移不断增长时,处理这些数据以确保数据库性能和存储效率至关重要。以下是几种常见的处理策略:
1. 分区表(Partitioning)
什么是分区?
分区表是将表的数据按一定规则分成多个物理子表,查询时系统可以只访问相关分区,从而提高查询效率。
分区方式:
范围分区(Range Partitioning):根据某个列的值(如日期)来划分。例如,将订单表按年份或月份进行分区。
列表分区(List Partitioning):根据某个列的具体值进行分区,例如按地区或订单状态进行分区。
哈希分区(Hash Partitioning):使用哈希函数将数据均匀分布到多个分区。
优点:通过分区,可以减少查询扫描的数据量,提升查询效率,同时分区也便于管理历史数据。
2. 归档历史数据
什么是数据归档?
将较早的、访问频率低的历史数据移到专门的归档表或历史库中,以减小主表的大小和提升主表的查询性能。
实现方式:
创建归档表:可以按年、季度或月定期将旧订单数据移动到归档表中。主表只保留最近的订单数据。
使用存储过程或定时任务:通过 MySQL 的事件调度器(Event Scheduler)或外部任务调度器(如 Cron)定期归档数据。
查询方式:
用户查询时,可以默认查询主表,如果需要查询历史数据,查询归档表。
3. 索引优化
索引管理:
随着数据量增加,现有的索引可能不再高效。定期分析查询模式,创建适合新查询需求的索引,或删除冗余的索引。
覆盖索引:确保常用查询可以通过索引直接获取数据,减少回表操作,提升查询速度。
4. 垂直拆分
垂直拆分:
如果订单表包含大量列,而部分列并不常用,可以将这些不常用的列拆分到独立的表中,以减少主表的宽度。
查询优化:通过减少主表的宽度,查询所需扫描的记录大小减小,从而提高查询速度。
5. 水平拆分(Sharding)
水平拆分:
将订单表按一定规则(如用户ID、订单ID)拆分到不同的物理数据库或表中。每个拆分后的表称为一个“分片”。
优点:
每个分片的数据量较小,查询和插入操作更快。
实现方式:
可以使用应用层的分片逻辑,也可以借助数据库中间件(如 MyCAT)来实现透明分片。
6. 数据压缩
什么是数据压缩?
利用 MySQL 的压缩技术减少数据的存储空间。特别是对于只读的历史数据,压缩存储可以节省大量空间。
实现方式:
可以使用 MySQL 的压缩表选项,或者在文件系统层面上对数据文件进行压缩存储。
7. 定期清理无用数据
无用数据清理:
如果订单表中存在已取消、无效或重复的订单,可以定期清理这些数据,以减少数据表的大小。
自动化清理:使用存储过程或定时任务自动清理无用数据。
8. 数据库分库
分库策略:
当订单数据量大到单一数据库无法承载时,可以将数据拆分到多个数据库中。
常见策略包括按用户分库、按订单ID分库等。
9. 缓存和CDN
缓存热数据:
使用缓存系统(如 Redis)存储经常访问的订单数据,减少对数据库的直接访问。
CDN加速:对于频繁访问的静态数据(如订单的相关静态资源),可以使用 CDN 来加速访问。
总结
随着订单数据的增长,适时采用分区、归档、索引优化、垂直和水平拆分等策略可以有效应对数据量增长带来的性能问题。选择合适的策略应根据具体的业务需求和数据访问模式,确保数据库在高效处理大量数据的同时保持良好的性能。
13.mysql事物底层实现原理是什么?持久化原理是什么?#
JVM#
1.说一下jvm运行时数据区域#
JVM(Java虚拟机)在运行时将其管理的内存划分为多个不同的数据区域,每个区域有其特定的用途和生命周期。这些区域统称为 JVM 运行时数据区域。以下是 JVM 运行时数据区域的主要组成部分:
1. 程序计数器(Program Counter Register)
概述:程序计数器是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。
作用:在任意一个时刻,CPU都在执行某一条特定的指令,程序计数器存储的就是当前线程所执行的字节码指令的地址。如果是方法调用,它会存储当前方法的入口地址。如果是 Native 方法,程序计数器值为空(undefined)。
多线程:由于 JVM 的多线程是通过线程轮流切换并分配处理器执行时间来实现的,因此每个线程都有自己的程序计数器,线程独立,各不影响。
2. Java虚拟机栈(JVM Stack)
概述:每个线程创建时都会同时创建一个虚拟机栈,虚拟机栈描述的是 Java 方法执行的内存模型。
作用:每个方法被调用时都会创建一个称为“栈帧”的数据结构,栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等。方法调用到完成,对应一个栈帧在栈中的入栈和出栈。
局部变量表:用于存储方法的参数和局部变量。在方法执行时分配的基本数据类型和对象引用(非对象本身)都存储在这里。
线程私有:JVM 栈是线程私有的,生命周期与线程相同。
3. 本地方法栈(Native Method Stack)
概述:本地方法栈为虚拟机使用到的 Native 方法服务,类似于 Java 虚拟机栈。
作用:它保存了每个本地方法调用时所需的状态,例如局部变量、操作数栈等,具体实现取决于使用的 JVM。
线程私有:本地方法栈也是线程私有的,生命周期与线程相同。
4. 堆(Heap)
概述:堆是 JVM 内存管理中最大的一块区域,几乎所有的对象实例以及数组都在堆上分配。
作用:堆是垃圾收集器管理的主要区域之一,因此又称为“GC 堆”。
分代:堆内存通常被划分为“新生代”和“老年代”两部分,新生代又细分为“Eden 区”和两个“Survivor 区”。大多数新创建的对象分配在新生代,生命周期较长的对象最终晋升到老年代。
线程共享:堆是线程共享的,所有线程访问的对象都存储在这里。
5. 方法区(Method Area)
概述:方法区也称为“永久代”或“元空间”(在 Java 8 之前称为永久代,在 Java 8 之后用元空间代替),用于存储已被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
作用:它保存了类的元数据信息,如类名、访问修饰符、字段描述、方法描述等。也存放运行时常量池、方法数据和方法代码等。
线程共享:方法区是线程共享的。
6. 运行时常量池(Runtime Constant Pool)
概述:运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用。
作用:常量池中的内容包括编译期确定的数值常量和符号引用,以及运行时解析得到的字段、方法引用等。动态常量池可以在运行时将常量添加到池中。
线程共享:运行时常量池是线程共享的。
7. 直接内存(Direct Memory)
概述:直接内存并不是 JVM 规范的一部分,但它也是 JVM 内存管理中不可忽视的一部分。它是由 Java NIO 中的 DirectByteBuffer 类使用的,直接分配在物理内存中的内存空间。
作用:直接内存不在堆上分配,减少了堆内存的使用,提高了 I/O 操作的性能,但它仍然受制于操作系统的内存管理。
线程共享:直接内存是线程共享的。
总结
JVM 的运行时数据区域各有分工,程序计数器和 JVM 栈主要用于执行控制和方法调用,堆是对象实例的存储空间,方法区则存储类信息和常量池。这些区域的合理管理和优化对于 Java 应用程序的性能至关重要。
2.详细介绍一下jvm的栈 #
JVM 的栈,即 Java 虚拟机栈(JVM Stack),是 JVM 运行时数据区域的重要组成部分。每个线程在创建时都会拥有自己的 JVM 栈,用于管理 Java 方法的调用与执行,并存储每个方法执行所需的数据。
JVM 栈的作用
JVM 栈的主要作用是存储线程执行方法时的栈帧。每个方法在调用时,都会创建一个栈帧(Stack Frame),用于存储局部变量、操作数、动态链接信息和方法返回地址等内容。
栈帧的结构
栈帧是 JVM 栈中的基本单元,它包含了执行一个方法所需的所有信息。一个栈帧主要包括以下几个部分:
局部变量表(Local Variable Table):
作用:存储方法的参数和局部变量。
结构:局部变量表是一个数组,每个元素称为一个“槽位(Slot)”。每个基本数据类型(如
int
、float
)和对象引用(Reference
)都可以存储在槽位中。Slot 分配:基本数据类型
int
、float
等占用一个 Slot,而long
和double
占用两个连续的 Slot。对象引用只占用一个 Slot,存储的是指向堆中对象的引用。操作数栈(Operand Stack):
作用:用于在方法执行过程中临时存储数据和进行操作。操作数栈是一个后进先出(LIFO)的栈。
操作:方法执行时,指令会将数据压入操作数栈或从中弹出数据。例如,加法指令会从操作数栈中弹出两个数,进行运算后再将结果压回栈中。
栈深度:操作数栈的深度在编译时已经确定,不会在运行时发生变化。
动态链接(Dynamic Linking):
作用:提供方法调用中符号引用的解析。栈帧中包含了指向方法所在类的运行时常量池的引用,动态链接用来将符号引用解析为实际的内存地址。
指向常量池的指针:用于支持方法调用时的动态链接,确保方法和字段在运行时能够正确引用。
方法返回地址(Return Address):
作用:当一个方法执行完毕后,程序需要返回到调用该方法的位置继续执行,这个地址就是方法返回地址。
异常处理:如果在方法执行过程中发生了异常,方法返回地址也可能指向异常处理表,控制异常的处理流程。
JVM 栈的生命周期
线程私有:JVM 栈是线程私有的,每个线程拥有独立的 JVM 栈。线程的每次方法调用和执行都在自己的 JVM 栈上操作,不与其他线程共享。
栈的创建与销毁:JVM 栈的生命周期与线程一致。当线程创建时,JVM 栈也随之创建;当线程结束时,JVM 栈也随之销毁。
栈的大小和调整
栈的大小:JVM 栈的大小可以通过 JVM 启动参数
-Xss
来设置。栈的大小决定了栈帧的数量和深度,也即能嵌套调用的方法数量。调整策略:在实际应用中,根据线程的调用深度和所需的栈大小,适当调整 JVM 栈的大小,以避免栈溢出错误。
栈的异常
栈溢出(StackOverflowError):
原因:如果线程调用的方法嵌套太深(如递归调用没有正确终止),超出了栈的最大深度,就会引发
StackOverflowError
。这通常发生在递归调用过多或递归深度超过 JVM 栈的容量时。内存溢出(OutOfMemoryError):
原因:如果 JVM 栈需要的内存超过了可以分配的内存,且没有足够的内存扩展 JVM 栈时,会抛出
OutOfMemoryError
。总结
JVM 栈是 Java 虚拟机管理方法调用和执行的重要区域,每个线程都有自己的栈,栈由栈帧组成。栈帧包含局部变量表、操作数栈、动态链接信息和方法返回地址。JVM 栈的大小与线程的方法调用深度密切相关,设置合理的栈大小对于程序的稳定性至关重要。
3.一个方法调用另外一个方法会创建很多栈吗? #
在 JVM 中,每当一个方法调用另一个方法时,不会创建新的栈。相反,它会在当前线程的栈中创建一个新的栈帧(Stack Frame)。
详细解释
同一线程的栈:每个线程在创建时会分配一个独立的 JVM 栈。这个栈包含了该线程执行过程中所有方法的栈帧。栈帧存储了方法的局部变量、操作数栈、动态链接信息和返回地址等。
方法调用时的栈帧:当一个方法调用另一个方法时,JVM 会为被调用的方法创建一个新的栈帧,并将其压入当前线程的 JVM 栈顶部。新的栈帧包含了被调用方法所需的所有执行信息。
栈帧的入栈和出栈:
当方法 A 调用方法 B 时,JVM 会为方法 B 创建一个新的栈帧并将其压入栈中。此时,方法 A 的栈帧会在栈的下面,方法 B 的栈帧在上面。
当方法 B 执行完毕后,方法 B 的栈帧会从栈中弹出,控制权返回到方法 A,方法 A 的栈帧又成为栈顶的栈帧。
栈的生命周期:栈帧的生命周期与方法的执行周期一致,当一个方法执行完毕后,其对应的栈帧就会从 JVM 栈中移除(出栈)。
- 当
methodA()
被调用时,JVM 会为methodA()
创建一个栈帧,并压入当前线程的 JVM 栈。- 在
methodA()
内部调用methodB()
时,JVM 会为methodB()
创建一个新的栈帧,并将其压入栈顶。- 接着,
methodB()
调用methodC()
时,JVM 又会为methodC()
创建一个栈帧,压入栈顶。- 当
methodC()
执行完毕后,其栈帧会从栈中弹出,栈顶变为methodB()
的栈帧。- 然后
methodB()
执行完毕,栈顶变为methodA()
的栈帧。- 最后
methodA()
执行完毕,栈帧弹出,JVM 栈为空(假设这是线程中的最后一个方法)。总结
在同一线程中,方法调用时不会创建新的 JVM 栈,而是通过在现有栈中添加栈帧来管理方法的调用和返回。每次方法调用都会创建一个新的栈帧,方法执行完毕后栈帧会被移除。这种机制使得 JVM 能够有效地管理方法调用链和局部变量的作用域。
4.详细解释一下jvm的堆#
JVM 的 堆(Heap) 是 Java 虚拟机管理内存的核心区域之一,主要用于存储 Java 对象及其相关数据。堆是垃圾收集器(GC)管理的主要区域,对性能和内存管理至关重要。以下是对 JVM 堆的详细解释:
堆的结构和功能
堆的结构:
堆空间分区:堆内存通常被划分为不同的区域,包括:
新生代(Young Generation):存储新创建的对象。新生代又被进一步划分为三个区域:
Eden 区:所有新创建的对象首先分配在 Eden 区。
Survivor 区(S0 和 S1):Eden 区中的存活对象会被复制到一个或两个 Survivor 区。每个 Survivor 区用于存放从 Eden 区复制过来的对象。
老年代(Old Generation/ Tenured Generation):存储经过多次垃圾回收后仍然存活的对象。老年代通常用于存放生命周期较长的对象。
永久代/元空间(Permanent Generation/ Metaspace):在 Java 8 之前称为永久代,存储类的元数据,如类信息、常量池等。在 Java 8 及之后,永久代被元空间取代,元空间不再使用堆内存,而是使用本地内存。
堆的功能:
对象存储:堆是所有对象实例和数组的主要存储区域。
垃圾回收:垃圾收集器主要在堆内存中进行工作,通过标记、清除、压缩等算法回收不再使用的对象,释放内存空间。
垃圾回收机制
堆内存的垃圾回收机制非常重要,以确保有效使用内存和提高应用程序性能。主要的垃圾回收算法包括:
标记-清除算法(Mark-and-Sweep):
过程:首先标记所有需要回收的对象,然后清除那些没有标记的对象。
优点:简单直接,能有效回收不再使用的对象。
缺点:清除过程中可能会产生内存碎片,影响性能。
复制算法(Copying):
过程:将活动对象从一个区域(如 Eden 区)复制到另一个区域(如 Survivor 区),未使用的对象在复制过程中被丢弃。
优点:高效地回收对象,减少内存碎片。
缺点:需要额外的内存空间来存放复制的对象。
标记-整理算法(Mark-and-Compact):
过程:标记活动对象后,将这些对象移动到堆的一个端,清理未使用的对象,整理内存。
优点:减少内存碎片,提高内存使用效率。
缺点:可能会增加整理时间的开销。
分代收集(Generational Collection):
过程:将堆内存分为新生代和老年代,不同区域使用不同的垃圾回收策略。新生代采用复制算法,而老年代则使用标记-整理算法。
优点:根据对象的生命周期特性选择不同的回收策略,提高回收效率。
缺点:复杂的回收策略可能增加调试难度。
堆的参数配置
在 JVM 启动时,可以通过一些参数来配置堆的大小:
初始堆大小(-Xms):
作用:设置 JVM 启动时堆内存的初始大小。
示例:
-Xms512m
表示初始堆大小为 512 MB。最大堆大小(-Xmx):
作用:设置 JVM 可以使用的最大堆内存大小。
示例:
-Xmx2g
表示最大堆大小为 2 GB。新生代大小(-Xmn):
作用:设置新生代的大小,通常是堆总大小的比例。
示例:
-Xmn256m
表示新生代大小为 256 MB。堆的内存管理
堆内存管理:堆内存的管理涉及到对象分配、垃圾回收、内存碎片整理等。合理配置堆内存大小和选择合适的垃圾回收策略,可以有效提高应用程序的性能。
内存泄漏:如果应用程序中存在引用未被清理的对象,可能导致内存泄漏,影响应用性能。因此,需要定期监控和分析堆内存使用情况,及时发现并解决内存泄漏问题。
总结
JVM 的堆是 Java 虚拟机的关键内存区域,用于存储对象实例和数组。堆的垃圾收集机制负责回收不再使用的对象,以释放内存空间。通过合理配置堆内存参数和选择合适的垃圾回收策略,可以优化 JVM 的性能和内存管理。
5.深拷贝与浅拷贝#
在 Java 编程中,深拷贝(Deep Copy) 和 浅拷贝(Shallow Copy) 是用于复制对象的两种主要方法。理解它们的区别对于正确地管理对象复制和避免潜在的错误非常重要。
浅拷贝(Shallow Copy)
浅拷贝 是指创建一个新的对象,但新的对象中的字段与原对象中的字段共享相同的引用。换句话说,浅拷贝会复制对象的基本数据类型字段,但不会复制引用字段指向的对象。结果是,新对象和原对象中的引用字段指向相同的对象。
特点
- 复制:复制对象的基本数据类型字段的值(如
int
、float
等),以及对象引用的地址,但不复制引用的对象本身。- 引用共享:浅拷贝后的对象与原对象共享引用类型字段的实际对象,修改这些对象会影响到原对象。
- 方法:在 Java 中,浅拷贝通常通过
Object
类中的clone()
方法来实现,这个方法默认执行浅拷贝。深拷贝(Deep Copy)
深拷贝 是指创建一个新的对象,同时递归地复制原对象中的所有引用对象,确保新对象与原对象完全独立。深拷贝会复制对象的基本数据类型字段及其引用字段指向的对象,而不仅仅是引用本身。
特点
- 复制:不仅复制对象的基本数据类型字段,还会递归地复制引用字段所指向的对象,从而创建完全独立的对象。
- 引用独立:深拷贝后的对象与原对象完全独立,修改新对象不会影响原对象。
- 方法:深拷贝通常需要手动实现,或者通过序列化(Serialization)来实现。可以通过自定义
clone()
方法、构造函数或者使用ObjectOutputStream
和ObjectInputStream
实现深拷贝。总结
- 浅拷贝 仅复制对象的基本数据类型字段及引用字段的引用,引用类型字段指向的对象不被复制,原对象和新对象共享这些引用对象。
- 深拷贝 复制对象及其引用字段指向的所有对象,确保原对象和新对象完全独立,避免了共享引用导致的问题。
选择使用浅拷贝还是深拷贝取决于具体的需求。如果需要复制的对象中包含复杂的引用关系或嵌套对象,通常需要实现深拷贝以确保对象的完全独立性。
6.jvm中的引用类型(四大引用类型)#
在 Java 中,JVM 提供了四种不同类型的引用来管理对象的生命周期和垃圾回收。这四种引用分别是:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference) 和 虚引用(Phantom Reference)。每种引用类型都有不同的特性和用途,用于不同的场景来优化内存管理和资源回收。
1. 强引用(Strong Reference)
强引用 是 Java 中最常用的引用类型,也是默认的引用类型。它指向对象时,垃圾回收器不会回收这个对象,只要强引用还存在,垃圾回收器就不会将对象回收。
特点
- 生存期:强引用的对象在强引用存在期间不会被回收。
- 用途:用于普通对象的引用。
2. 软引用(Soft Reference)
软引用 用于对那些可能有用但不是必需的对象进行引用。如果内存不足,垃圾回收器会优先回收这些对象。在内存充足的情况下,软引用指向的对象会尽可能保留。
特点
- 生存期:在系统内存充足时,软引用指向的对象会被保留;在内存不足时,垃圾回收器会回收这些对象。
- 用途:用于缓存数据,例如实现内存缓存系统。
3. 弱引用(Weak Reference)
弱引用 用于对那些不重要的对象进行引用。如果垃圾回收器发现对象仅被弱引用引用时,它会立即回收这个对象。弱引用适用于那些缓存数据但不重要的情况。
特点
- 生存期:只要垃圾回收器运行,弱引用指向的对象就会被回收,不会被保留。
- 用途:用于缓存,特别是那些需要在内存不足时能够及时释放的对象。
4. 虚引用(Phantom Reference)
虚引用 是最弱的一种引用类型。虚引用主要用于跟踪对象的垃圾回收过程。虚引用的对象一旦被回收,虚引用对象本身不会对垃圾回收做任何影响,虚引用仅用于实现一些清理操作,比如对象的最终清理。
特点
- 生存期:虚引用的对象一旦被回收,虚引用对象的
get()
方法会返回null
。虚引用对象不会阻止垃圾回收器回收对象。- 用途:用于进行对象的最终清理操作,如清理资源。
总结
- 强引用:默认引用类型,对象只要有强引用就不会被回收。
- 软引用:适用于内存缓存,如果内存不足,垃圾回收器会回收这些对象。
- 弱引用:用于引用那些不重要的对象,垃圾回收器会优先回收这些对象。
- 虚引用:用于跟踪对象的垃圾回收过程,进行最终清理操作。
每种引用类型有不同的应用场景,选择合适的引用类型可以帮助更好地管理内存和优化性能。
7.jvm的垃圾回收算法有哪些? #
JVM 的垃圾回收(GC)算法旨在自动管理内存,回收不再使用的对象,避免内存泄漏和提高应用程序的性能。JVM 提供了多种垃圾回收算法,主要包括以下几种:
1. 标记-清除算法(Mark-and-Sweep)
标记-清除算法 是最基本的垃圾回收算法,主要包括两个阶段:标记阶段和清除阶段。
特点
标记阶段:垃圾回收器遍历所有活跃的对象(从根对象开始),标记所有可达的对象。
清除阶段:遍历整个堆内存,清除未被标记的对象,即不再使用的对象。
优点
简单:算法简单易懂,实现也比较容易。
缺点
内存碎片:清除阶段可能会产生内存碎片,影响性能。
效率低:在大型堆中,标记和清除的过程可能较为耗时。
2. 复制算法(Copying)
复制算法 通过将活动对象从一个区域复制到另一个区域来进行垃圾回收。常用于新生代垃圾回收。
特点
分代:将堆分为两个区域,称为“From 区”和“To 区”。
复制:将 From 区中的活动对象复制到 To 区,清理 From 区中未被复制的对象。
优点
高效:复制算法的垃圾回收过程较为高效,因为对象的整理工作由复制操作完成。
避免碎片:由于对象被复制到新区域,避免了内存碎片的问题。
缺点
内存消耗:需要额外的内存来存放 To 区,以便进行复制操作。
3. 标记-整理算法(Mark-and-Compact)
标记-整理算法 是在标记-清除算法的基础上进行改进的,解决了内存碎片的问题。
特点
标记阶段:标记所有活跃的对象。
整理阶段:将所有标记的对象移动到堆的一端,整理剩余的内存块。
优点
减少碎片:通过整理内存块,减少内存碎片,提高内存使用效率。
缺点
整理开销:整理阶段需要额外的计算开销,可能影响性能。
4. 分代收集算法(Generational Collection)
分代收集算法 是一种基于对象生命周期的垃圾回收策略,主要将堆内存划分为新生代和老年代,分别采用不同的回收策略。
特点
新生代:使用复制算法,频繁回收新创建的对象。
老年代:使用标记-整理算法或标记-清除算法,回收生命周期较长的对象。
分代:通过将对象分代处理,提高回收效率,减少回收开销。
优点
提高效率:针对不同对象的生命周期采用不同的回收策略,提高整体回收效率。
缺点
复杂性:分代策略需要管理多个区域和策略,复杂性较高。
5. G1 垃圾回收器(Garbage-First)
G1 垃圾回收器 是 JDK 7 引入的,旨在实现高效的垃圾回收,特别是针对大内存和低延迟应用。
特点
分区:将堆分成多个大小相同的区域(Region),这些区域可以是 Eden 区、Survivor 区或老年代。
回收:G1 收集器会根据垃圾量和回收成本,优先回收垃圾最多的区域(因此得名“Garbage-First”)。
预测:G1 能够预测回收时间,减少应用程序的停顿时间。
优点
低延迟:通过分区和预测,优化垃圾回收的时间和效率,减少停顿时间。
缺点
复杂性:G1 收集器的实现和调优较为复杂。
6. ZGC(Z Garbage Collector)
ZGC 是从 JDK 11 开始引入的低延迟垃圾回收器,旨在实现非常低的暂停时间,适用于大内存、高并发的应用。
特点
并发回收:几乎所有的垃圾回收工作都在并发中完成,减少停顿时间。
分代回收:支持分代回收,但采用不同的策略来提高性能。
优点
极低延迟:ZGC 可以在纳秒级别进行垃圾回收,适合对延迟敏感的应用。
缺点
成熟度:相比其他垃圾回收器,ZGC 可能在某些场景下不够成熟。
7. Shenandoah GC
Shenandoah GC 是从 JDK 12 引入的低延迟垃圾回收器,类似于 ZGC,也专注于降低垃圾回收的停顿时间。
特点
并发回收:垃圾回收操作在应用线程运行时并发进行,减少停顿时间。
分代管理:支持新生代和老年代的分代管理。
优点
低停顿:与 ZGC 类似,Shenandoah GC 旨在实现低停顿的垃圾回收。
缺点
适用性:目前可能不适合所有场景,需要根据具体应用进行评估。
总结
标记-清除、复制算法 和 标记-整理 是最基本的垃圾回收算法。
分代收集 策略通过分代管理对象,提高了回收效率。
G1、ZGC 和 Shenandoah 是现代垃圾回收器,提供了低停顿和高效回收的能力。
每种垃圾回收算法和收集器都有其特定的优点和适用场景,选择合适的垃圾回收策略对于优化应用程序的性能至关重要。
Mybatis #
1.#{} 与 ${}的区别#
1)#{}是预编译处理,$ {}是字符串替换。
2)MyBatis在处理#{}时,会将SQL中的#{}替换为?号,使用PreparedStatement的set方法来赋值;MyBatis在处理 $ { } 时,就是把 ${ } 替换成变量的值。
3)使用 #{} 可以有效的防止SQL注入,提高系统安全性。
项目实战中使用,请阅读我博客中Java项目实战分类中的--
要理解记忆这个题目,我觉得要抓住两点:
(1)$ 符号一般用来当作占位符,常使用Linux脚本的同学应该对此有更深的体会吧。既然是占位符,当然就是被用来替换的。知道了这点就能很容易区分$和#,从而不容易记错了。
(2)预编译的机制。预编译是提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译。我们知道,SQL注入是发生在编译的过程中,因为恶意注入了某些特殊字符,最后被编译成了恶意的执行操作。而预编译机制则可以很好的防止SQL注入。在某些特殊场合下只能用${},不能用#{}。例如:在使用排序时ORDER BY ${id},如果使用#{id},则会被解析成ORDER BY “id”,这显然是一种错误的写法。
2.通常一个xml映射文件里的sql都与一个DAO接口中的方法对应,问DAO接口的工作原理是什么,参数不同时方法可以重载吗? #
Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象 MappedProxy,代理对象会拦截接口方法,根据类的全限定名+方法名,唯一定位到一个MapperStatement并调用执行器执行所代表的sql,然后将sql执行结果返回。
Mapper接口里的方法,是可以重载的但是Mapper.xml中对应的id只能有一个不然会报错,因为是使用 全限名+方法名 的保存和寻找策略。
(1)Dao接口,就是Mapper接口。
(2)接口的全限名,就是映射文件中的namespace的值;
(3)接口的方法名,就是映射文件中Mapper的Statement的id值;
(4)接口方法内的参数,就是传递给sql的参数。
当调用接口方法时,通过 “接口全限名+方法名”拼接字符串作为key值,可唯一定位一个MapperStatement,因为在Mybatis中,每一个SQL标签,都会被解析为一个MapperStatement对象。
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面 id 为 findStudentById 的 MapperStatement。
3.什么是物理删除,什么是逻辑删除,怎么实现逻辑删除 #
物理删除 和 逻辑删除 是两种常见的数据删除策略,主要用于管理数据库中的数据。当你需要删除数据时,可以选择其中一种策略来处理数据的实际删除或标记删除。
1. 物理删除(Physical Deletion)
物理删除 是指将数据从数据库中彻底删除。这种删除方式是将数据从数据库表中移除,使其不再存在于任何地方。
特点
- 数据彻底删除:数据在物理上被删除,不再占用存储空间。
- 恢复困难:一旦数据被物理删除,恢复数据通常非常困难,除非有备份。
- 性能:删除操作后,数据库的存储空间会释放,可能对性能有积极影响。
实现
这个 SQL 语句将
id
为1
的用户记录从users
表中删除。2. 逻辑删除(Logical Deletion)
逻辑删除 是指将数据标记为已删除,而不是从数据库表中实际删除数据。数据依然存在于数据库中,但通过某种标记或状态指示它已被删除,从而不再对应用程序可见或可用。
特点
- 数据保留:数据在数据库中保留,但通过标记或状态将其标识为已删除。
- 恢复方便:可以轻松恢复逻辑删除的数据,只需修改标记或状态。
- 存储空间:数据仍占用存储空间,可能会导致表的体积不断增长。
- 性能:查询时需要考虑排除已逻辑删除的记录,这可能会影响性能。
实现
要实现逻辑删除,可以在数据库表中添加一个标记字段(通常是布尔型、枚举型或状态字段),用以表示记录是否被删除。例如,使用一个名为
is_deleted
的布尔字段:
修改表结构:
2.逻辑删除记录:
3.查询未删除的记录(需要在查询中考虑逻辑删除的标记):
总结
- 物理删除 是将数据从数据库中彻底删除,不可恢复。
- 逻辑删除 是通过标记数据为已删除来实现删除,数据仍然保留在数据库中,可以通过修改标记来恢复数据。
逻辑删除的实现涉及到在数据库表中添加额外的标记字段,并在查询和操作数据时考虑这些标记。选择哪种删除策略取决于应用程序的需求和数据管理策略。
4.什么是mybitis的一级缓存还有二级缓存,如何开启#
在 MyBatis 中,缓存机制是用于提升查询性能的重要手段。MyBatis 提供了一级缓存和二级缓存两种缓存机制,以减少数据库访问的频率,提升系统的响应速度。以下是它们的详细介绍:
1. 一级缓存(Local Cache)
一级缓存 是 MyBatis 中默认开启的缓存机制,作用范围是 SQLSession,即在同一个 SQLSession 中执行的相同查询语句会从缓存中读取数据,而不需要再次访问数据库。
特点
- 作用范围:一级缓存是 SQLSession 级别的缓存,即只在同一个 SQLSession 中有效。
- 默认开启:MyBatis 自动开启一级缓存,不需要手动配置。
- 缓存失效:当 SQLSession 执行了
insert
、update
或delete
等更新操作时,一级缓存会被清空,以保证数据一致性。工作原理
- 当 SQLSession 执行一个查询时,MyBatis 会先检查一级缓存中是否有相同查询条件的结果,如果有则直接返回缓存数据。
- 如果缓存中没有对应的结果,MyBatis 会执行 SQL 查询,将结果存入一级缓存,并返回结果。
2. 二级缓存(Global Cache)
二级缓存 是 MyBatis 提供的可选缓存机制,作用范围是 Mapper 映射文件,即在多个 SQLSession 之间共享缓存数据。二级缓存需要手动配置并开启。
特点
- 作用范围:二级缓存是 Mapper 映射文件级别的缓存,多个 SQLSession 共享该缓存。
- 手动开启:二级缓存默认是关闭的,需要在 MyBatis 配置文件和 Mapper 文件中手动开启。
- 持久化:二级缓存的数据可以持久化到磁盘,以在应用重启时仍然有效。
开启方式
要启用二级缓存,通常需要以下几步:
配置全局启用二级缓存: 在 MyBatis 的全局配置文件
mybatis-config.xml
中启用二级缓存。2.在 Mapper 文件中配置缓存: 在需要使用二级缓存的 Mapper 映射文件中,通过
<cache>
标签启用缓存。3.确保实体类实现序列化: 使用二级缓存时,MyBatis 会将查询结果存储在缓存中,因此要缓存的对象(通常是实体类)需要实现
java.io.Serializable
接口。4.配置其他缓存属性(可选): 可以在
<cache>
标签中配置缓存的其他属性,例如缓存的大小、清除策略等。
- eviction:缓存回收策略(默认是 LRU,即最近最少使用策略)。
- flushInterval:刷新间隔时间(以毫秒为单位)。
- size:缓存的数量。
- readOnly:只读模式,如果为 true,缓存中的数据不能被修改。
总结
- 一级缓存:作用于 SQLSession 级别,默认开启。每个 SQLSession 都有自己的一级缓存,无法跨 SQLSession 共享。
- 二级缓存:作用于 Mapper 映射文件级别,多个 SQLSession 可以共享该缓存。需要手动开启,并配置相应的 Mapper 文件和实体类。
通过合理使用 MyBatis 的一级缓存和二级缓存,可以有效减少数据库的访问频率,提升系统性能。
5.当实体类中的属性名和表中的字段名不一样怎么办?说出两种1.取别名2.ResultMap #
当实体类中的属性名和数据库表中的字段名不一致时,MyBatis 提供了多种方式来解决这个问题。你可以使用以下两种常见的方法来进行映射:
1. 使用 SQL 查询中的别名
通过在 SQL 查询中使用别名,将数据库表字段名映射为与实体类属性名相同的名称。这种方法简单直接,不需要额外的配置。
示例
假设数据库表
users
中的字段是user_name
,而实体类中的属性是userName
,你可以在 SQL 语句中使用别名:在这个例子中,
user_name AS userName
将数据库中的字段user_name
映射为实体类中的属性userName
。2. 使用 ResultMap
ResultMap
是 MyBatis 提供的一种高级映射机制,允许你在 XML 映射文件中明确指定数据库字段与实体类属性之间的关系。这种方法更加灵活,适用于复杂的映射场景。配置步骤
定义 ResultMap:
在 XML 映射文件中,使用
<resultMap>
标签定义字段与属性的映射关系。
使用 ResultMap:
在 SQL 查询中使用
resultMap
属性引用定义好的ResultMap
。
说明
<resultMap>
定义了字段到属性的映射,其中id
标签用于主键映射,result
标签用于普通字段映射。property
属性指定实体类中的属性名,column
属性指定数据库表中的字段名。总结
- 取别名:在 SQL 查询中使用
AS
关键字,将数据库字段名映射为实体类的属性名,适用于简单的场景。- ResultMap:通过 XML 配置文件中定义的
<resultMap>
,显式指定字段与属性的映射关系,适用于复杂的映射场景。根据具体的需求和场景,选择合适的方式进行字段与属性的映射,可以确保数据的正确性和代码的可维护性。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix