一、Java 基础
java 面向对象特性
- 封装(Encapsulation):
public class Student {// 将name和age封装起来private String name;private int age;// 提供方法设置和获取这些属性public void setName(String name){this.name = name;}public String getName(){return name;}public void setAge(int age){this.age = age;}public int getAge(){return age;}
}
- 继承(Inheritance):
// 父类
public class Animal {public void eat() {System.out.println("动物吃东西");}
}// 子类
public class Dog extends Animal {@Overridepublic void eat() {System.out.println("狗吃骨头");}
}
- 多态(Polymorphism):
public class Animal {public void sound(){System.out.println("动物发出声音");}
}public class Cat extends Animal{public void sound(){System.out.println("猫发出声音:喵喵喵");}
}public class Test {public static void main(String args[]) {Animal a = new Cat(); // Cat 对象被当作 Animal 来使用a.sound();}
}
- 抽象(Abstraction):
public abstract class Animal {public abstract void sound(); // 抽象方法
}public class Dog extends Animal {@Overridepublic void sound() {System.out.println("狗发出声音: 汪汪汪");}
}public class Test {public static void main(String args[]) {Animal a = new Dog(); // Dog 对象被当作 Animal 来使用a.sound();}
}
Java 中访问控制符 public、protected、default 和 private
当前类 | 子类 | 当前包 | 其他包 | |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | |
default | √ | √ | ||
private | √ |
Java 中基本数据类型有哪些?以及所占字节数?
类型 | 所占字节数 |
---|---|
byte | 1 ,取值范围是-128到127。 |
short | 2 ,取值范围是-32768到32767。 |
int | 4 ,取值范围是-2147483648到2147483647。 |
long | 8 ,取值范围是-9223372036854775808到9223372036854775807。 |
double | 8 ,可表示约7位有效数字的浮点数。 |
float | 4 ,可表示约16位有效数字的浮点数。 |
char | 2 ,用于表示一个字符。 |
boolean | 不明确定义其大小,只有两个取值,即true和false。 |
final,finally,finalize的区别
final 是一个修饰符,用于修饰类、方法、属性。修饰类时类不能被继承,修饰方法时方法不能被子类重写,修饰属性时属性不能被修改。
finally 是一个关键字,和 try cache 连用,用于执行一些资源关闭操作。
finalize 是 Object 类中的一个方法,用于垃圾回收机制。finalize 方法在对象被垃圾回收前调用,用于进行资源释放。
重载和重写的区别?
重载是指一个类中有多个重名的方法,它们的参数、返回值可以不同。
重写是指子类重写父类的方法,方法名、参数和返回值必须相同。
抽象类和接口有什么区别
抽象类 | 接口 |
---|---|
使用 abstract 修饰 | 使用 Interface |
有构造器、和成员属性(构造器和规范子类实例化行为,子类必须要传构造器定义的参数) | 无构造器、成员属性 |
可以有非抽象方法 | 有默认方法 |
单继承(一个类只能继承) | 多实现(一个类可以实现多个接口) |
对事物的抽象 | 对行为的抽象(比抽象类更抽象) |
String类能被继承吗,为什么
不能,String 类被 final 修饰,不能被继成。这样是为了保证 String 的不可变性。
Java中变量和常量有什么区别
变量可以被修改,而常量不能被修改,使用 final 修饰。
说说你对Integer缓存的理解
在 java 中,Integer 对 -128~127 范围整数进行了缓存。当创建一个 Integer 对象时,会直接从缓存中获取对象,不会创建新对象。
Integer缓存是Java中的一个优化特性,主要用于减少不必要的对象创建,提高了程序的执行效iciency。这个特性在Java 5中被引入,主要应用于Integer.valueOf()方法。
Integer缓存主要体现在-128到127之间的整数。当我们使用Integer.valueOf()方法创建一个在这个范围内的Integer对象时,Java会直接从缓存中返回这个值的实例,而不是新建一个对象。这是因为在此范围内的整数在日常开发中使用的频率比较高,直接使用缓存可以提高性能。
例如,当我们执行Integer.valueOf(100)时,返回的对象实际上是在Java启动时就创建并缓存的,而不是一个新的对象。此时如果再执行一次Integer.valueOf(100),返回的将是同一个对象。
需要注意的是,这个特性只适用于通过Integer.valueOf()方法创建的Integer对象,如果直接使用new Integer()方式创建的Integer对象,Java会创建一个新的对象。
这个缓存机制对于节省内存和提高程序执行效率有一定的帮助,但是也可能导致一些编程上的陷阱,比如在使用"=="比较Integer对象时,可能得到的并不是期望的结果。因此在编程时需要注意这个特性带来的影响。
Integer.valueOf(1) 会生成新对象吗?
答案是:不会
Integer.valueOf(int) 方法是 Integer 类的静态方法,用于返回表示指定 int 值的 Integer 实例。它会首先检查值是否在 -128 到 127 的范围内,如果在范围内,则返回一个缓存的 Integer 对象,而不是新创建一个对象。这是因为在这个范围内的整数常用,所以通过缓存可以提高性能和节省内存。
如果传入的 int 值不在 -128 到 127 的范围内,Integer.valueOf(int) 方法会创建一个新的 Integer 对象,并返回这个新对象。
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // 输出:true,使用缓存的 Integer 对象Integer c = Integer.valueOf(200);
Integer d = Integer.valueOf(200);
System.out.println(c == d); // 输出:false,创建了新的 Integer 对象
异常处理机制
Java中的异常处理机制主要包括五个关键词:try、catch、finally、throw、throws。
- try:用来包围可能会抛出异常的代码。
- catch:用来捕获和处理try中抛出的异常。
- finally:无论是否产生异常,finally关键字后面的代码都会被执行。
- throw:手动抛出一个异常。
- throws:用在方法声明上,表示该方法可能会抛出哪些异常。
Java的异常处理机制主要是通过以上五个关键字,组成了try-catch-finally这样的结构来处理运行时的错误。如果一个方法可能会产生某种异常,但是并不能处理这种异常,那么可以用throws进行声明,然后由调用这个方法的代码来处理。
Java的异常类是一个类系统,所有的异常类都是Throwable类的子类。其中,Error和Exception是其两大子类,Error表示系统错误,一般我们不处理,Exception表示需要我们处理的异常,它下面有许多子类,如IOException, NullPointerException等。
处理异常的一般步骤是:首先,尝试执行try里面的代码。如果出现异常,系统会抛出一个异常对象,然后转到与之匹配的catch块处理。如果try代码块没有异常,catch块里的代码不会被执行。最后,无论是否出现异常,finally代码块中的代码都会被执行。
静态变量和实例变量的区别
作用范围:
- 静态变量是类的所有实例共享的变量,使用 Static 修饰,当某个对象修改了这个变量,会影响到所有的对象。
- 实例变量是属于对象的,每个对象都有一份独立的副本,互不影响。
初始化:
- 静态变量在类加载的时候就被初始化。
- 实例变量在创建对象的时候初始化。
值传递和引用传递
1、关于值传递
值传递:是指在调用函数时,将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,就不会影响到实际参数
如下图所示,当传递参数之前会将参数进行复制,函数中修改了参数,不会影响实际参数
值传递是对于是对基本数据而言,例如下面例子,number没有改变。
public class Test01 {public static void main(String[] args) {int number = 10;new Test01().change(number);System.out.println(number);}public void change(int a){a+=1;}
}
2、关于引用传递
引用传递:是指在调用函数时,将实际参数的地址传递到函数中,那么在函数中对参数进行修改,将会影响到实际参数
引用数据类型分为两个部分,引用变量和对象,这两个部分放在不同的地方,引用变量在栈中,而对象是放在堆内存中的,引用变量指向对象。
如下图所示,当传递参数之前会将参数进行复制,但是复制的是引用变量,复制后的引用变量还是指向内存中的同一对象,所以引用传递中,函数修改了参数会影响实际参数
引用传递是对于引用数据类型而言,例如对于User类姓名的修改,会改变对象的name。
public class Test01 {public static void main(String[] args) {User user = new User("小明", 12);new Test01().change(user);System.out.println(user);}public void change(User a){a.setName("小张");}
}
注意点,引用类型中的String的值是放在常量池中,我们改变副本的值不会影响到原来的值。
例如在change放发中改变了副本的值,但是原来的str字符串不会改变。
public class Test01 {public static void main(String[] args) {String str = "hello";new Test01().change(str);System.out.println(str);}public void change(String a){a="yes";}
}
Java 创建对象有几种方式
- 使用 new 创建
- 使用反射创建
- 对象反序列化
- 使用 clone 克隆
Java8的新特性
- 函数式接口
- Lambda表达式
- 接口默认方法
- 方法引用
- stream流
- 日期时间类库
TCP 三次握手,四次挥手
TCP 协议在连接时会三次握手,断开连接时四次挥手
参考视频yyds:TCP三次握手详解–三分钟精品课堂系列_哔哩哔哩_bilibili
参考视频:TCP四次挥手详解–三分钟精品课堂系列_哔哩哔哩_bilibili
http 和 https 的区别
- 传输协议:HTTP 是未加密的明文传输,数据在传输过程中可能被恶意捕获和修改。HTTPS 通过 SSL/TLS 协议加密数据,保护数据在传输过程中的安全。
- 默认端口:HTTP 的默认端口是 80,HTTPS 的默认端口是 443。
- 证书:HTTPS需要到CA申请证书,一般免费证书很少,需要付费。
cookie 、session 和 token 的区别?
cookie 和 token 是信息是保留在客户端的,session 信息是保留在服务端的
cookie 信息保留在客户端信息是不安全的容易被篡改
session 信息保留在服务端,占用了大量的内存。
token 解决了 cookie 和 session 出现的问题。token 是由服务端加密算法(JWT)获得的,之后保留到客户端中,后续每次请求都需要带上 token 。
String、StringBuffer 和 StringBuider
- String:是不可变的字符序列。底层是 char 类型的数组,每次对 String 类型进行修改都会生成一个新的 String 对象,所以对于频繁修改内容的字符串最好不 要用 String,因为每次生成对象都会对系统性能产生影响。
- StringBuffer:是可变的字符序列。在进行频繁的字符串操作时,可以使用 StringBuffer,因为 StringBuffer 是在原对象的基础上进 行操作,不会像 String 那样产生新的对象。而且,StringBuffer 是线程安全的,也就是说多个线程可以同时操作 StringBuffer 对象,虽然这会带来一定的系统开销。
- StringBuilder:是可变的字符序列。与 StringBuffer 类似,StringBuilder 也是在原对象的基础上进行操作,不会产生新的对象。但 是,与 StringBuffer 不同,StringBuilder 不是线程安全的。因此,如果一个字符串在被多个线程操作的情况下,应 该使用 StringBuffer,但如果一个字符串只在单个线程中被使用,那么使用 StringBuilder 性能会更好,因为它避免了 线程同步带来的系统开销。
Java 中如何将字符串反转
1、使用 StringBuffer 或者 StringBuilder 中的 reverse() 方法。
2、将字符串转换成字符数组,然后遍历字符数据
Stream 并行流是如何实现的,什么情况下使用并行流反而更慢?
可以使用parallel()将流变成并行流,或者直接使用parallelStream()。
底层实现:线程池的
Java 8使用了Fork/Join框架来实现并行流。Fork/Join框架是一个用于并行计算的框架,通过将任务分割成更小的任务并行执行,最后将结果合并起来。在并行流的背后,Fork/Join框架将任务分成更小的子任务,并使用多个线程同时处理这些子任务,最后将结果合并起来返回。
并行流反而更慢场景:
1、数据量不大。
2、流本身要求有顺序的话并行效率更低。
Java 中常用类及方法
“==”与equls方法的区别详解
- 先来说说"=="
"=="既能比较基本数据类型,也能比较引用数据类型。
- 比较基本数据类型的时候,比较的是值的大小
- 引用数据类型分为两个部分,栈中的引用变量和内存中的对象,引用变量储存的是对象在内存中的地址。比较引用数据类型的时候,比较的是两个引用变量存储的地址是否相等。
- 再来谈谈equals方法
equals方法是对于引用数据类型而言,比较的是对象的内容
- equals方法是在"=="上进行改进,我们可以看看Object类equals方法源码,它的底层就是用等于等于比较的。
public boolean equals(Object obj) {return (this == obj);}
当我们的创建的类,要比较两个对象的实例是否相等,要重写equals方法和hashcode方法。equals方法里就是我们判断两个对象的实例是否相等条件。
- equls方法常用于比较两个字符串内容是否相等,我们可以看看String类的源码
public boolean equals(Object anObject) {if (this == anObject) {return true;}if (anObject instanceof String) {String anotherString = (String)anObject;int n = value.length;if (n == anotherString.value.length) {char v1[] = value;char v2[] = anotherString.value;int i = 0;while (n-- != 0) {if (v1[i] != v2[i])return false;i++;}return true;}}return false;}
它的底层是先比较地址是否相等,如果相等的就直接认为两个字符串是相等的。如果地址不相等,在比较字符串的内容是否相等。
【异常】异常种类有哪些?
异常(Throwable)分为 Exception 和 error , Exception 又分为运行时异常,IO异常, error 分为 JVM错误 和 JWT 错误,JVM 错误有 StackOverFlow 栈溢出,和 OutOfMemory 内存溢出。
【异常】throw 和 throws 有什么区别?
throw 用于主动抛出异常,throws 用于可能抛出异常的方法上,可以抛出多个异常。
【异常】如何自定义异常,并进行异常处理?
自定义异常:继承异常类
public class MyCustomException extends RuntimeException {// 添加自定义属性private int errorCode;// 自定义构造方法public MyCustomException(String message, int errorCode) {super(message);this.errorCode = errorCode;}// 添加自定义方法public int getErrorCode() {return errorCode;}
}
进行异常处理:
@ControllerAdvice
public class GlobalExceptionHandler {// 处理自定义异常@ExceptionHandler(MyCustomException.class)public ResponseEntity<ErrorResponse> handleMyCustomException(MyCustomException ex) {ErrorResponse errorResponse = new ErrorResponse(ex.getMessage(), ex.getErrorCode());return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);}}
【反射】什么是反射?
在程序运行时动态获取和操作类信息(属性和方法)。
【反射】获取 Class 有几种方式
- 类名.class
- 对象.getClass()
- Class.forName()
【注解】Java 中如何自定义注解?
- @Target 指定注解可以在哪些地方使用。
- @Retention 指定注解生效时机
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface A {String value() default "";
}
【集合】容器集合有那一些?
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
【集合】List 、Set 集合有哪些?
集合容器
Collection又分为 List 和 Set 两大类,分别有多种实现。
List 集合
-
ArrayList :1、底层是数组,增删慢查询快。
2、初始容量是10,扩容因子是0.5,扩容时扩容至原来的**1.5倍**。
-
LinkedList :底层是链表,增删快查询慢。
-
vector :ArrayList和LinkedList都是线程不安全的,而vector是线程安全的。不过已经不推荐使用了,
推荐的线程安全的集合是 CopyOnWriteArrayList
Set 集合
- HashSet :底层是Hash表实现的,其实就是 HashMap 的key。
- LinkedHashSet :与 HashSet 相比,可以记录元素插入的顺序。
- TreeSet :底层是红黑树实现,插入的元素是有顺序的。
总结:1、List 集合存储元素可以重复,Set 集合存储元素是唯一的
2、线程安全的集合只有 vector ,要使用线程安全的集合可以使用 ConcurrentXXX,例如ConcurrentHashSet、ConcurrentArrayList。
【集合】ArrayList 和 LinkedList 的区别?查询和插入的时间复杂度分别是多少?
对于ArrayList:
- 底层是数组实现,查询快增删慢,在内存中的空间是连续的。
- 查询(get)操作的时间复杂度是O(1),因为ArrayList使用基于索引的方式存储元素,可以通过索引直接访问指定位置的元素。
- 插入(insert)操作的时间复杂度是O(n),因为在插入元素时,需要将插入位置之后的元素都向后移动一个位置。
对于LinkedList:
- 底层是双向链表实现,查询慢增删快,在内存中可以是碎片空间,通过指针连接。
- 查询(get)操作的时间复杂度是O(n),因为LinkedList使用链表的方式存储元素,需要从头或尾开始遍历链表找到指定位置的元素。
- 插入(insert)操作的时间复杂度是O(1),因为在插入元素时,只需要修改相邻节点的引用,不需要移动大量元素。
【集合】HashMap与Hashtable的区别?
HashMap | Hashtable | |
---|---|---|
线程安全性 | 不是线程安全的 | 线程安全(通过 Synchronized 实现) |
继承关系 | 实现了 Map 接口 | 继承 Dictionary 抽象类,并且实现了 Map 接口 |
容量及扩容 | 初始容量为 16,扩容因子为 0.75, 每次扩容到原来的 2 倍。 | 初始容量为 11,扩容因子为 0.75, 每次扩容到原来的 **2n - 1 **倍。 |
key 为空情况 | 可以存空值 null | 不能存空值 |
hash 算法不同 |
【集合】如何获取线程安全的集合?
- 通过 Collections 中的一些列的 Synchonsized 方法将普通集合进行转换。
-
直接使用 juc 并发包下的集合
| **package java.util; ** | package java.util.concurrent; 包下 |
| — | — |
| List | CopyOnWriteArrayList:是线程安全的动态数组,它可以用作线程安全的List。它的实现方式是在每次修改操作(添加、删除、修改等)时,都会创建一个新的数组,并将元素复制到新数组中。这样就保证了在多线程并发修改的情况下,不会出现数据不一致的问题。 |
| Set | CopyOnWriteArraySet:是线程安全的集合类,它实现了 Set 接口,并且内部 CopyOnWriteArrayList 实现。它的特点是在修改操作时会创建一个新的数组来进行修改,以保证线程安全性。
ConcurrentSkipListSet:是线程安全的有序集合,它可以用作线程安全的Set。它内部使用跳表(Skip List)数据结构来实现有序性和高并发性。 |
| Map | ConcurrentHashMap:是线程安全的哈希表,它可以用作线程安全的Map。它使用分段锁机制,不同的线程可以同时访问不同的分段,在读操作上可以提供较高的并发性能。 | -
使用同步锁
通过在访问集合时使用同步锁来保证线程安全。可以使用synchronized关键字来实现同步,或者使用Lock接口及其实现类来实现更灵活的同步控制。
【集合】红黑树和其他树的区别
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色的。
- 红黑树通过旋转和重新着色操作,保持整棵树的平衡
【集合】HashMap 底层原理
Java集合常见面试题总结(下)
JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 hash &(length-1)判断当前元素存放的位置(这里的 length 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
static final int hash(Object key) {int h;// key.hashCode():返回散列值也就是hashcode// ^:按位异或// >>>:无符号右移,忽略符号位,空位都以0补齐return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7 的 HashMap 的 hash 方法源码.
static int hash(int h) {// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
}
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
jdk1.8 之前的内部结构-HashMap
JDK1.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
<br /> jdk1.8之后的内部结构-Hash![](https://cdn.nlark.com/yuque/0/2024/png/35876559/1716516268626-66ccd6ec-eae5-4b3b-885f-b6f4cfb1741e.png#averageHue=%23ece9e8&clientId=ue02682d7-64a8-4&from=paste&id=u36fbe1d8&originHeight=371&originWidth=453&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u0f44fd4b-4754-4279-8966-8ceab04d5a7&title=)Map
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
我们来结合源码分析一下 HashMap 链表到红黑树的转换。
1、 putVal 方法中执行链表转红黑树的判断逻辑。
链表的长度大于 8 的时候,就执行 treeifyBin (转换红黑树)的逻辑。
// 遍历链表
for (int binCount = 0; ; ++binCount) {// 遍历到链表最后一个节点if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 如果链表元素个数大于等于TREEIFY_THRESHOLD(8)if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st// 红黑树转换(并不会直接转换成红黑树)treeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;
}
2、treeifyBin 方法中判断是否真的转换为红黑树。
final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;// 判断当前数组的长度是否小于 64if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)// 如果当前数组的长度小于 64,那么会选择先进行数组扩容resize();else if ((e = tab[index = (n - 1) & hash]) != null) {// 否则才将列表转换为红黑树TreeNode<K,V> hd = null, tl = null;do {TreeNode<K,V> p = replacementTreeNode(e, null);if (tl == null)hd = p;else {p.prev = tl;tl.next = p;}tl = p;} while ((e = e.next) != null);if ((tab[index] = hd) != null)hd.treeify(tab);}
}
将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。
【集合】HashMap 的长度为什么是 2 的幂次方
原文连接:
Java集合常见面试题总结(下)
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ hash & (length-1)”。(length 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
【多线程】什么是线程?什么是进程?
线程是 CUP 执行的基本单位,一个进程有多个线程组成。
【多线程】什么是并行,什么是并发?
- 并行:并行是指多个任务在同一时刻被并行处理。比如在一个多核心的CPU上,可以有多个处理器在同一时刻处理多个不同的任务,每个处理器处理一个独立的任务。这就是并行。
- 并发:并发是指在同一时间段内多个任务都在运行,但在某一瞬间只有一个任务在单个CPU上运行。如果系统只有一个CPU,那么那些看起来同时运行的任务其实是在不断地进行着切换,这种就是并发。
简单来说,并发和并行的关系可以这么理解:并发是逻辑上的同时发生,可能实际上是交替执行的;而并行是物理上的同时发生,需要硬件的支持,真正地同时执行多个任务。
【多线程】线程有几种状态
在 Thread.State 枚举类中有 6 种状态,在操作系统层面有 5 中状态。
public enum State {/*** 初始*/NEW,/*** 运行*/RUNNABLE,/*** 阻塞*/BLOCKED,/*** 等待*/WAITING,/*** 超时等待*/TIMED_WAITING,/*** 终止*/TERMINATED;
}
操作系统层面有 5 种状态,运行分为了运行中和就绪,而等待、超时等待和阻塞这三种状态都算一种阻塞状态,它们的关系如下:
【多线程】线程中有哪些方法,有什么特点?
方法 | 说明 |
---|---|
start() | 启动一个线程,使线程进入可运行(Runnable)状态,等待调度执行。 |
run() | 定义线程要执行的任务,通常在自定义的线程类中重写该方法。当线程的 start() 方法被调用时,会执行 run() 方法中的代码。 |
sleep(long millis) | 使当前线程暂停指定时间(以毫秒为单位)的执行,进入阻塞状态。在指定时间结束后,线程会重新进入可运行状态。 |
join() | 使当前线程等待其他线程执行完毕后再继续执行。调用 join() 方法的线程会被阻塞,直到被等待的线程执行完毕。 |
interrupt() | 中断线程,给线程发送一个中断信号,线程可以选择在接收到中断信号后终止执行。 |
isInterrupted() | 只检查线程实例的中断状态,不会清除中断状态。 |
Thread.interrupted() | 检查当前线程的中断状态,并清除中断状态。 使用了 try-catch(InterruptedException)捕获了中断异常,中断状态也会被清除 |
yield() | 暂停当前正在执行的线程对象,并让其他具有相同优先级的线程有机会执行。如果没有其他具有相同优先级的线程,或者其他线程都处于阻塞状态,那么当前线程将继续执行。 |
setPriority(int priority) | 设置线程的优先级,优先级越高的线程在竞争资源时有更大的概率获得执行权。 |
isAlive() | 判断线程是否处于活动状态,即线程已启动且尚未终止。 |
【多线程】线程怎么停止?
Thread thread = new Thread();
thread.stop(); // 已弃用
thread.interrupt(); // 中断线程,修改中断标志位
thread.isInterrupted(); // 查看线程中断状态
【多线程】start() 和 run() 有什么区别
start() | run() | |
---|---|---|
1 | 异步执行 | 同步执行 |
2 | 启动一个新线程 | 存放任务代码 |
3 | 只能执行一次 | 可以无限调用 |
【多线程】wait( ) 和 sleep( ) 的区别?
- 所属类别不同:wait( ) 是 Object 类的,而 sleep( ) 是 Thread 类的。
- 锁行为的差别:wait( ) 会释放锁,而 sleep( ) 不会释放锁。
- 使用范围: wait( ) 方法必须在同步方法或同步块中调用,否则会抛出IllegalMonitorStateException异常。而sleep()方法没有这个限制,可以在任何地方使用。
【多线程】volatile 关键字
volatile 是 Java 提供的一种轻量级的同步机制,它主要有两个特性,即保证变量的可见性和禁止指令重排序。
- 保证变量的可见性:在 Java 中,每个线程都有一个独立的工作内存,线程的操作都在自己的工作内存中进行,然后将结果同步回主内存。如果一个变量被 volatile 关键字修饰,那么当一个线程修改了这个变量后,新的值会立即同步回主内存,当其他线程需要读取这个变量时,会直接从主内存中读取,而不是从自己的工作内存中读取。这就保证了 volatile 变量在所有线程中的可见性。
- 禁止指令重排序:在 Java 中,为了提高程序的运行效率,编译器和处理器可能会对代码进行优化,包括将多条指令的顺序进行调整。但是如果涉及到 volatile 变量的操作,系统会禁止进行指令重排序。这是因为 volatile 变量通常涉及到多线程的同步,如果指令重排序可能会导致程序行为的改变。
在底层实现上,volatile 通过添加内存屏障达到其效果。在写操作时,会在写指令后添加一条 store屏障,强制将修改的内容刷新至主内存。在读操作时,会在读指令前添加一条load屏障,强制从主内存中读取内容,而不是从缓存中取。这样就保证了其他线程能立即看到最新的修改。另外,这些屏障也阻止了指令的重排序。
/*** 1、线程 A 会进入到死循环。* 2、线程 B 改变标志位 flag* 3、线程 A 对线程 B 修改标志位可见,跳出死循环*/
public class VolatileDemo {private volatile boolean flag = false;public void setFlag(boolean value) {this.flag = value;}public void work() {// flag默认是 false ,会执行死循环while (!flag) {System.out.println("do ....");}System.out.println("Work completed!");}public static void main(String[] args) {VolatileDemo demo = new VolatileDemo();// 执行死循环new Thread(demo::work,"A").start();// 通过改变标志位(因为flag使用 volatile 修饰,对其他线程都可见),打破线程A的死循环new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}demo.setFlag(true);},"B").start();}
}
【多线程】ThreadLocal 线程局部变量
ThreadLocal 是 Java 中一种很重要的线程局部变量工具类。它为每个线程都创建一个单独的变量副本,每个线程可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
ThreadLocal 的底层实现主要依赖于每个线程内部的一个叫做 ThreadLocalMap 的数据结构。
- ThreadLocalMap 是 ThreadLocal 的内部类,用于存储线程的局部变量。每个 Thread 对象内部都有一个 ThreadLocalMap 对象。
- ThreadLocalMap 的 key 是 ThreadLocal 对象,value 是线程局部变量。也就是说,每个线程都持有一个独立的 ThreadLocalMap,并且这个Map里面保存了当前线程所有的 ThreadLocal 变量。
- 当通过 ThreadLocal.set(value) 方法设置线程局部变量时,实际上是把 ThreadLocal 对象作为 key,value 作为 value,存入到当前线程的 ThreadLocalMap 中。
- 当通过 ThreadLocal.get() 方法获取线程局部变量时,实际上是从当前线程的 ThreadLocalMap 中,以 ThreadLocal 对象为 key,取出对应的 value。
这样,每个线程都有自己独立的 ThreadLocalMap,而且 Map 中以 ThreadLocal 对象为 key,存储了所有的线程局部变量,从而实现了线程之间的数据隔离,使得每个线程都有自己独立的线程局部变量。
使用场景:动态数据源切换
【多线程】ThreadLocal 存在内存泄露吗?
ThreadLocal 存在内存泄露,主要原因在于 ThreadLocalMap 的 key 使用了弱引用(weak reference),而 value 使用了强引用(strong reference)。
在 Java 中,只要对象被强引用关联,那么无论这个对象是否再被使用,垃圾回收器都不会回收它。而弱引用关联的对象,在下一次垃圾回收时,无论内存是否充足,都会被回收。
【多线程】Synchronsize 底层实现原理?
Synchronized 是 Java 中的一个关键字,用于实现线程间的同步,保证共享资源的互斥访问,从而避免多线程下的数据不一致问题。
Synchronized 的底层实现原理是基于 JVM 内的监视器锁(monitor)。在 JVM 规范中,每一个对象和类都有与之关联的监视器。当一个线程尝试获取对象锁时,实际上是在尝试获取这个对象的监视器。
具体流程如下:
- 当一个线程访问一个被 synchronized 修饰的代码块或方法时,JVM 会检查这个线程是否已经获取了这个对象的锁。
- 如果没有获取到锁,那么这个线程就会进入到一个入口队列,队列中的线程会阻塞,直到获取到锁。
- 如果已经获取到锁,那么这个线程可以执行这个 synchronized 修饰的代码块或方法,同时锁的计数器会加 1。
- 当这个线程执行完这个 synchronized 修饰的代码块或方法后,计数器会减 1。如果计数器为 0,锁就会被释放,此时阻塞在入口队列中的线程就可以尝试获取这个锁。
补充说明:
- 如果一个线程正在执行一个 synchronized 代码块,而其他线程试图调用同一个对象的其他 synchronized 方法,那么这些线程将会被阻塞,直到第一个线程完成 synchronized 代码块的执行并释放锁。这就是 synchronized 的互斥性。
- 如果一个线程正在执行一个对象的 synchronized 代码块,同时另一个线程正在执行这个对象的非 synchronized 代码块,那么非 synchronized 代码块的执行不会受到阻塞。这是因为 synchronized 锁定的是对象,而非代码。
【多线程】Synchonsized 锁的是什么?
Synchonsized 可以用在方法上,和同步代码块上。
使用在方法上时:
- 静态方法:锁的是类。
- 非静态方法:锁的是调用方法的对象。
使用在同步代码块上,根据传入的参数进行加锁:
- this :锁的是调用对象。
- 类.class :锁的是类。
【多线程】Synchronized 和 ReentrantLock 的区别?
- 属性区别:
- Synchronsized是Java关键字,是Java内置的一种同步方式,属于Java语言层面的互斥手段。
- ReentrantLock 是Java并发包(java.util.concurrent.locks)中的类,是更高级的线程同步机制。
- 使用方式区别:
- Synchronized使用方便,当它获取的锁是无需手动释放的,当Synchronized代码执行后,系统会自动让线程释放对锁的占用。
- ReentrantLock则需要手动获取和释放锁。如果不主动释放锁,就有可能导致出现死锁现象。
- 灵活性区别:
- Synchronized在资源竞争时,如果资源被占用,其他线程只能等待,无法干预。
- ReentrantLock则提供了一个可以响应中断的锁获取操作,一个可定时的锁获取操作,以及一个可以无限期等待获取锁的操作,这些都是Synchronized所不具备的。
- 安全性区别:
- Synchronized不支持状态的确认,也无法获取锁的占有情况,更无法看到等待获取锁的线程。
- 而ReentrantLock可以看到锁的状态,还可以获取等待锁的线程,从而更好地控制并发状态。
【多线程】如何实现线程之间的通信?
- 使用 Synchronsized + wait() + notify()/notifyAll()
public class Book2 {private int number = 10;// 还书public synchronized void increment() throws InterruptedException {while (number == 10) {this.wait();}System.out.println(Thread.currentThread().getName() + ",还了第" + number++ + "书");this.notifyAll();}// 借书public synchronized void decrement() throws InterruptedException {while (number == 0) {this.wait();}System.out.println(Thread.currentThread().getName() + ",借出第" + number-- + "书");this.notifyAll();}
}
- 使用 Lock + Condition(await() + signalAll())
public class Book {private int number = 10;private final ReentrantLock lock = new ReentrantLock();private final Condition condition = lock.newCondition();// 还书public void increment() {lock.lock();try {while (number == 10) {condition.await();}System.out.println(Thread.currentThread().getName() + ",还了第" + number++ + "书");condition.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}// 借书public void decrement() {lock.lock();try {while (number == 0) {condition.await();}System.out.println(Thread.currentThread().getName() + ",借出第" + number-- + "书");condition.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}
}
【多线程】怎么防止虚假唤醒?
不使用 if 判断,使用 while( ) 作为判断等待条件
【多线程】线程死锁的四个条件?
- 互斥条件:一个资源每次只能被一个线程访问
- 请求与保持:一个线程因请求资源而阻塞式,对以获得的资源保持不放
- 不剥夺条件:进程获得的资源,在未使用完成之前,不能被强行剥夺
- 循环等待:若干个进程之间形成一种头尾相接的循环等待资源关系
【多线程】创建线程有哪些方式?
继承Thread类 | 创建一个新的类,该类继承自Thread类,然后在该类中重写Thread类的run()方法。然后实例化该类并调用其start()方法启动线程。 |
---|---|
实现Runnable接口 | 创建一个新的类,该类实现Runnable接口,并实现该接口的run()方法。然后创建Thread对象,将Runnable对象作为Thread对象的target,然后调用Thread对象的start()方法启动线程。 |
使用Callable和Future接口 | 创建一个新的类,该类实现Callable接口,然后将Callable对象作为参数传递给FutureTask的构造器,然后将FutureTask对象作为Thread的target,最后调用Thread对象的start()方法启动线程。在Callable接口的call()方法中可以返回值,并且可以抛出异常。 |
使用线程池 | 通过Executor框架中的Executors类的静态工厂方法创建线程池,然后调用ExecutorService的execute()或submit()方法。 |
使用Fork/Join框架 | 在Java 7中引入,用于大型并行任务。 |
ScheduledExecutorService接口 | 用于执行定时任务或周期性任务。 |
class MyCallable implements Callable<Integer> {public Integer call() {return 123;}
}public class Main {public static void main(String[] args) throws Exception {FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());new Thread(futureTask).start();System.out.println("Callable return: " + futureTask.get());}
}
【多线程】常用线程池有哪些?
- newFixedThreadPool:创建一个固定数目的,可重用的线程池。适用于为了满足资源管理的需求,需要限制当前线程数量的应用场景,它可控制线程最大并发数,超出的线程会在队列中等待。
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {executorService.execute(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is running.");}});
}
executorService.shutdown();
- newCachedThreadPool:创建一个可根据需要创建新线程的线程池。适用于执行很多短期异步的小程序或者负载较轻的服务器。
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {executorService.execute(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is running.");}});
}
executorService.shutdown();
- newSingleThreadExecutor:创建一个只有一个线程的线程池,它可以保证先进先出的执行顺序。适用于需要保证顺序执行的场景,并且只有一个后台任务时使用。
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {executorService.execute(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is running.");}});
}
executorService.shutdown();
- newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。适用于需要进行定时/周期性任务的场景。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {public void run() {System.out.println(Thread.currentThread().getName() + " is running.");}
}, 1, 3, TimeUnit.SECONDS);
【多线程】自定义线程池,与线程池七大参数
参数 | 说明 |
---|---|
corePoolSize | 线程池中的核心线程数量,当提交一个任务时,如果当前线程池中的线程数量未达到corePoolSize,则新建线程执行任务,如果达到了,则不再新建。 |
maximumPoolSize | 线程池中允许的最大线程数量,如果当前线程池的线程数量达到了corePoolSize,并且阻塞队列也已经满了,那么就会新建线程执行任务,直到线程数量达到maximumPoolSize。如果此时仍有新的任务提交,那么就会采取拒绝策略。 |
keepAliveTime | 非核心线程的闲置超时时间,如果一个非核心线程完成了任务后,如果在keepAliveTime内没有新的任务,那么就会被销毁。 |
unit | 超时时间的单位,通常使用java.util.concurrent.TimeUnit中的枚举值。 |
workQueue | 阻塞队列,用于存储待执行的任务,常用的有如下几种:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。 |
threadFactory | 线程工厂,用于创建新的线程并为其设置有意义的名称。 |
handler | 拒绝策略,当线程数量达到maximumPoolSize并且阻塞队列已满时,我们可以采取一些策略:比如丢弃任务、抛出异常、使提交任务的线程执行任务等。 |
- ThreadPoolExecutor.AbortPolicy:直接抛出RejectedExecutionException异常阻止系统正常运行。这是默认的拒绝策略。
2. ThreadPoolExecutor.CallerRunsPolicy:只用调用者所在的线程来运行任务。如果执行器已关闭,则会丢弃该任务。
3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃阻塞队列中的队头任务,并执行当前任务。如果执行器已关闭或者将要关闭,那么任务也会被丢弃。
4. ThreadPoolExecutor.DiscardPolicy:直接丢弃任务,而不抛出任何异常。如果允许任务丢失,这是一种可行的策略。
|
下图是线程池构造器:
二、Spring 家族
【Spring】Spring 中的 IoC 和 AOP
IoC 控制反转
IoC 控制反转,以前我们创建对象是直接 new,使用了 IoC 后我们不用主动创建对象了,只需要配置好 Bean 注册到容器中,后面需要调用我们可以直接去容器中取。
IoC 是一种编程思想,具体实现是 DI 依赖注入。当我们需要从容器中获取对象时,可以使用 @Autowired + @Qualifier(value = “”) 或 @Resource来获取对象。
AOP 面向切面编程
是一种切面编程方式,它是通过动态代理实现的。
【Spring】AOP 中通知顺序
【Spring】DI 依赖注入注解 @Autowired 、 @Resource 和 @Inject
@Autowired 先 byType 如果有多个 Bean 在 byName 获取指定名称的 Bean(默认是首字母小写的类名)。或者使用 @Qualifier 来指定名字。
@Resource 先 byName 在 byType
@Inject 和 @Autowired 一样,配合@Named 来获取指定名称的 Bean 。
【Spring】定义 Bean 的注解有哪些?
业务相关:@Repository、@Service、@Controller
配置相关:@Component、@Configuration
直接配置:@Bean
【Spring】 @Component 和 @Configuration 的区别?
一句话概括就是 @Configuration 中所有带 @Bean 注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例。
例如下面代码 userInfo 方法中直接调用了country() 方法,和 @Autowired Country country 效果相同:
@Configuration
public class MyBeanConfig {@Beanpublic Country country(){return new Country();}@Beanpublic UserInfo userInfo(){return new UserInfo(country());}}
【Spring】Bean 的作用域有哪些?
- singleton:单例的
- prototype:多例
- request
- session
- global-session
【Spring】Bean 的生命周期
【Spring】 Bean 实例化和 Bean 初始化有什么区别?
实例化:对象通过类构造函数创建对象,这个时候还没有为属性赋值。
初始化:类实现 InitializingBean 接口,重写 afterPropertiesSet() 方法,初始化时将执行 afterPropertiesSet() 方法中的逻辑
【Spring】事务是如何实现的?
事务是通过动态代理来实现了,具体过程如下:
【Spring】事务失效场景?
-
类中一个事物方法直接调用另一个事物方法。
失效原因:第一个事物方法没有使用动态代理的对象去调用另一个事物方法。<br /> 解决方法:可以将当前类在注入到当前类中,或者直接从 AopContext 中获取。
-
方法使用 private 或者 final 修饰。
失效原因:private 修饰的方法无法被重写, AOP 动态代理,有权限问题。
-
数据库本身就不支持事务。
-
抛出了非 RuntimeException
失效原因,@Trancatetional 默认只会帮我们捕获 RuntimeException 异常,捕获到了就进行回滚。<br /> 解决方法:使用 **rollbackFor** 属性来指定异常类型。例如 @Transactional(rollbackFor = Exception.class)
-
创建了一个新线程来执行事务方法。
失效原因:线程中抛出的异常在主线程中无法捕获,所以发生的异常无法进行回滚<br /> 解决方法:使用声明式事务
【Spring】框架中使用了哪些设计模式?
动态代理:AOP 和 @Trancatetion 事务使用了
单例模式:单例 Bean
工厂模式:BeanFactory 创建 Bean
【SpringMVC】执行流程
【SpringBoot】SPI 机制
【SpringBoot】SpringBoot启动流程
- 创建一个 SpringApplication 对象
- 调用对象的 run 方法。
- 加载 SpringBoot 的配置(yaml 文件和自动配置类)
- 创建应用上下文 ApplicationContext
- 执行命令行 Runner,Spring Boot提供了CommandLineRunner 和 **ApplicationRunner **两个接口,如果用户实现了这两个接口,Spring Boot启动时会调用这两个接口的run方法。
- 启动内嵌的 Servlet 容器。
- 应用启动完成。
@SpringBootApplication
public class MyApplication implements CommandLineRunner {public static void main(String[] args) {SpringApplication.run(MyApplication.class, args);}@Overridepublic void run(String... args) throws Exception {// 在这里编写你的命令行任务代码System.out.println("Hello, World!");}
}
【SpringBoot】SpringBoot自动配置
SpringBoot 自动配置是通过 @Import 注解和 Spring.factories 文件实现的。
- 在 SpringBoot 启动的时候,会先加载 @SpringBootApplication 注解,这个注解包含了 @SpringBootConfiguration、@EnableAutoConfiguration 和 @ComponentScan 三个注解
- 对于@EnableAutoConfiguration 注解,其作用就是开启自动配置功能。在这个注解中有一个 @Import 注解,用于导入 AutoConfigurationImportSelector 类。
- AutoConfigurationImportSelector 类会读取所以的 META-INF/spring.factories 配置文件,并解析其中的org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置类
- 通过遍历所以的配置类,并使用 ConfigurationClassPostProcessor 去加载并解析配置类中的 @Bean 信息。如果条件满足,Spring 就会将这些类进行实例化并注册到容器中。
- 最后完成 Spring 容器初始化工作。
【SpringBoot】如何自定义 starter
SpringBoot - 自定义 Starter
【SpringCloud-Alibaba】整体架构
三、MyBatis
1、#{}
和 ${}
的区别
#{}
是预编译处理,会在SQL查询中以参数形式出现,即PreparedStatement方式。MyBatis会将SQL中的#{}替换为?来防止SQL注入。它可以用来传递基本类型,String类型,还有pojo类型的参数。
${}
是字符串替换,MyBatis会将 ${} 替换成变量的字符串表示形式,因为它是直接拼接在SQL语句中,所以有可能会有SQL注入的问题,通常不建议使用。它只能接收基本类型和String类型的参数。
2、 一级缓存和二级缓存
一级缓存:sqlSession 级别,默认开启。
二级缓存:namespace 级别,默认关闭。
3、Spring 整合了 MyBatis,一级缓存会失效吗?
会失效,因为一级缓存是 sqlSession 级别的,交给 Spring 容器管理后,会在执行 sql 时创建 sqlSession,sql 执行完成后会关闭 sqlSession。
四、数据库(MySQL、Redis)
【MySQL】执行引擎 InnoDB 和 MyISAM 的区别
InnoDB | MyISAM | |
---|---|---|
事务 | √ | × |
锁 | 行锁 | 表锁 |
外键约束 | √ | × |
表空间 | 较大 | 较小 |
【MySQL】什么是索引?以及优缺点
索引是一种排好序的数据结构,用于提高磁盘上数据的检索效率。在 MySQL 中 InnoDB 搜索引擎使用的是 B+ 树来实现索引。
优点:
- 通过 B+树的结构来存储数据,可以大大减少数据检索时的磁盘 IO 次数,从而提高数据查询的性能。
- B+树索引在进行范围查找时,只需要找到起始节点,然后基于叶子节点的链表结构往下读取,即可查询效率较高。
- 通过唯一索引约束,可以保证数据表中的每一行数据的唯一性。
缺点:
- 数据的增加、修改、删除,需要涉及好索引的维护,当数据量较大时,索引的维护会带来较大的性能开销。
- 一个表中允许存在一个聚簇索引和多个非聚簇索引,但是索引的数量不能太多,否则将造成索引维护的成本过高。
- 创建索引的时候,需要考虑到索引字段的分散性,如果字段的重复数据过多,创建索引反而会带来性能降低。
【MySQL】B 数和 B+ 树有什么区别?
先说说 B 树的特点:
- 节点是排好序的。
- 一个节点可以存多个元素,并且多个元素也是排好序的。
而 B+ 树拥有 B 树的特点,并且又进行了一些改进。
- 叶子节点之间有指针。
- 非叶子节点上的元素在叶子节点上都有冗余了。也就是叶子节点上存储了所有元素,并且是排好序的。
【MySQL】什么是索引覆盖?
查询的字段正好在索引中,不需要在通过回表查询数据,查询效率高。
【MySQL】聚簇索引和非聚簇索引
聚簇索引:索引和数据行是放在一起的,InnoDB 引擎使用的就是聚簇索引,一个表中只有一个聚簇索引,主键索引就是聚簇索引,如果没有创建主键,表中会有一个隐藏的主键字段。
非聚簇索引:索引和数据行不是放在一起的。索引只存储索引字段和主键,如果要查询全部数据,需要回表。MyISAM 引擎使用的是非聚簇索引
【MySQL】什么是 MVCC ,以及 MVCC 原理
MVCC 全称为多版本并发控制(Multi-Version Concurrency Control),是为了解决并发数据一致性问题。
MVCC 在每个事务开始时创建数据的一个快照来实现并发控制,每个事务只能看到自己开始时的数据快照,而不能看到其他事务的修改。这样,每个事务都在自己的数据快照上进行操作,从而避免了直接的数据冲突。
【MySQL】怎么查看 SQL 的执行计划,并且说一下执行计划中的字段有什么含义?
查看执行计划: explain + 查询语句
- id:查询优先级,越小的越优先执行,相同的从上往下执行。
- select_type:查询类型
- table:查询的表
- partitions: 表示查询操作所涉及的分区表信息。
- type:查询类型
- possible_keys:可能用到的索引
- key:用到的索引
- key_len:用到 key 的长度
- ref:表示索引的连接列或常量。
- rows:表示MySQL估计需要扫描的行数。
- filtered:表示从table中筛选出的行所占百分比。
- Extra:表示额外的操作或信息,常见的值有Using index(使用了覆盖索引)、Using where(使用了WHERE条件)、Using temporary(使用了临时表),Using filesort(排序操作)等。
【MySQL】SQL 调优
查看 sql 的执行计划,type 字段可以看到是否是全表扫描,如果是全表扫描就需要优化,在通过 possible_key 和 key 字段来查看索引使用情况。如果 possible_key 没有信息,就需要创建索引了。如果 possible_key 有信息而 key 没有信息,就要看是不是索引失效了。
索引失效情况:
- 条件字段使用函数进行了处理。
- like 模糊匹配在匹配的字符串前面加了 %
- 数据类型不匹配,存在隐式转换。
- 使用 != 或 <> 操作符。
- 复合索引不按照索引最左前缀原则提供查询条件。
其他调优方法:
- 如果表的数据量太大了,可以进行分库分表。
- 使用 redis 缓存热点数据。
【MySQL】什么是事务?
事务是一组一起执行的SQL语句。如果所有SQL语句都成功执行,那么事务就成功提交,数据持久保存。如果一个SQL语句执行失败,那么事务就会回滚,所有在该事务中的SQL语句都不会影响到数据。
【MySQL】事务的 ACID 原则
事务有以下四个特点,通常被称为ACID特性:
- 原子性(Atomicity):事务中的所有操作,要么全部执行成功,要么全部不执行,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失。
- 原子性:事务中所有操作,要么都执行成功,要么都执行失败。(Undo Log 实现)
- 一致性:事务操作前后,数据是完整了,例如两个账户转账后的总金额不变。
- 隔离性:多个事务之间是独立的,互不影响。
- 持久性:事务一旦提交,所有更改就保存到数据库中了,数据不会丢失。(Redo Log 实现)
【MySQL】事务实现原理
【MySQL】事务隔离级别以及问题
MySQL 事务默认的隔离级别是可重复读(repeatable read),并且使用 MVCC 解决幻读问题。
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
Read uncommitted | √ | √ | √ |
read committed | × | √ | √ |
repeatable read | × | × | √ |
Serializable | × | × | × |
【MySQL】雪花算法做主键 id
雪花算法依赖与服务器的时间,如果时间回滚可能出现相同 id
【MySQL】如何优化深分页limit 1000000
- 使用覆盖索引:只查询索引字段,减少回表。
- 记住上一次查询的 id,查询时带上 id 条件进行过滤。
- 避免深度分页:在设计程序时,尽量避免深度分页。例如,可以提供搜索功能,让用户快速找到他们所需数据,而不是浏览大量的页面。
【Redis】Redis 数据类型有哪些,以及应用场景
数据类型 | 说明 | 应用场景 |
---|---|---|
String(字符串) | 存储字符串 | 1. incr 可以用于点赞统计 |
List(列表) | 底层是一个双向链表,容量为 2 的 32 次方减 1 个元素,大概 40 多亿。 | 1. 简单消息队列 |
Hash(哈希) | 底层的值为 key-value 键值对,简单说就是 Map<String, Map<Object, Object>> | 1. 用于存储对象信息 |
Set(集合) | 单值多 value,元素都是唯一的 | 1. 标签 2. 关注列表 |
ZSet(有序集合) | 在set基础上,每个 value 值前加一个score分值,之前set是k1 v1 v2 v3, 现在 Zset 是 k1 score1 V1 score2 V2 | 1. 排行榜 |
bitmap(位图) | 由 0 和 1 状态的二进制位的 bit 数组 | 1. 上下班打卡签到 2. 电影、广告是否被点击过 |
GEO(地理空间) | 用于地图 | 1. 查看附近的地点 |
HyperLogLog(基数统计) | 用于统计一个集合中不重复的元素个数 | 1. 统计 UV(独立访客)数 |
Stream(流) | Redis 版的 MQ 消息中间件+阻塞队列 | 1. 消息中间件 |
bitfield(位域) | 将一个Redis字符串看作是一个由二进制位组成的数组并能对变长位宽和任意没有字节对齐的指定整型位域进行寻址和修改 |
【Redis】Redis 持久化
Redis 持久化分为 RDB 和 AOF,
- RDB (Redis Database)将数据集内存快照写入到 RDB 文件中。比较适合备份和恢复的场景(默认开启)。
- AOF (Append only File )将 Redis 的写操作追加到 AOF 文件中。AOF 持久化时,会将写操作命令先写入到 AOF 缓冲区中,然后按照缓冲区写回策略写回到磁盘上。AOF 文件会越来越大,当超过配置的阈值之后会进行 AOF 重写,将操作同一个 key 的多条写命令合并成一个最终命令。比较适合数据安全性高和可读性强的场景。
【Redis】AOF 重写机制
AOF 重写配置有aof-rewrite-min-size 和 aof-rewrite-percentage,当AOF文件的大小超过配置的aof-rewrite-min-size选项(默认64MB),并且当前的AOF文件比上一次重写后的AOF文件的大小增长超过了配置的aof-rewrite-percentage选项所设置的百分比时,Redis就会自动触发AOF重写。
【Redis】AOF 缓冲区写回策略
Always | 同步写回,每一个写命令执行完立即同步的将日志写回磁盘。 |
---|---|
everysec(默认) | 每秒写回,每个写命令执行完,只是先把日志写入到 AOF 内存缓冲区,每隔 1 秒将缓冲区内容写入磁盘 |
no | 由操作系统控制写回。 |
【Redis】Redis 中哨兵机制,集群机制
Redis 实现高性能,高可用必须多台部署,有两种方法:
-
主从+哨兵
主机将数据同步写到从机中,从机只能读不能写,适合读写分离场景,主机写,从机读。哨兵用于监控主机状态,通过心跳来检查主机是否存活,如果主机挂掉将从从机中选取一台当做主机。哨兵最少部署三台一上,防止主观下线判断错误。当超过半数的哨兵都认为主机挂掉时,主机才是客观下线。
-
集群部署
集群至少有 3 台及以上个主机,数据是均匀的分布在每台主机上,通过 hash 槽实现,hash 槽一共有 16384 个,每一台主机映射一定数量的 hash 槽。
【Redis】Redis 为什么快
- 基于内存操作。
- 单线程避免了上下文切换带来的开销。
- 非阻塞 io 多路复用
- 良好的数据结构
【Redis】Redis 中 bigkey 问题
在 Redis 中 key-value 大小超过 10kb 或者 Hash、List、Set 和 ZSet 元素超过 5000 个,这个 key 就是一个 bigkey。
【Redis】缓存双写一致性问题
缓存双写一致性问题出在第 3 步。在多线程情况下,回写可能出现数据问题。可以采用双检加锁策略,代码如下:
【Redis】布隆过滤器
布隆过滤器是由一个初始值都为 0 的 bit 数组和多个哈希函数构成,用来快速判断集合中是否存在某个元素。判断存在误差,判断存在不一定有,但判断不存在就一定没有。
两个对象经过多个哈希函数运算:
【Redis】缓存击穿、缓存穿透、缓存雪崩
问题 | 描述 | 解决方案 |
---|---|---|
缓存击穿 | redis 中某一个热点 key 过期 导致大量的请求直接访问到数据库,造成数据库压力过大 | 1. 设置 key 永不过期 |
缓存穿透 | 查询的数据在 redis 中不存在,数据库中也不存在。 造成数据库频繁访问。 | 1. redis 缓存空的数据到 2. 使用布隆过滤器 |
缓存雪崩 | redis 中大量的 key 在同一时间失效 导致大量请求直接到达数据库。 | 1. 将过期时间加上一个随机数。 |
【Redis】清除内存的方式
- 立即删除:时间换空间
- 惰性删除:空间换时间
- 定期删除:立即删除和惰性删除折中,随机删除过期的 key,还是存在过期 key 一直没有被删除情况,需要兜底方案也就是缓存淘汰策略
【Redis】缓存淘汰策略
LRU(Least Recently Used)最近最少使用(时间维度)
LFU(Least Frequently Used)最不频繁使用(频率维度)
【Redis】事务支持是如何实现的?有哪些事务命令?
- Redis的事务通过MULTI、EXEC、WATCH和UNWATCH命令来实现。
- MULTI命令用于开启一个事务;EXEC命令用于执行事务中的命令;WATCH和UNWATCH命令用于对某个键进行监控,当该键的值发生变化时,事务会被取消。
五、Linux、Nginx、Docker
【Liunx】Linux 基础命令
cd
pwd
ps
mv
mkdir
cp
vim
cat
rm
netstat
top
…
【Liunx】vim 命令
一般模式
默认模式,在这个模式中, 你可以使用『上下左右』按键来移动光标,你可以使用『删除字符』或『删除整行』来处理档案内容, 也可以使用『复制、贴上』来处理你的文件数据。
1.删除和复制操作
2.光标移动操作
编辑模式
在一般模式中可以进行删除、复制、贴上等等的动作,但是却无法编辑文件内容的!要等到你按下『i, I, o, O, a, A, r, R』等任何一个字母之后才会进入编辑模式。注意了!通常在 Linux 中,按下这些按键时,在画面的左下方会出现『INSERT 或 REPLACE 』的字样,此时才可以进行编辑。而如果要回到一般模式时, 则必须要按下『Esc』这个按键即可退出编辑模式。
命令模式
可以进行存盘、退出、显示行号、搜索、批量替换等操作。要在一般模式下输入" / “或 " : " 或者”?"可进入命令模式,当命令执行前按esc或者命令执行完毕,自动回到一般模式
【Linux】less 查看日志
操作 | 功能说明 |
---|---|
空白键 | 向下翻动一页; |
[pagedown] | 向下翻动一页 |
[pageup] | 向上翻动一页; |
/字串 | 向下搜寻『字串』的功能;n:向下查找;N:向上查找; |
?字串 | 向上搜寻『字串』的功能;n:向上查找;N:向下查找; |
q | 离开 less 这个程序; |
【Nginx】nginx 为什么可以做到稳定可靠?
nginx 有一个守护线程,当引用挂掉了可以重新拉起来
nginx是一个高性能的HTTP和反向代理服务器,设计的目标就是为了处理大量的并发连接。它有以下几个方面保证了其稳定可靠:
1. 高并发处理能力:nginx 使用异步非阻塞I/O模型,可以处理大量并发连接,相比传统的web服务器,它的CPU、内存等资源消耗较低,因此更加稳定。
2. 高可靠性:nginx不会因为某个请求出现错误而影响到其他请求,每个请求都是独立处理的。即使某个请求处理失败,也不会影响到其他用户的使用。
3. 轻量级:nginx的安装和配置都非常简单,代码量也较小,更易于维护和管理,从而提高了其稳定性。
4. 强大的负载均衡和反向代理能力:nginx可以将请求分发到多个后端服务器,从而提高了可用性和容错性。
六、设计模式(单例模式、工厂模式、和代理模式)
1、 单例模式
分为懒汉式和饿汉
2、工厂模式
分为简单工厂模式、工厂方法模式 和 抽象工厂模式
简单工厂模式
工厂方法模式
抽象工厂模式
3、代理模式
代理模式分为,静态代理和动态代理, 动态代理有 JDK动态代理(代理的是接口), CGLib动态代理(代理的是类)
静态代理
JDK动态代理(代理的是接口)
CGLib动态代理(代理的是类)
七、JVM 虚拟机
1、JDK、JRE 和 JVM 是什么?
JDK java 开发工具包,JRE java 运行环境,JVM java 虚拟机
2、JVM 是如何实现跨平台的?
JVM 在不同系统平台上有对应的 JVM 版本,编译后的字节码可以在这些 JVM 上运行。
3、JVM 内存模型
4、 什么是双亲委派机制?
类加载器不断向上层委派,优先让上层类加载器加载。如果没有加载到在由该类加载器加载。
4、怎么标记对象是垃圾?
**引用计数法**
: 创建后的每一个对象都有与之对应的**计数器 **,当对象被引用时计数器 +1 ,引用完成时计数器 -1,当计数器为 0 时表 示对象没有被引用,就可以识别出垃圾对象。
问题:当两个对象相互引用时,会出现循环引用,对象就都不会被回收。
**可达性算法**
: 通过一些列 GC ROOT 作为起点开始向下遍历,形成一条条引用链,如果对象不在引用链上,表示对象没有被引用,就是可以回收的垃圾
GC Roots是一组引用,包括:
线程中虚拟机栈中正在执行的方法中方法参数、局部变量所对应的对象引用
线程中本地方法栈中正在执行的方法中方法参数、局部变量所对应的对象引用
方法区中保存的类信息中静态属性所对应的对象引用
方法区中保存的类信息中常量属性所对应的对象引用
等等
5、JVM 垃圾回收算法有哪些?
- 标记-清除
- 标记-整理
- 复制
6、JVM 垃圾回收机制是怎么样的?
6、 什么是分代算法?
分代算法不是具体的算法,而是一种理念。
新生代使用复制算法,老年代使用标记清除算法和标记整理算法。
7、 垃圾回收器有哪些,Java8 默认使用的是哪个?
八、ElasticSearch
1、数据类型有哪些
2、Text 和 keyword 的区别
它们都是存储的字符串,Text 会进行分词,而 keyword 不会。Text 不支持聚合查询,而 keyword 支持。
3、倒排索引
倒排索引的工作方式:
一、我们有两个文档:
- 文档1:Hello World
- 文档2:Hello Elasticsearch
二、分词后的结果是:
- 文档1:Hello, World
- 文档2:Hello, Elasticsearch
三、建立倒排索引的词典(Term Dictionary):
- Hello:文档1,文档2
- World:文档1
- Elasticsearch:文档2
四、当我们搜索"Hello"时:
- Elasticsearch会查找词典中的"Hello"词条,发现它对应的文档列表是:文档1,文档2。
- 因此,搜索结果会返回这两个文档。
你可以在网上搜索“Elasticsearch 倒排索引”来查找具体的图示,这样可能更直观一些。
4、es 深度分页是怎么样的
在通常情况下,我们使用的是从偏移(offset)开始返回一定数量(size)的搜索结果进行分页。当我们要获取大量数据,或者进行深度分页(比如获取第10000页的数据)时,这种方式效率低下,且可能导致内存溢出。
ES的深度分页使用了滚动(scroll)的机制。滚动查询可以获取大量数据,而不需要一次性将这些数据加载到内存中。当执行滚动查询时,ES会创建一个快照,保留查询结果的状态,然后为这个快照分配一个唯一的滚动ID,以后的查询可以通过这个ID得到下一批的数据。
滚动查询的过程大致如下:
1. 执行初始的滚动查询,获取第一批数据和滚动ID。
2. 使用滚动ID执行后续查询,获取下一批数据,并更新滚动ID。
3. 重复步骤2,直到所有数据都被检索完。
4. 清除滚动查询。
这种方式可以有效地处理大量数据的查询,避免内存溢出,但是数据返回的顺序可能与
九、RocketMQ
1、MQ 有什么用,有哪些具体应用场景?
MQ 主要有三个作用,异步、解耦、削峰
异步:
- 可以提高系统的相应速度和吞吐量。
解耦:
- 服务之间进行解耦,可以减少服务之间的影响,提高系统的稳定性和可扩展性。
- 解耦之后可以实现数据分发。生产者发送一个消息后,可以由多个消费者来处理。
削峰:
- 应对突发流量冲击
2、怎么保证消息不丢失?
三个地方存在数据丢失:
1、生产者发送消息
2、mq 主从同步以及刷盘
3、消费者接受消息
生产者发送消息
- 消费发送+回调
- 发送事务消息
存盘
同步刷盘信息安全性高,不会丢失数据,但是效率低。异步刷盘效率更高,但是存在数据丢失
3、如何保证消息的幂等性?
MQ 没有提供幂等性解决机制,需要有消费者自行控制。
可以有唯一的 ID 来控制,
4、如何保证消息的顺序?
MQ 只需要保证局部有序,不需要保证全局有序。
RocketMQ 可以使用顺序消息。顺序消息需要设置消息分组,同一个消息分组中的消息将被投递到同一个消息队列中。消费者进行消费时就是顺序的。
5、使用 MQ 如何保证分布式事务的最终一致性
生产者要保证 100% 的消息投递,事务消息机制
消费者需要保证幂等消费。唯一 ID
6、如何防止消息堆积?
- 配置合理的消费者数量:确保消费者的数量能够处理消息的流量,避免因为消费者数量不足而导致消息堆积。
- 调整消息生产者的发送速率:如果消息生产者发送消息的速率过快,而消费者的处理速度跟不上,就会导致消息堆积。因此,根据消费者的处理能力,合理调整消息生产者的发送速率,避免消息积压。
- 动态扩容消费者:当消息堆积的问题出现时,可以根据实际需求动态增加消费者,增加消息的消费能力,从而减轻消息堆积的压力。
十、 Zookeeper 分布式协调框架
1、zk 实现分布式锁
- 在ZooKeeper上创建一个持久化的目录作为根节点,用于存储所有的锁节点。例如,创建一个名为/locks的目录。
- 当节点需要获取锁时,创建一个有序临时节点(例如:/locks/lock-0001),并在节点上注册一个Watcher监听。
- 节点获取锁的方式是判断自己创建的节点是否是当前/locks目录下最小的节点,如果是,则获取到锁;否则,等待。
- 当其他节点释放锁时,ZooKeeper会通知等待队列中的节点,节点再次判断自己的节点是否是当前/locks目录下最小的节点,如果是,则获取到锁。
- 节点完成对共享资源的访问后,删除自己创建的临时节点,释放锁,其他节点会收到ZooKeeper的通知,进行竞争。