Java
Java基础
- 基本数据类型
字节 | 默认值 | ||
---|---|---|---|
byte | 1 | 0 | |
short | 2 | 0 | |
int | 4 | 0 | |
long | 8 | 0L | |
float | 4 | 0f | |
double | 8 | 0d | |
char | 2 | ‘u0000’ | |
boolean | 1比特 | false |
引用数据类型:类,接口,数组,字符串
- 常量池技术
Java的包装类型(例如Integer、Boolean、Character等)都有一个内部的常量池,可以用来存储一些常用的包装类型对象,以便在程序中重用这些对象。这个技术被称为“包装类型常量池技术”。
当我们使用自动装箱的语法创建一个包装类型对象时,Java会首先检查该对象的值是否已经存在于常量池中。如果已经存在,Java会直接返回该对象的引用,而不是创建一个新的对象。这样可以节省内存,提高程序的性能。
- 为什么有包装技术
基本数据类型有默认值,支持泛型和集合
- 自动拆装箱
自动装箱是指Java编译器自动将基本数据类型转换为对应的包装类型,自动拆箱是指Java编译器自动将包装类型转换为对应的基本数据类型。在自动装箱时,Java编译器会自动将基本数据类型转换为对应的包装类型,实际上相当于调用了包装类型的valueOf()方法。在自动拆箱时,Java编译器会自动将包装类型转换为对应的基本数据类型,实际上相当于调用了包装类型的xxxValue()方法。
- NPE问题
数据库调用、三目运算符
//在空引用上使用.length()导致报错
String str = someCondition ? null : "Hello"; int length = str.length();
- string stringbuffer stringbuilder区别
长度 | 线程安全 | 性能 | |
---|---|---|---|
string | 不可变 | 线程安全(final) | 最低 |
stringbuffer | 可变 | 线程安全(synchronized) | 居中 |
stringbuilder | 可变 | 不安全 | 最高 |
- 重写和重载
参数,返回列表 | ||||
---|---|---|---|---|
重写 | 子类重写父类方法 | 相同 | 运行 | 动态绑定 |
重载 | 一个类中多个方法 | 不同 | 编译 | 前期绑定 |
- ==和equal的区别
==用于比较两个对象的引用(地址)是否相等,equals()用于比较两个对象的内容是否相等。equals要重写否则和==一样
String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");
System.out.println(str1 == str2); // 输出 true,因为str1和str2引用同一个字符串对象
System.out.println(str1.equals(str2)); // 输出 true,因为str1和str2的内容相等
System.out.println(str1 == str3); // 输出 false,因为str1和str3引用不同的字符串对象
System.out.println(str1.equals(str3)); // 输出 true,因为str1和str3的内容相等
- 内部类
可以访问 | 可以访问 | |||
---|---|---|---|---|
成员内部类 | 是否可以访问外部类成员变量和方法 | 是否可以被外部访问 | ||
局部内部类 | 可以访问 | 不能 | ||
匿名内部类 | 可以访问 | 不能 | ||
静态内部类 | 可以访问外部静态成员 | 不能 |
- 接口和抽象类
抽象类 | 接口 | |
---|---|---|
继承 | 单继承 | 多继承 |
构造方法 | 可以存在 | 不能存在 |
关键字 | abstract | interface |
方法 | 非抽象方法、抽象方法(子类实现) | 只能有抽象方法,子类必须实现 |
变量 | 可以有成员变量 | 只能有常量 |
- 深拷贝和浅拷贝
浅拷贝只是复制了对象的引用,而没有复制对象本身。深拷贝会递归复制一个对象及其所有的成员变量,确保新的对象与原对象没有任何关联。
深拷贝实现方法:重写对象的 clone() 方法。使用序列化和反序列化实现深拷贝。使用第三方库进行深拷贝。
注解 反射
- 反射
应用场景:动态加载类和创建对象、动态获取和操作类的成员、动态扩展应用程序、动态代理
- 注解
- 加载类:注解是定义在Java类中的,因此在解析注解之前需要先加载包含注解的类。
- 解析注解:Java在运行时通过反射机制获取类的注解,解析注解的过程实际上是解析注解类型中的元数据。
- 获取元数据:元数据包括注解中定义的各种属性,包括默认值、数据类型、是否可重复等。Java通过反射机制获取注解中的元数据,并根据元数据对注解进行解析。
- 执行注解:Java会根据注解中的元数据执行相应的操作。例如,如果注解中定义了某个属性为A,那么Java会根据属性A的值执行相应的操作。
泛型
- 泛型
使用泛型的好处在于可以在编译期间检查类型的一致性和安全性,避免在运行时出现类型转换错误和异常,提高代码的可读性和可维护性。同时,泛型还可以使代码更加通用和灵活,能够适应各种类型的数据和对象。
- 作用
类型安全性:泛型可以让程序员在编写代码时指定某些变量或方法的类型,从而在编译时进行类型检查。
可读性:泛型可以使代码更加易读和易理解。
代码复用:泛型可以使代码更加灵活,使得代码可以适用于不同类型的数据,而无需为每种类型编写重复的代码。
集合类型的安全性:Java 集合类是泛型类型,泛型可以使集合类更加安全。
- 泛型擦除机制
Java 泛型擦除机制(Generics Type Erasure)是指在编译时期将泛型类型信息擦除(erased)掉,转换为普通的非泛型类型,并且在运行时期不再有泛型类型信息存在。
- 通配符
在 Java 泛型中,通配符(Wildcard)是一种用于表示泛型类型中未知类型参数的占位符。通配符用问号 ?
表示,它可以出现在泛型类型、方法的参数列表、方法的返回值等地方。通配符可以使代码更加灵活,使得泛型类型可以适用于不同类型的数据。
无界通配符:无界通配符使用 ?
表示,它表示类型参数可以是任何类型,包括 Object。
上界通配符:上界通配符使用 extends
关键字定义,它表示类型参数必须是指定类型的子类或本身。
上边界通配符:上边界通配符使用 extends
关键字定义,它表示类型参数必须是指定类型的子类或本身。
下边界通配符:下边界通配符使用 super
关键字定义,它表示类型参数必须是指定类型的超类或本身。
- 通配符和泛型的区别
泛型可以声明为常量或者变量,通配符不行;
泛型可以声明为类或者方法;通配符不行;
泛型会被擦除,通配符捕获具体类型。
string类
异常
可检查异常:文件不存在、数据库异常
运行时异常:空引用异常、数组越界、非法参数
错误:栈溢出、内存分配异常
IO流
- 字符流和字节流
字节流操作的是字节(byte)数据,可以处理所有类型的数据,包括文本、图像、音频等二进制数据。字节流主要用于读写二进制文件(如图像、音频文件)和网络编程中传输二进制数据。
字符流操作的是字符(char)数据,可以自动处理字符编码与解码,支持多种编码格式(如UTF-8、GBK等),适合读写文本文件和处理文本数据。字符流主要用于读写文本文件,如txt、csv、xml等。
在处理字符数据时,如果直接使用字节流,需要自行实现字符编码和解码的过程,这会增加代码的复杂度和出错的可能性。而使用字符流,Java会自动完成字符编码和解码的工作,方便快捷。另外,字符流还提供了缓冲区和字符集转换等功能,可以提高数据的读写效率和灵活性。
- IO设计模式
装饰器模式:InputStream、OutputStream、Reader和Writer等流类都是基于装饰器模式实现的。使用装饰器模式可以方便地扩展I/O流的功能,同时也避免了类爆炸问题。
工厂模式:File类和FileInputStream/FileOutputStream类等都使用了工厂模式。工厂模式可以封装对象的创建过程,隐藏创建的具体实现细节,提供简单的接口给客户端使用。
观察者模式:输入流或输出流发生某些事件(如数据可用或到达流的末尾)时,可以使用观察者模式通知相应的观察者对象进行处理。
适配器模式:InputStreamReader和OutputStreamWriter等转换流就是使用适配器模式实现的。适配器模式可以将一个类的接口转换成客户端需要的另一种接口。
- 体系
字节流:输入都是 ×××InputStream
输出都是 ×××OutPutStream
字符流:输入都是 ×××Reader
输出都是 ×××Writer
BIO | 一直阻塞直到获取数据 | 适用于连接数量少的情况 |
NIO | 同步:等待IO缓冲区数据就绪;非阻塞:不用原地等待,隔段时间访问 | |
AIO | 不用阻塞等待,异步操作结束后通知进程 |
NIO:创建通道channel,创建缓冲区buffer读取和写入数据,channel注册到select中,开始监听,有一个channel就绪,就开始接收
Java 集合
- list set map
List | 有序 | 可以重复 | ArrayList、LinkedList、Vector | Collection接口 |
Set | 无序 | 不能重复 | HashSet、TreeSet | Collection接口 |
Map | 键值对 | 键不能重复 | HashMap、TreeMap、LinkedHashMap | Map |
线程安全:list: CopyOnWriteArrayList\ConcurrentLinkedDeque\ConcurrentSkipListSet
Vector、Stack、Hashtable、ConcurrentHashMap、ConcurrentLinkedQueue
线程不安全:ArrayList、LinkedList、HashSet、TreeSet、HashMap
- 分类
list
底层实现 | 线程安全 | 复杂度 | ||
---|---|---|---|---|
ArrayList(推荐) | Object数组 | 线程不安全 | 数组插入删除复杂度、随机访问 | 预留空间 |
Vector | Object数组 | 线程安全 | 数组插入删除复杂度 | |
LinkedList | 双向列表 | 线程不安全 | 链表插入删除复杂度 | 每个元素占更大空间 |
Queue和Deque
底层实现 | ||||
---|---|---|---|---|
PriorityQueue | Object数组 | 默认最小堆 | ||
ArrayQueue | Object数组+双指针 | |||
ArrayDeque | 可变长数组+双指针 |
Map
底层实现 | 线程安全 | |||
---|---|---|---|---|
HashMap | 数组+链表,超过阈值改用数组+红黑树 | 线程不安全 | 默认容量16,扩容变为2倍 | |
LinkedHashMap | HashMap+红黑树 | |||
Hashtable | 数组+链表 | 线程安全 | 默认容量11,扩容变为2n+1 | |
TreeMap | 红黑树 | 默认按key升序排列 |
Set(元素唯一、线程不安全)
底层实现 | ||||
---|---|---|---|---|
HashSet | HashMap | 不安全 | ||
LinkedHashSet | LinkedHashMap | FIFO | ||
TreeSet | 红黑树 |
- ArrayList Vector的区别
扩容 | ||||
---|---|---|---|---|
ArrayList | 线程不安全 | 没有同步机制 | 性能更高 | 1.5 |
Vector | 线程安全 | synchronized关键字来保证线程安全 | 同步造成开销 | 2 |
- ArrayList LinkedList的区别
底层 | 访问 | 插入删除 | 空间 | |
---|---|---|---|---|
ArrayList | 数组 | O(1) | O(n) | |
LinkedList | 双向链表 | O(n) | O(1) | 记录相邻节点 |
- Queue和Dueue
实现类 | ||||
---|---|---|---|---|
Queue | collection | FIFO | LinkedList和PriorityQueue | |
Dueue | collection | 两端删除添加 | ArrayDeque和LinkedList |
- ArrayDeque和LinkedList
底层 | ||||
---|---|---|---|---|
ArrayDeque | 循环数组 | |||
LinkedList | 链表 |
ArrayList
HashMap
- HashMap底层实现:
JDK1.7:
结构:数组+链表
默认大小:16
负载因子:0.75(基于时间成本和空间利用率的折衷)
put:初始化——key计算hashcode——桶空新建一个entry写入——桶是链表遍历key的hashcode,存在相等就覆盖,否则新写一个
扩容:扩容为2倍
get:计算hashcode遍历链表
JDK1.8:
结构:数组+链表+红黑树
put:初始化——key计算hashcode——桶空新建一个entry写入——
桶是链表遍历key的hashcode,存在相等就覆盖,否则新写一个,判断是否需要转化为红黑树(阈值为8)
桶是红黑树按照红黑树写入方式
- 多线程安全问题
扩容时resize方法容易在一个桶上形成环形链表
- NULL
key不允许null(无法进行hashcode计算)、value可以为null
- HahsMap迭代方式:
(1) 迭代器(Iterator)方式遍历;
(2)For Each 方式遍历;
(3)Lambda 表达式遍历(JDK 1.8+);
(4)Streams API 遍历(JDK 1.8+)。
CurrentHashMap
- 底层实现
JDK1.7:
结构:segment+hashentry
put:计算hashcode——找到对应段获取锁——遍历hashentry判断是否相等,相等更新value
扩容:创建原来长度2倍的空间,对每个段进行扩容复制
get:key定位段,定位具体元素。hashentry的value使用voliate修饰,整个过程不用加锁
**JKD1.8 **:
问题:查询遍历效率太低
底层结构:CAS+synchronized+node节点
put:key计算hashcode——定位Node——CAS尝试写入——是否需要转化为红黑树
- NULL
key、value都不允许为null,并发可能导致不一致性
- ConcurrentHashMap和Hashtable
性能 | 键值对为null | 迭代器 | 初始容量 | |
---|---|---|---|---|
ConcurrentHashMap | 分段锁 | 允许为空 | 弱迭代器 | 可以设置 |
Hashtable | synchronized | 不允许 | 强迭代器 | 固定 |
Hashset
- HashSet查重复
当你把对象加入HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode
值作比较,如果没有相符的 hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode
值的对象,这时会调用equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
Java并发
关键字
- volatile关键字
volatile
是 Java 中的一个关键字,用来修饰变量。在多线程并发编程中,volatile
关键字的作用是告诉 JVM:这个变量是不稳定的,每次读取它的值时都需要从内存中重新获取,而不能使用 CPU 缓存中的值,同时在写入变量值时,也要立即刷新到内存中,而不是在某个时刻之后才进行。
使用 volatile
可以保证多线程并发访问该变量时的可见性和有序性,即任何线程对该变量的修改,都能立即反映到其他线程中。但是,volatile
并不能保证操作的原子性(i++这种复合操作),因此不能完全替代 synchronized
。
另外,由于 volatile
强制变量从内存中读写,因此其性能会稍微慢一些。一般来说,只有在对变量的写入操作较少,而对读取操作较多的情况下,才适合使用 volatile
。
- synchronized
synchronized
是 Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
使用方法:
- 修饰实例方法:给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
- 修饰静态方法:给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁
- 修饰代码块: 表示进入同步代码库前要获得 给定对象的锁、表示进入同步代码前要获得 给定 Class 的锁
- synchronized原理
Java每个对象都以一个monitor,synchronized通过获取monitorenter获取对象锁,通过monitorexit释放锁
- synchronized锁升级过程
偏向锁:只有一个线程争抢锁资源的时候.将线程拥有者标识为当前线程.
轻量级锁(自旋锁):一个或多个线程通过CAS去争抢锁,如果抢不到则一直自旋.
重量级锁:多个线程争抢锁,向内核申请锁资源,将未争抢成功的锁放到队列中直接阻塞.
- ReentranLock和synchronized的区别
ReentranLock | synchronized | |
---|---|---|
可重入锁 | 支持 | 支持 |
底层 | API | JVM |
功能 | 等待可中断、公平锁、条件变量 | |
锁粒度 | 代码块 | 方法 |
性能 | 高并发场景更好 | 低并发场景更好 |
公平锁 | 公平非公平锁都可以 | 非公平锁 |
- Java的锁
synchronized、ReentrantLock、读写锁、乐观锁
线程进程
进程:进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程:线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。
协程:由程序显示控制调度切换,轻量级
- 从JVM角度说明线程和进程
线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
- 开启线程的方式
thread类创建线程调用start开启、runnable接口、callable接口
- 线程生命周期
NEW: 初始状态,线程被创建出来但没有被调用 start()
。
RUNNABLE: 运行状态,线程被调用了 start()
等待运行的状态。
BLOCKED :阻塞状态,需要等待锁释放。
WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
- 线程上下文切换
线程切换条件:
主动让出 CPU,比如调用了
sleep()
,wait()
等。时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
- 产生死锁的条件
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
- sleep()和wait()方法对比
sleep() | wait() | |
---|---|---|
锁 | 没释放 | 释放 |
使用场景 | 线程交互通信 | 暂停执行 |
是否自动苏醒 | 自动苏醒 | notify()或notifyAll()方法来唤醒 |
来源 | thread类本地方法 | object类方法 |
- 可以直接调用thread的run()吗?
调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话会把 run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
- 乐观锁和悲观锁
悲观锁:悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。悲观锁通常多用于写比较多的情况下(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。
乐观锁:乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了。乐观锁通常多于写比较少的情况下(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量。
- 乐观锁实现方式
版本号机制:一般是在数据表中加上一个数据版本号 version
字段,表示数据被修改的次数。当数据被修改时,version
值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 version
值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version
值相等时才更新,否则重试更新操作,直到更新成功。
CAS算法:涉及到三个操作数:V :要更新的变量值(Var) E :预期值(Expected) N :拟写入的新值(New)。当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。
- 乐观锁的问题
ABA问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,可能在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 “ABA”问题。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。
循环时间开销大:CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。使用pause指令
只能保证一个共享变量的原子操作
- 线程通讯方式
共享变量、等待通知机制(wait/notify)、阻塞队列、信号量
ThreadLocal
ThreadLocal
是 Java 中的一个类,它提供了线程本地变量的支持。每个线程都拥有自己的 ThreadLocal
变量副本,线程之间的操作互不干扰。
ThreadLocal
的作用是在多线程环境中,为每个线程提供一个独立的变量副本,这样每个线程都可以独立地改变自己的变量副本,而不会影响其他线程。这种方式通常被用来实现线程安全的对象,例如日期格式化器和数据库连接等。
具体来说,ThreadLocal
可以用来存储线程相关的数据,例如用户信息、Session、数据库连接等,这些数据在多线程环境下需要被共享,但是又不能被不同线程之间共享,否则会造成数据混乱。
另外,ThreadLocal
的实现原理是在每个线程内部维护一个 ThreadLocalMap
类型的映射表,用于存储变量副本。当调用 ThreadLocal
的 get()
方法时,会根据当前线程获取对应的变量副本;当调用 set()
方法时,会将变量副本存储到当前线程的映射表中;当不再需要该变量时,需要调用 ThreadLocal
的 remove()
方法来删除该变量副本,避免内存泄漏。
Thread类中包括threadLocals
和 一个 inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量。最终的变量是放在了当前线程的 ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解为只是ThreadLocalMap
的封装,传递了变量值。
每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
- 内存泄漏
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后 最好手动调用remove()
方法
线程池
- 优点
降低资源消耗、提高响应速度、提高线程可管理性
- 创建方法
通过ThreadPoolExecutor
构造函数来创建、通过 Executor
框架的工具类 Executors
来创建
- 线程池参数
corePoolSize
: 任务队列未达到队列容量时,最大可以同时运行的线程数量。
maximumPoolSize
: 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
keepAliveTime
:线程池中的线程数量大于 corePoolSize
的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime
才会被回收销毁;
unit:时间单位;
workQueue:阻塞队列
threadFactory:线程工厂
handler:拒绝策略
- 线程池常用阻塞队列
ArrayBlockingQueue | ||||
LinkedBlockingQueue | ||||
有界队列、
无界队列:队列容量无限大
同步队列:每次有新任务先判断有没有空闲线程,没有直接就创建一个线程
延迟阻塞队列:
- 线程池处理任务流程
- 拒绝策略
ThreadPoolExecutor.AbortPolicy
: 抛出异常来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
: 调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。
JDK8新特性
函数式接口:代码更简洁、表达能力更强、更容易并发编程(方法是无状态的)
lambda表达式:替换匿名内部类
stream流:链式编程
optinal:防止NPE问题
Date-Time APi:增强日期表示方法
JVM
程序计数器(线程私有)
记录当前正在执行的指令地址
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了
虚拟机栈(线程私有)
局部变量表:存放了编译期可知的各种数据类型
操作数栈:存放方法执行过程中产生的中间计算结果
动态链接:主要服务一个方法需要调用其他方法的场景,符号引用转换为调用方法的直接引用
方法返回地址:return、抛出异常
可能报错:
**StackOverFlowError
**若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError
错误。
OutOfMemoryError
如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常。
本地方法栈
MySQL
- 数据库引擎
- sql语句执行过程
语法解析——权限验证——查询缓存——查询优化器——执行计划生成——数据检索——数据排序分组——数据返回——事务处理
- 存储引擎架构
MySQL的存储引擎架构是一个插件式的架构。
包括组件:
- MyISAM和InnoDB
事务 | 锁 | 数据缓存 | 外键约束 | 索引 | 崩溃恢复 | |
---|---|---|---|---|---|---|
MyISAM | 不支持 | 表级锁 | 键缓存 | 不支持 | 全文索引 | |
InnoDB | 支持 | 行级锁 | 缓冲池 | 支持 | 普通索引 | 自动崩溃恢复和恢复日志功能 |
- char和varchar的区别
CHAR
和VARCHAR
的区别在于,CHAR
是固定长度的,如果存储的内容不足长度,则会补齐空格;VARCHAR
是可变长度的,根据存储的内容动态分配空间,不会补齐空格。
varchar有两字节记录长度,扩容时如果空间还够直接添加,否则要开辟新空间然后复制删除。
- 建表考虑的问题
数据库设计、数据类型、主键设计、索引设计、字段属性、数据库引擎、数据库安全、数据库备份与恢复
底层结构
- B+树缺点
占用内存较大、插入删除操作复杂、更新代价大、不是和随机访问
事务
- 事务
数据库事务(Transaction)是指一组数据库操作,这组操作被视为一个不可分割的工作单元,只有在所有操作都完成时,才会被提交到数据库中,否则会被回滚(Rollback)并撤销所有操作。
- 事务特点
原子性、一致性、隔离性、持久性
- 并发事务带来的问题
脏读(Dirty Read):指一个事务读取了另一个事务未提交的数据。
不可重复读(Non-Repeatable Read):指一个事务多次读取同一条记录,但在读取的过程中,这条记录被另一个事务修改了,导致读取的结果不一致。
幻读(Phantom Read):指一个事务执行了一次范围查询,但在查询的过程中,另一个事务插入了一条新的数据,导致查询的结果不一致。
- 不可重复读和幻读的区别
不可重复读是指在一个事务中,多次读取同一条记录,但在此期间另一个事务更新了该记录,导致两次读取的结果不一致。幻读是指在一个事务中,多次查询同一范围的记录,但在此期间另一个事务插入了一条符合查询条件的记录,导致两次查询的结果不一致。
不可重复读的原因是在一个事务中多次读取同一条记录时,另一个事务可能会更新该记录。幻读的原因是在一个事务中多次查询相同的条件时,另一个事务可能会插入一条符合查询条件的记录。
- 事务隔离级别
读未提交、读已提交、可重复读(mysql默认级别)、串行化
脏读 | 不可重复读 | 幻读 | 加锁读 | |
---|---|---|---|---|
读未提交 | 是 | 是 | 是 | 否 |
读已提交 | 否 | 是 | 是 | 否 |
可重复读 | 否 | 否 | 是 | 否 |
串行化 | 否 | 否 | 否 | 是 |
- MVCC
作用:MVCC采用版本号或时间戳来标识数据版本,每个事务在读取数据时只读取自己开始时间之前的数据版本,而对数据的修改会创建新的数据版本。这样,即使多个事务对同一数据进行读写操作,也不会相互阻塞。而且,当一个事务要修改某个数据行时,如果该数据行已被其他事务修改,则会进行冲突检测,从而避免数据的丢失或者不一致。
原理:
- 每个数据行都有版本号或时间戳
- 事务读取数据时,只会读自己开始之前可见的版本,如果该版本数据被修改就回滚事务,直到读到可见版本
- 事务修改数据时,创建新数据版本,与当前事务关联,如果已经被修改就需要冲突检测
- 某个数据版本被所有事务都看不见,则清理掉该版本
- InnoDB 存储引擎的行锁
记录锁(Record Lock)是 InnoDB 存储引擎最基本的锁类型,用于锁定某一行数据。
间隙锁(Gap Lock)用于锁定一个范围,而不是单独的某一行数据。
临建锁(Next-Key Lock)是记录锁和间隙锁的结合,既可以锁定某一行数据,也可以锁定一个范围。临建锁用于解决幻读问题,同时也可以避免覆盖读和不可重复读的产生。
索引
- 索引
索引是一种特殊的数据结构,用于在数据库中快速定位数据。
作用:索引可以加速数据的检索和查询,同时也可以提高数据库的性能和并发能力。
缺点:用额外的存储空间,并且在插入、修改和删除数据时会增加额外的操作开销。
- 索引分类
功能逻辑:普通索引、唯一索引、主键索引、全文索引
实现方式:聚簇索引、非聚簇索引
作用字段:单列索引、联合索引
普通索引 | 普通字段添加的索引,没有特殊要求 | |
唯一索引 | 字段的值是唯一的,可以有多个唯一性索引 | unique |
主键索引 | 主键添加的索引,一张表只有一个 | |
全文索引 | 用于处理文本数据。对文本进行分词,然后将分词结果存储在倒排索引中 | fulltext |
聚簇索引 | 将数据存储在索引中,每个表只能有一个聚簇索引 | |
非聚簇索引 | 将索引和数据分开存储,索引中只包含索引列的值和指向数据行的指针。 | |
单列索引 | 一个字段上的索引 | |
联合索引 | 多个字段组合创建的索引 |
- 建立索引考虑问题
索引类型、索引列、索引长度、索引复合、索引选择性、索引覆盖、索引使用、索引删除、索引优化
适合做索引的字段:主键、经常用于过滤排序分组的字段
不适合:经常更新、选择性较差
- 主键索引和二级索引
主键索引是一种特殊的索引类型,它是用于唯一标识每一行数据的索引。在 MySQL 中,每一张表都必须有一个主键索引。
B+树中,主键索引放一整行信息,非主键索引只放主键和索引
二级索引则是指除了主键索引以外的其他索引,用于提高查询效率。二级索引的查找方式与主键索引类似,但是由于每一行数据都会有一个对应的主键值,因此二级索引需要先通过主键值找到行数据,再进行二次查找。先查二级索引再查主键索引。
区别:主键索引是唯一的,而二级索引可以重复。
主键索引是必须的,而二级索引是可选的。
主键索引的大小一定小于二级索引。
- 覆盖索引
覆盖索引是指在查询语句中,所有需要返回的字段都可以从索引中获取,而不需要再到数据表中进行查询的一种情况。
使用覆盖索引的优点是可以减少查询语句的 IO 操作次数,从而提高查询效率。同时,由于不需要访问数据表,所以在查询过程中也不会发生锁的冲突,从而避免了死锁等问题的发生。
要求:
查询语句中需要返回的所有字段都被包含在索引中。
索引必须是一个覆盖索引,即所有需要返回的字段的值都在索引中。
- 最左前缀匹配规则
最左前缀匹配规则是用于优化索引查询的一种技术。该规则指出,如果在一个复合索引(即包含多个列的索引)中,查询条件只涉及到索引的最左侧列或最左侧列的连续子集,则可以利用该索引进行快速匹配。
- 索引失效
函数操作、使用了 NOT、!=、<>、OR 操作符、索引列排序分组、模糊查询、数据类型不匹配、索引列过长、数据量小、统计信息不准确
日志
- 常见日志类型
错误日志(Error log):记录MySQL服务器在启动、运行过程中出现的错误信息和警告信息;
查询日志(Query log):记录MySQL服务器接收到的所有查询请求,可用于性能调优和问题排查;
慢查询日志(Slow query log):记录执行时间超过指定阈值(默认为10秒)的SQL语句,可以帮助识别性能瓶颈和优化查询语句;
二进制日志(Binary log):记录所有修改数据库数据的操作,可用于数据备份和恢复、数据同步等;
事务日志(Transaction log):记录MySQL执行的所有事务操作,用于数据恢复和恢复到指定的时间点;
慢查询摘要日志(Slow query summary log):记录慢查询的信息摘要,可用于分析查询性能和优化
redo log
- 刷盘策略
log_buff(内存) —> os cache —>OS刷新 (flush)—> disk
策略一:每隔一秒把redo log buffer刷到磁盘文件系统中,最坏丢一秒
策略二:每次事务提交都刷盘,最安全,消耗IO
策略三:事务提交先刷到缓存,每隔一秒刷到磁盘
- redo log实现事务可持久性
当执行一条事务中的SQL语句时,会首先将修改的数据记录到内存中的Redo log缓冲区中,然后再将修改同步到磁盘上的数据文件中。Redo log采用循环写入的方式,当缓冲区写满后,就会将缓冲区中的数据写入到磁盘上的Redo log文件中,然后再将缓冲区清空,以便接收下一个事务的修改。
当MySQL宕机或崩溃时,可以通过Redo log中记录的事务日志信息来进行数据恢复。在MySQL重新启动时,会首先读取Redo log文件中的内容,并将其中尚未同步到数据文件的修改重新应用到数据文件中,从而恢复数据的一致性。
undo log
- undo log实现事务的原子性
Undo log是记录了事务在修改数据时的原始值的一种日志,用于在回滚事务时恢复数据。当事务进行修改操作时,MySQL会将修改前的数据记录在Undo log中,然后将新数据写入到数据页中。如果事务需要回滚,MySQL会读取Undo log中的原始值,然后将其写入到对应的数据页中,从而撤销事务的修改操作。
需要注意的是,Undo log只记录了事务在修改数据时的原始值,并不记录事务执行过程中的所有操作。因此,在使用Undo log进行回滚时,可能会有一些操作无法回滚。此外,Undo log还需要占用一定的存储空间,因此需要定期进行清理。
- binlog
记录对数据库执行的更改操作,即使表结构变更和表数据修改没有使数据库修改,也会被记录。
分类:
statement模式:每一条修改数据的sql都会记录
row模式:每一行具体变更事件
mixed模式:statement和row的混合模式默认statement
主要应用场景:主从复制
刷盘时机:在 MySQL 中,事务提交时会将事务的所有更改写入 binlog 文件中,然后将其刷盘到磁盘中。
重新生成:mysql停止或重启;flush logs;超过max_binlog_size
SQL优化
- sql优化
避免使用select *
使用union all代替union
小表驱动大表
批量操作不要写循环
多用limit
增量查询:每次只同步一批数据,这一批数据只有100条记录。每次同步完成之后,保存这100条数据中最大的id和时间,给同步下一批数据的时候用。
连接查询代替子查询
参数是子查询时,使用 EXISTS 代替 IN:可以用到索引、in需要扫描整张表
join的表不要过多
索引优化:explain
能用where不用having
避免使用否定形式
- 慢查询可能原因
数据量过大、索引问题、锁问题、IO吞吐量小
- 慢查询优化
使用选择性高的字段并前置(能过滤较多记录)
尽量使用覆盖索引
索引失效:组合索引左匹配原则、发生隐式转换、组合索引,in + order by in会阻断排序用索引、范围查询会阻断组合索引,索引涉及到范围查询的索引字段要放在组合索引的最后面、前模糊匹配导致索引失效、or查询,查询条件部分有索引,部分无索引,导致索引失效、查询条件使用了函数运算、四则运算等、使用了!=、not in、选择性过低、asc和desc混用
- explain参数
id:执行子查询顺序
type:访问类型(system(只有一行数据) > const > eq_ref > ref > range > index > ALL)
可能索引、真实索引、索引长度、表名、查询行数
Redis
缓存
- 本地缓存
Ehcache 、Guava Cache 、Spring Cache、Caffeine(常用)
缺点:不利于分布式架构、容量受部署的机器限制
- 分布式缓存
缺点:复杂性提高,开发成本增加
方法:Memcached、Redis
- 多级缓存
本地缓存(速度快)+分布式缓存
使用场景:数据稳定、访问量大
Redis基础
- Redis的功能
键值存储,内存缓存,分布式锁,限流,消息队列(5.0新增数据结构stream,可以做轻量级)
- 做缓存
读写性能高、高并发处理、多种数据类型、数据持久化、高可用性
- 分布式缓存
支持的数据结构 | 数据持久化 | 性能、可扩展性 | ||
---|---|---|---|---|
memcached | 只支持键值对 | 不支持 | 多线程 | |
Redis | 字符串、哈希表、列表、集合、有序集合 | RDB\AOF | 单线程 |
- 支持的数据类型
应用 | 底层 | 优点 | |
---|---|---|---|
string | 常规数据、计数 | SDS(自己创建) | 可以保存二进制,获取字符串长度O(1),不会造成缓冲区溢出 |
list | 信息流、消息队列 | 双向链表 | |
set | 不能重复的数据 | 压缩列表+整数集合 | |
hash | 用户信息 | 数组+链表 | |
zset | 排序 | 跳跃表+压缩列表 | |
bitmap | 保存状态(0\1) | 连续二进制 | |
hyperloglogs | 大数据统计 | ||
geospatial | 附近的人 | sort set |
跳表:随机决定放在哪一层保持平衡性
每个节点要指向当前层下一个节点和下一层的节点
- 为什么速度快?
基于内存,内存访问速度远快于磁盘;
单线程+IO多路复用
数据结构优化
Redis应用
- Redis做统计网站的uv
使用HyperLogLog,HyperLogLog是一种基数(cardinality)估算算法,可以用来统计大数据集合的不同元素个数,而不需要将所有元素都存储在内存中。它的优点是占用内存小,计算速度快,适合处理大规模的数据集。
可以通过将每个用户的IP地址作为HyperLogLog的元素进行去重计数,从而得到网站的UV。有一定误差。
- Redis做排行榜
使用有序集合(sorted set)数据结构来存储每个用户的分数和排名信息。
有序集合是一种有序的键值对集合,其中每个键关联着一个分数值,可以根据分数值对集合中的键进行排序。在排行榜中,可以将每个用户的得分作为有序集合中的分数,将用户ID作为有序集合中的键,然后根据分数值对有序集合进行排序,得到用户的排名信息。
- Redis实现分布式锁
① setnx抢锁+expire设置过期时间(如果在过期之前发生异常,锁无法释放)
② lua脚本,保证抢锁释放锁的原子性(没执行完业务就释放锁,锁被其他线程误删)
③ 设置一个线程唯一值
最终:多机实现的分布式锁Redlock+Redisson
获取当前时间,向master节点请求加锁设置超时时间,超过半数节点加锁成功就加锁成功,否则获取锁失败,解锁
Redis线程模式
- 单线程模式
优点:简短高效、确保数据一致、支持原子性操作
缺点:单核限制、io限制、长时间阻塞
改进:IO多路复用、异步处理机制、高效数据结构、协议简单
- 多线程模式
提高网络IO读写性能
- 为什么不用多线程?
单线程容易维护、Redis的性能瓶颈是内存和网络不在Cpu上、多线程容易带来死锁上下文切换问题影响性能
- 为什么又用多线程?
提高网络IO性能只应用在了网络数据的读写上
Redis内存管理
- 过期数据处理
设置缓存数据过期时间、设置过期字典。
删除策略:
惰性删除是Redis默认的过期数据删除策略。当一个键值对过期时,并不会立即从内存中删除,而是在下一次访问该键值对时检查其是否过期,如果已过期,则删除。这种删除策略的好处是可以减少内存的碎片化,因为过期键值对的内存空间可以被重复利用。但是,它的缺点是可能会导致内存中存在大量的过期键值对,占用了大量的内存空间,并且检查过期键值对会对性能产生一定的影响。
定期删除是Redis另一种常用的过期数据删除策略。在这种策略下,Redis会定期(每秒钟)随机选择一些过期键值对进行删除。这种策略的好处是可以避免内存中存在大量的过期键值对,从而提高了内存的利用率,并且对性能的影响比惰性删除要小。但是,它的缺点是会造成一定的内存碎片化,因为删除操作可能会产生一些不连续的空闲内存块。
- 内存淘汰机制
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错;
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰;
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰;
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰;
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
volatile-lfu:最近不经常使用的原则,但只对设置了过期时间的键进行淘汰
- 内存碎片
内存碎片是指空闲内存块之间存在无法利用的小块空闲内存的现象。
原因:频繁的内存分配和释放。内存对齐(整块分配)。大量缓存数据的更新。
避免:减少内存分配和释放的次数;合理设置内存对齐参数;定期清理内存碎片
Redis持久化机制
- RDB持久化
RDB持久化是将Redis的数据集以快照的方式写入磁盘。当Redis需要保存数据时,它会将当前数据集写入一个RDB文件中。RDB文件包含了Redis在某个时间点上的数据快照,可以通过加载RDB文件来恢复数据。RDB持久化适用于数据集较大、对数据完整性要求不高的场景。
- AOF持久化
AOF持久化是将Redis的操作日志以追加的方式写入磁盘。每个Redis命令都会被追加到AOF文件的末尾。当Redis需要恢复数据时,它会按照AOF文件的顺序重新执行所有命令。AOF持久化适用于对数据完整性要求高的场景,因为它可以确保Redis的操作日志与实际执行的命令是一致的。
- AOF重写机制
后台重写进程,生成新的AOF文件,写入新的AOF文件,重写期间的命令也会追加到AOF文件,先追加到AOF缓冲区再进行持久化操作
Redis事务
- Redis事务的缺点
原子性问题:Redis事务并不是严格的原子性操作,即使在一个事务中发生错误,已经执行的命令也不会回滚。
隔离性问题:Redis事务也不是严格的隔离性操作。在多个客户端同时执行事务时,如果它们修改了相同的键,可能会发生竞态条件(race condition),导致最终结果出现错误。
性能问题:Redis事务在执行期间会将所有命令放到一个队列中,然后一次性执行。
- 解决方法
lua脚本可以实现事务和原子性操作,可以保证一些复杂的操作的数据一致性和原子性。
通过watch机制解决原子性问题、使用Redis的乐观锁机制解决隔离性问题
问题
- 缓存穿透、缓存击穿
缓存穿透通常指在缓存和存储层中都不存在数据,导致每次请求都要查询存储层。如果数据库中存在数据,但缓存中不存在,这种情况通常被称为缓存击穿。
解决方法:
缓存空对象:将缓存中不存在的数据缓存为空对象,避免缓存穿透。
布隆过滤器:在缓存层上使用布隆过滤器,过滤掉一些明显不存在的数据请求,避免请求到存储层。
数据预热:在应用启动时,将热门数据预先加载到缓存中,避免请求到存储层。
设置缓存时间:对于不太可能存在的数据,可以设置一个较短的缓存时间,从而降低缓存穿透的概率。
布隆过滤器:布隆过滤器是一种概率型数据结构,用于判断一个元素是否可能存在于一个集合中。它可以快速判断一个元素是否存在于一个大型集合中,而无需对整个集合进行遍历和比较。
布隆过滤器基于位数组和多个哈希函数构成。当一个元素被加入集合时,将会被多个哈希函数映射成多个位数组上的位置,并将这些位置标记为1。当判断一个元素是否存在于集合时,将该元素经过哈希函数映射到多个位数组上,如果所有位置的值都为1,则认为该元素可能存在于集合中;如果有任何一个位置的值为0,则认为该元素一定不存在于集合中。
由于布隆过滤器存在一定的误判率,因此它适合于那些可以容忍一定误判率的场景,如缓存穿透的解决方案等。
- 缓存雪崩
缓存雪崩是指缓存中大量的数据在同一时间过期失效,导致请求直接访问存储层,从而导致存储层的瞬时压力骤增,甚至会导致存储层宕机的情况。
解决方法:为缓存数据设置不同的过期时间,防止同时失效;在缓存数据过期前,异步更新缓存,避免大量缓存同时失效
- 读写策略
读 | 写 | 问题 | |
---|---|---|---|
Cache-Aside(旁路缓存) | 先写数据库中,再写入缓存 | 第一次请求数据不在cache;写操作频繁影响缓存命中率 | |
Write-Through(透写缓存) | 先写缓存,再写数据库 | 第一次请求数据不在cache | |
Write Behind Pattern(异步缓存写入) | 先写缓存标记脏数据,等待进程写入数据库 |
Redis集群
- 如何保证高可用
主从复制:master主节点负责处理写请求,slave从节点负责处理读请求。master宕机,选择一台slave作为master将主节点数据复制到从节点,保障高可用,实现读写分离提高并发量
- sentinel哨兵
sentinel可以监控Redis节点运行状态并自动完成故障转移
分布式系统:多个sentinel节点投票决定master节点是否不可用,sentienl是高可用
运行方式:sentinel节点每秒钟向master和slave发送ping,超时未回复则认为线下。slave下线不关心,如果master超时回复,则多个sentinel进行核实。一旦下线,则会选择一个slave作为新的master,选择slave的规则:
slave优先级:手动设置
复制进度:选择最相似的
运行id:选择较小的
- Redis Cluster
使用原因:缓存数据量大、并发量高
定义:部署多台Redis主节点,同时提供读写服务,可以为每一个master再配置slave
优点:横向扩充动态扩容缩容(直接添加或者删除节点)
具备主从复制、故障转移(内置sentinel机制)
基本要求:3个master、个slave
分片:哈希槽分区每个键值对属于一个哈希槽,共有16384个哈希槽,自动分配哈希槽到各个节点
- Redis Cluster如何实现各节点数据一致性
Redis Cluster使用Gossip协议来保持集群节点之间的通信和状态同步。每个节点定期地向其他节点发送信息,包括自己的状态、所知道的其他节点信息等。当节点接收到其他节点的信息时,它会将这些信息与自己的信息进行比对,以确保数据的一致性。
- 主从复制
首次同步是完全同步,通过RDB文件
之后是部分同步,记录复制位置并记录从redis的id
心跳加测:主从关系建立后,从服务器每一秒向主服务器发送一次状态
- 虚拟槽分区
Redis Cluster中采用了虚拟槽分区的方式来实现数据的分片和负载均衡。具体来说,Redis Cluster将整个数据集划分为16384个槽,每个节点可以负责多个槽。当一个节点加入或离开集群时,它会和其他节点交换槽信息,确保每个节点都知道哪些槽分配给了哪个节点。当客户端对集群进行读写操作时,Redis会根据Key所属的槽位,将请求转发到负责该槽位的节点上进行处理。这样,就实现了数据的分片和负载均衡。同时,为了确保数据的高可用性,每个槽位都会有多个副本(通常是3个),即使某个节点宕机,数据仍然可以通过其它副本进行读写。
- redis主从复制
第一次声明slaveof关系,执行完整的同步操作
阻塞
O(n)复杂度的指令、创建RDB快照、AOF日志记录/刷盘/重写阻塞、清空数据库、集群扩容、SWAP、CPU竞争
性能优化
- 命令执行步骤
发送命令、命令排队、命令执行、返回结果
优化方式:批量操作、lua脚本
MyBatis
- #{} 和 ${} 的区别是什么?
#{} 会将传入的参数都转义为一个占位符“?”并且使用 JDBC 的 PreparedStatement 对占位符进行赋值,从而避免 SQL 注入的风险。
${} 则是直接进行字符串替换,不会对传入的参数进行预编译处理,因此存在 SQL 注入的风险。
只能用${}场景:动态表名、列名,自定义sql片段,动态排序字段
- xml 映射文件中,除了常见的 select、insert、update、delete 标签之外,还有哪些标签?
<resultMap>
、 <parameterMap>
、 <sql>
、 <include>
、 <selectKey>
,加上动态 sql 的 9 个标签, trim|where|set|foreach|if|choose|when|otherwise|bind
等,其中 <sql>
为 sql 片段标签,通过 <include>
标签引入 sql 片段, <selectKey>
为不支持自增的主键生成策略标签。
- Dao接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?
Dao 接口里的方法可以重载,但是 Mybatis 的 xml 里面的 ID 不允许重复。
- 分页功能
(1) MyBatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页;
(2) 可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,
(3) 也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用 MyBatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。
- 动态SQL
动态 SQL 是指在 SQL 语句中根据条件动态拼接 SQL 语句的技术,通常用于构建复杂的查询语句。种类:<if>
<where>
<choose>
<foreach>``<bind/>
- 缓存
一级缓存:SqlSession级别
二级缓存:SqlSessionFactory级别
增删改会失效,先查二级再查一级
- 分页插件
RowBounds、PageHelper(推荐)
原理:获取分页参数、拦截查询Sql语句,根据参数添加查询条件LIMIT、封装对象并返回
MyBatis-plus
- 注解
作用 | 参数 | |
---|---|---|
@TableName | 实体类标识对应表 | 表名 |
@TableID | 属性标识主键 | 属性 |
@TableField | 属性名标识字段名 | 字段名 |
@TableLogic | 逻辑删除 |
- 主键生成策略
自动增长、uuid(排序困难)、Redis生成(固定步长增加)、雪花算法
雪花算法:时间戳+数据中心ID+机器ID+序列号
- 插件
分页插件
乐观锁:基于版本号机制
Spring
IOC
IoC(Inversion of Control:控制反转) 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。
- 控制 :指的是对象创建(实例化、管理)的权力
- 反转 :控制权交给外部环境(Spring 框架、IoC 容器)
好处:对象之间的耦合度或者说依赖程度降低;资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例
- IOC和DI区别
IoC(Inverse of Control:控制反转)是一种设计思想 或者说是某种模式。这个设计思想就是 将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。
Bean
- 注入方式
构造方法注入、setter注入、
概念:Bean 代指的就是那些被 IoC 容器所管理的对象
注解:
@Component
:通用的注解,可标注任意类为 Spring
组件。如果一个 Bean 不知道属于哪个层,可以使用@Component
注解标注。
@Repository
: 对应持久层即 Dao 层,主要用于数据库相关操作。
@Service
: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
@Controller
: 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service
层返回数据给前端页面
- @Component 和 @Bean 的区别是什么?
@Component
注解作用于类,而@Bean
注解作用于方法@Component
通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中,@Bean
注解通常是我们在标有该注解的方法中定义产生这个 bean@Bean
注解比@Component
注解的自定义性更强,而且很多地方我们只能通过@Bean
注解来注册 bean
- 注入Bean
@Autowired | @Resource | @Inject | |
---|---|---|---|
来源 | spring 内置 | JDK提供 | |
注入方式 | 默认byType | 默认byName | |
多个实现类 | 通过 @Qualifier 注解来显式指定名称 |
通过 name 属性来显式指定名称 |
|
支持构造方法注入,能注入final属性 |
- 作用域
singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。优点:避免资源重复创建、避免了冗余初始化、简化依赖管理、状态保持一致。缺点:难以扩展、破坏封装性、并发访问控制
prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean()
两次,得到的是不同的 Bean 实例。
request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
application/global-session (仅 Web 应用可用): 每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
- 生命周期
实例化——bean属性复制——初始化bean——使用bean——销毁bean
- beanfactory和factorybean的区别
BeanFactory是Spring IoC容器的核心接口,用于管理Bean对象的加载和配置,而FactoryBean是一个特殊的Bean,它本身是一个工厂对象,用于创建和管理其他Bean对象。BeanFactory提供了基本的Bean管理功能,而FactoryBean则扩展了BeanFactory,提供了更高级别的自定义Bean创建和管理能力。
AOP
作用:在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。
切面编程:
切 :指的是横切逻辑,原有业务逻辑代码不动,只能操作横切逻辑代码,所以面向横切逻辑
面 :横切逻辑代码往往要影响的是很多个方法,每个方法如同一个点,多个点构成一个面。这里有一个面的概念
应用场景:
日志记录:通过AOP技术,可以在程序执行前、执行后或者抛出异常时记录日志,方便程序员在出现问题时快速定位问题。
安全控制:通过AOP技术,可以在程序执行前进行身份认证和权限控制,保证程序的安全性。
性能监控:通过AOP技术,可以在程序执行前、执行后或者抛出异常时统计程序的执行时间和消耗资源,以便优化程序性能。
缓存控制:通过AOP技术,可以在程序执行前、执行后或者抛出异常时对数据进行缓存操作,提高程序性能。
事务管理:通过AOP技术,可以在程序执行前开启事务,在程序执行后提交或者回滚事务,保证数据的一致性和完整性。
异常处理:通过AOP技术,可以在程序抛出异常时进行处理,如记录日志、提示用户等,提高程序的可靠性和稳定性。
事务
- 支持什么事务管理
编程式事务(需要手动编写)、声明式事务(基于注解和基于xml)
- 事务管理接口
PlatformTransactionManager:事务管理接口
Spring 并不直接管理事务,而是提供了多种事务管理器。
TransactionDefinition:事务属性
事务管理器接口 PlatformTransactionManager
通过 getTransaction(TransactionDefinition definition)
方法来得到一个事务,这个方法里面的参数是 TransactionDefinition
类 ,这个类就定义了一些基本的事务属性。
TransactionStatus:事务状态
TransactionStatus
接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。
- @Transactional为什么能实现事务?
声明式事务,AOP和事务管理器的结合。假设类A或者类A的一个方法被标识@transactional
注解,spring容器就会给他生成一个代理类,代理对象会在前后添加事务管理
- spring事务失效
发生异常、没有被spring管理、数据源没有配置事务管理器、访问权限问题
注解
SpringMVC
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
- 核心组件
DispatcherServlet
:核心的中央处理器,负责接收请求、分发,并给予客户端响应。
HandlerMapping
:处理器映射器,根据 uri 去匹配查找能处理的 Handler
,并会将请求涉及到的拦截器和 Handler
一起封装。
HandlerAdapter
:处理器适配器,根据 HandlerMapping
找到的 Handler
,适配执行对应的 Handler
;
Handler
:请求处理器,处理实际请求的处理器。
ViewResolver
:视图解析器,根据 Handler
返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet
响应客户
- 执行流程
客户端(浏览器)发送请求,
DispatcherServlet
拦截请求。DispatcherServlet
根据请求信息调用HandlerMapping
。HandlerMapping
根据 uri 去匹配查找能处理的Handler
(也就是我们平常说的Controller
控制器) ,并会将请求涉及到的拦截器和Handler
一起封装。DispatcherServlet
调用HandlerAdapter
适配执行Handler
。Handler
完成对用户请求的处理后,会返回一个ModelAndView
对象给DispatcherServlet
,ModelAndView
顾名思义,包含了数据模型以及相应的视图的信息。Model
是返回的数据对象,View
是个逻辑上的View
。ViewResolver
会根据逻辑View
查找实际的View
。DispaterServlet
把返回的Model
传给View
(视图渲染)。把
View
返回给请求者(浏览器)
SpringBoot
- SpringMVC和SpringBoot之间的区别
springmvc | springboot | |
---|---|---|
项目结构 | 手动配置 | 约定优先于配置 |
配置方式 | xml或java代码 | 自动配置 |
依赖管理 | 手动引入 | 集成第三方依赖 |
对外提供服务 | 部署在web容器 | 打包jar、war |
- 对象之间的耦合度或者说依赖程度降低;
- 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例
注解
- SpringBootApplication
@SpringBootConfiguration
:表示该类是 Spring Boot 应用程序的配置类。@EnableAutoConfiguration
:表示启用自动配置机制,根据类路径中的依赖项和其他 Bean 的存在来猜测和配置 Bean。@ComponentScan
:表示扫描当前包及其子包以查找其他组件、配置和服务。
- restful web常用注解
spring bean相关:
@Autowired
自动导入对象到类中,被注入进的类同样要被 Spring 容器管理@Component
:通用的注解,可标注任意类为Spring
组件。@Repository
: 对应持久层即 Dao 层,主要用于数据库相关操作。@Controller
: 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。@Service
: 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。@RestController
注解是@Controller
和@ResponseBody
的合集,表示这是个控制器 bean,并且是将函数的返回值直接填入 HTTP 响应体中,是 REST 风格的控制器。@Configuration
一般用来声明配置类,可以使用@Component
注解替代,不过使用@Configuration
注解声明配置类更加语义化。
处理http请求类型:
GET POST PUT DELETE
- 前后端传值
@PathVariable
用于获取路径参数"/users/{id}"
,@RequestParam
用于获取查询参数/users?page=1&size=10
。@RequestBody
用于读取 Request 请求(可能是 POST,PUT,DELETE,GET 请求)的 body 部分并且Content-Type 为 application/json 格式的数据,接收到数据之后会自动将数据绑定到 Java 对象上去。系统会使用HttpMessageConverter
或者自定义的HttpMessageConverter
将请求的 body 中的 json 字符串转换为 java 对象。
配置信息
- 读取配置信息
@Value("${property}")
@ConfigurationProperties
@PropertySource
依赖
- optional标签
可选依赖,B依赖A(设置了optional标签),C依赖B,但是C不依赖A。
自动装配流程
@EnableAutoConfiguration
注解通过Spring提供的@Import
注解导入了AutoConfigurationImportSelector
类(@Import注解可以导入配置类或者Bean到当前类中)。 AutoConfigurationImportSelector
类中getCandidateConfigurations
方法会将所有自动配置类的信息以List的形式返回。这些配置信息会被Spring容器作bean来管理。
启动流程
//MainApplication.java
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
构建了一个SpringApplication
对象,然后再调用其run
方法来启动SpringBoot项目
步骤:
加载配置文件——准备环境变量——打印标志——创建容器——初始化容器——刷新容器——返回容器对象
- 从
spring.factories
配置文件中加载EventPublishingRunListener
对象,该对象拥有SimpleApplicationEventMulticaster
属性,即在SpringBoot启动过程的不同阶段用来发射内置的生命周期事件; - 准备环境变量,包括系统变量,环境变量,命令行参数,默认变量,
servlet
相关配置变量,随机值以及配置文件(比如application.properties
)等; - 控制台打印SpringBoot的
bannner
标志; - 根据不同类型环境创建不同类型的
applicationcontext
容器 - 从
spring.factories
配置文件中加载FailureAnalyzers
对象,用来报告SpringBoot启动过程中的异常; - 为刚创建的容器对象做一些初始化工作,准备一些容器属性值等,对
ApplicationContext
应用一些相关的后置处理和调用各个ApplicationContextInitializer
的初始化方法来执行一些初始化逻辑等; - 刷新容器
- 执行刷新容器后的后置处理逻辑
- 调用
ApplicationRunner
和CommandLineRunner
的run方法,我们实现这两个接口可以在spring容器启动后需要的一些东西比如加载一些业务数据等; - 报告启动异常,即若启动过程中抛出异常,此时用
FailureAnalyzers
来报告异常; - 返回容器对象
参数校验
校验注解:JSR校验注解、Hibernate Validator校验注解
验证请求体:参数前加@Valid
注解
验证请求参数:类前加@Validated
注解
异常处理
定时任务
@Scheduled
注解加在方法上,@EnableScheduling
注解加在启动类上
- 过滤器和拦截器的区别
- 拦截器是基于Java的反射机制的,而过滤器是基于函数回调的
- 过滤器是基于 Servlet 规范实现的,而拦截器是基于 Spring 框架的
- 过滤器只能对请求进行处理,而拦截器可以对请求和响应都进行处理
- 过滤器在请求进入 Servlet 容器之前处理请求,而拦截器在请求进入 Spring MVC 控制器之前进行处理
- 过滤器可以修改请求参数,拦截器不能修改请求参数,只能修改请求头和请求体中的数据
- 过滤器的使用场景比较单一,一般用于对请求进行预处理,例如字符编码、日志记录等;而拦截器的使用场景比较广泛,可以用于权限校验、日志记录、性能监控、流量控制等
- 过滤器可以配置在 web.xml 文件中,而拦截器需要在 Spring 配置文件中进行配置
计算机网络
网络模型
- OSI
功能 | |
---|---|
物理层 | 主要负责数据的传输和接收,以及网络中的电气、机械和功能特性等硬件层面的定义。 |
数据链路层 | 负责在相邻的节点之间传输数据,通过在数据帧中添加标识符和校验和来保证数据的可靠传输,同时还可以控制访问介质(如局域网)。 |
网络层 | 负责数据包在网络中的路由选择和转发,通过IP协议实现不同网络之间的连接和通信。 |
传输层 | 提供可靠的端到端数据传输服务,通过TCP协议(面向连接)和UDP协议(无连接)来实现数据的分段传输、流量控制和错误恢复等功能。 |
会话层 | 负责建立、管理和终止应用程序之间的会话,确保数据在传输过程中的顺序、完整性和可靠性。 |
表示层 | 负责数据的格式化和编码,以便不同的应用程序能够相互理解和交换数据。 |
应用层 | 为用户提供网络服务,包括电子邮件、文件传输、Web访问等,同时也是用户和网络之间的接口。 |
- OSI模型缺点
理论复杂、实现困难、缺乏灵活性、过于理论化、失去实际意义
- TCP\IP模型
功能 | |
---|---|
网络接口层 | 负责在物理网络上实现数据传输,主要使用以太网等协议 |
网络层 | 负责在不同网络之间传输数据包,使用IP协议实现不同网络之间的连接和通信 |
传输层 | 提供端到端的可靠数据传输服务,同时还提供流量控制和错误恢复等功能 |
应用层 | 提供应用程序与网络之间的接口,负责处理用户数据和应用程序数据的传输 |
- 网络分层的好处
各层之间相互独立、提高整体灵活性
数据链路层
- MAC地址
MAC地址(Media Access Control Address),也称为物理地址或硬件地址,是指网络设备(如网卡、交换机等)在出厂时预设的唯一标识符,用于标识网络设备之间的通信。MAC地址由48位二进制数字组成,通常用16进制数表示,例如00-1A-2B-3C-4D-5E。
MAC 地址有一个特殊地址:FF-FF-FF-FF-FF-FF(全 1 地址),该地址表示广播地址。
网络层
- TCP和UDP的区别
TCP | UDP | |
---|---|---|
是否面向连接 | 是 | 否 |
是否可靠 | 是 | 否 |
是否有状态 | 是 | 否 |
传输效率 | 较慢 | 较快 |
传输形式 | 字节流 | 数据报文段 |
首部开销 | 20 ~ 60 bytes | 8 bytes |
是否提供广播或多播服务 | 否 | 是 |
使用场景 | 准确性要求高的场景,文件传输接收邮件 | 即时通讯,直播音视频 |
上层协议 | HTTP、HTTPS、FTP、SMTP、POP、IMAP、TELNET、SSH | DNS、DHCP |
- TCP三握手四挥手
三握手:
一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,然后客户端进入 SYN_SEND 状态,等待服务器的确认;
二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端,然后服务端进入 SYN_RECV 状态
三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务器端都进入ESTABLISHED 状态,完成TCP三次握手。
四挥手:
第一次挥手 :客户端发送一个 FIN(SEQ=X) 标志的数据包->服务端,用来关闭客户端到服务器的数据传送。然后,客户端进入 FIN-WAIT-1 状态。
第二次挥手 :服务器收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (SEQ=X+1)标志的数据包->客户端 。然后,此时服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态。
第三次挥手 :服务端关闭与客户端的连接并发送一个 FIN (SEQ=y)标志的数据包->客户端请求关闭连接,然后,服务端进入LAST-ACK状态。
第四次挥手 :客户端发送 ACK (SEQ=y+1)标志的数据包->服务端并且进入TIME-WAIT状态,服务端在收到 ACK (SEQ=y+1)标志的数据包后进入 CLOSE 状态。此时,如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后,客户端也可以关闭连接了。
- 客户端为什么等待2MSL时间
防止客户端的ACK包没有发送到服务器端,如果服务器没有收到就会重发FIN,然后客户端再发ACK并等待2MSL,防止服务端没有收到ACK一直等待
- TCP如何保证传输可靠性
应答机制、序列号、滑动窗口、超时重传
应用层
- 常见协议
HTTP | 超文本传输协议 主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。 |
SMTP | 发送电子邮件 |
POP3\IMAP | 接收邮件 |
FTP | 文件传输协议 |
Telnet | 远程登陆协议(明文传输) |
SSH(取代Telnet) | 远程登录会话和其他网络服务提供安全性的协议 |
- HTTP3基于TCP还是UDP
HTTP 3.0 之前是基于 TCP 协议的,而 HTTP3.0 将弃用 TCP,改用 基于 UDP 的 QUIC 协议 。此变化主要为了解决 HTTP/2 中存在的队头阻塞问题。由于 HTTP/2 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。
- HTTP和HTTPS
端口号 :HTTP 默认是 80,HTTPS 默认是 443。
URL 前缀 :HTTP 的 URL 前缀是 http://
,HTTPS 的 URL 前缀是 https://
。
安全性和资源消耗 : HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
HTTPS加密原理:客户端请求、服务端发送证书、客户端验证证书、客户端生成随机数使用服务端公钥加密、服务端收到密文使用私钥系解密、使用随机数对称加密传输数据
- url输入到页面发生了什么?
DNS解析、TCP链接、发送HTTP请求、服务器处理返回HTTP报文、浏览器解析渲染页面、连接结束
- HTTP如何保存用户状态
session(会话)机制,可以储存再cookie和url中
- HTTP1.0和HTTP1.1
连接方式 : HTTP 1.0 为短连接,HTTP 1.1 支持长连接。
状态响应码 : HTTP/1.1中新加入了大量的状态码,光是错误响应状态码就新增了24种。比如说,100 (Continue)
——在请求大资源前的预热请求,206 (Partial Content)
——范围请求的标识码,409 (Conflict)
——请求与当前资源的规定冲突,410 (Gone)
——资源已被永久转移,而且没有任何已知的转发地址。
缓存处理 : 在 HTTP1.0 中主要使用 header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。
带宽优化及网络连接的使用 :HTTP1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
Host头处理 : HTTP/1.1在请求头中加入了Host
字段。
- session和cookie关系和区别
cookie | session | |
---|---|---|
存储位置 | 客户端浏览器 | 服务器内存磁盘 |
安全性 | 恶意用户窃取修改 | 相对更安全 |
传输方式 | 在每个HTTP请求中都会被发送到服务器端 | 只在用户首次访问服务器时传输一次,之后只需要传递Session ID |
数据大小 | 一般为4KB左右 | Session数据大小没有明确限制,但是过大的Session数据会占用服务器内存,影响系统性能。 |
有效期 | 可以设置过期时间,使得浏览器在一定时间后自动删除 | 通常会在用户关闭浏览器或超过一定时间后被自动删除 |
- Get和Post
作用 | 请求参数位置 | 长度限制 | 安全性 | 多次请求 | |
---|---|---|---|---|---|
get | 请求资源 | url | 有 | 低 | 结果相同 |
post | 提交数据 | 请求体 | 无 | 高 | 不同 |
设计模式(OOP)
- 设计模式的原则
开闭原则 | 扩展开放、修改关闭 |
单一职责原则 | 一个类负责一个功能 |
里氏替换原则 | 所有引用基类的地方必须能透明使用其子类对象 |
依赖倒置原则 | 依赖抽象,不依赖具体实现 |
接口隔离原则 | 类之间依赖关系建立在最小接口上 |
合成聚合复用原则 | 尽量使用组合,而不是继承 |
迪米特法则 | 一个软件实体应该尽可能少的与其他实体发生相互作用 |
- 设计模式分类
概念 | 种类 | |
---|---|---|
创建型 | ||
结构型 | ||
行为型 |
应用场景
- spring
单例模式 | 单例bean |
工厂模式 | beanfactory |
代理模式 | AOP |
模板方法模式 | JDBCTemplate |
观察者模式 | 事件机制 |
适配器模式 | HandlerAdapter |
过滤器模式 | Servlet Filter |