# 问题手册:
现在开始做最完整的总结。
# 一:JAVA 基础相关
-
Object 的九大方法.
九大方法分别是:
clone()
equals()
finalize()
getClass()
hashCode()
notify()
notifyAll()
toString()
wait()
-
重载与重写的区别?以及构造器是否能被
override
?-
重写是子类和父类之间的关系,是垂直关系,而重载是同一个类中方法之间的关系,是水平关系。
重写要求参数列表相同,而重载要求参数列表不同。
-
构造器不能被继承,方法也不能被重写,但是在同一个类中,构造器可以被重载。
-
-
String StringBuffer
和String StringBuilder
的区别,以及。String
为什么是不可变的,final
关键字的原理(深入内存模型理解)?-
String Buffer
与String Builder
的区别?-
区别 1:线程安全
StringBuffer
是线程安全,StringBuilder
是线程不安全的,StringBuffer
所有公开的方法都是通过synchronized
关键字修饰。 -
区别 2:缓冲区
StringBuffer
每次获取toString
都会直接使用缓存区的toStringCache
值来构造一个字符串。StringBuilder
每次都需要复制一个字符数组,再来构造一个字符串。 -
区别 3:性能
String Builder
由于不是同步的,没有加锁的操作,所以性能高于String Buffer
。
-
-
String
为什么是不可变的?通过阅读
String
的源代码,可以发现其在类上和成员变量上使用final
关键字修饰,而一旦将引用声明为final
,就不能改变这个引用,即被final
修饰的变量只能被赋值一次。-
修饰类、方法
final
修饰方法
把方法锁定,以防止任何继承类修改它的含义,不能被重写。
final
方法比非final
方法速度快,在编译的时候就已经静态绑定了。final
修饰类
表明类不能被继承。
final
类中的成员变量可以根据需要设为final
,但是final
类中的成员方法都会被隐式地指定为final
方法。 -
final
的底层原理?对于
final
域,编译器和处理器需要遵守两个重排序规则。-
在构造函数内对一个
final
域的写入,与随后把这个被构造对象的引用值给一个引用变量,这两个操作之间不能重排序。(先写入
final
变量,后调用该对象引用)原因:编译器会在
final
域的写之后,插入一个StoreStore
屏障 -
初次读一个包含
final
域的对象的引用,与随后初次读这个final
域,这两个操作之间不能重排序。(先读对象的引用,后读
final
变量)原因:编译器会在读
final
域操作的前面插入一个LoadLoad
屏障
-
-
-
-
接口和抽象类的区别?
- 接口不能有方法的实现,抽象类可以有方法体。
- 接口中的成员变量默认为
public static final
。而抽象类中的成员变量默认为default
. - 接口中的所有的方法都是
public、abstract
的,而抽象类中的方法可以在子类中被重新定义与复制,抽象方法被abstract
修饰。 - 一个类可以通过
implements
实现多个接口,但一个类只能只能通过extends
实现一个抽象类。接口强调特定功能的实现,而抽象类强调所属关系。
-
==
与equals
比较?- 第一种情况没有重写
equals
方法时,==
比较的是对象的地址,以及基本数据类型的值。equals
方法与 == 作用相同。 - 第二种情况,重写
equals
方法后,equals
方法比较的是对象的值。
- 第一种情况没有重写
-
hashCode
与equals
,为什么重写equals
时,必须重写hashCode
方法?- 相同的对象必须有相同的哈希值,不同的对象,哈希值不一定不同。
- 以
hashSet
为例,将对象加入HashSet
时,HashSet
会先计算对象的HashCode
来判断对象加入的位置,同时也会与该位置的已存在对象的hashCode
值作比较,如果没有相同的hashCode
,HashSet
会假设对象没有重复出现,如果有相同值的Hash Code
,这时会调用equals
方法来检查hashCode
相同的对象是否相同,如果两者相同,hashSet
加不会让其操作成功,如果不同的会啊,就会重新散列到其他的位置,这样就可以减少比较的次数,提高执行速度。
总结:根据规定,相同的对象就需要有相同的
hashCode
的值 当
hashCode
相同时,对象不一定相同,当equals
为true
是,hashCode
一定相同。 -
浅拷贝与深拷贝?
浅拷贝拷贝的是对象的引用,而深拷贝拷贝的是对象本身。
-
BIO、NIO、AIO
有什么区别?-
BIO:(Bolking I/O)
同步阻塞I/O
模式,数据的读入与写入阻塞在同一个线程内完成,在活动连接数不是特别高的情况下,这种模型的效果不错,但是面对十万甚至百万连接时,传统的BIO
模式就无能为力了。 -
NIO:(New I/O)
同步非阻塞I/O
模型,是一种基于通道技术的I/O
操作方法,对于高负载、高并发的应用,可以使用NIO
来开发。
-
1. 多路复用:
多路复用采用的是操作系统底层的模型,``select poll epoll``模型,相比于传统的``NIO``模式,多路复用调用底层的``select\poll\epoll``等操作,将轮询的过程交给操作系统,这样的好处是一个单线程就能调度很多``IO``请求业务.
2. ``Netty``:是由``JBOSS``提供的一个``Java``开源框架,是基于``NIO``的客户、服务器编程框架。``Netty``提供异步的、事件驱动的网络应用程序框架和工具,可以简化应用开发过程。
-
AIO:(Asynchronous I/O)````AIO
时NIO
的升级版,它是异步阻塞的I/O
模型,基于事件和回调机制实现,当应用操作结束后直接返回,不会阻塞在那里。但是该技术应用不是很广泛。 -
多路复用机制之
select poll epoll
之间的区别,以及使用场景.(待补充)-
select
的时间复杂度为O(n)
无差别轮询所有流,通过遍历
fdset(位图
)找,到有文件描述符 (fd) 就绪的,再进行后续操作。其中,文件描述符最大的数量一般为 1024 实际使用只有 1021 左右 . -
poll
的时间复杂度为O(n)
在 本质上与
select
是一样的操作,但是,poll
使用的是链表存储,所以就没有数量的限制. -
epoll
的时间复杂度为O(1)
通过在内存上开辟一块事件列表空间,用来管理存储的信息,这快空间的结构式红黑树的结构,通过
epoll_ctl
函数,将socket
信息注册到该空间中。还有一个是就绪列表空间,存储就绪信息,用epoll_wait
函数访问,将就绪列表中就绪的socket
接连,拷贝到用户态,唤醒用户态进程进行读取。有两种工作模式:
LT
水平触发:事件就绪后,用户可以选择处理或者不处理,如果用户本次不处理,那么下次调用epoll_wait
时仍然将未处理的事件打包给用户态。ET
边缘触发:事件就绪后,用户必须处理,ET 模式在很大程度上减少了epoll
事件被重复触发的次数,效率高于LT
。
-
-
序列化?
-
什么是对象序列化,反序列化。
对象序列化就是将对象以二进制的形式保存在硬盘上,而反序列化就是将二进制文件转化为对象读取。
-
在实现序列化接口的时候一般生成一个
serialVersionUID
,有什么作用。通过声明
serialVersionUID
的字段,显式声明自己类的serialVersionUID
。来标注类的序列号。 -
反射机制?
-
获取
Class
对象方式一:Class.forName()
; -
获取
Class
对象方式二:TargetClass.newInstance()
; -
静态编译与动态编译:静态编译在编译时确定对象,动态编译在运行时确定对象.
-
Spring
框架的IOC
以及AOP
功能都与反射有关系. -
设计模式的问题合集?
设计模式共有 23 种,分为三大类:创建型模式、结构型模式、行为型模式。这里就介绍主要的几种模式。
-
单例模式?
单例模式保证单例对象的类只有一个实例的存在。整个系统只有一个实例。优点:不会频繁的创建和销毁对象,浪费系统的资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122//在开始代码之前,需要知道静态方法的加载顺序,首先是静态代码块(随类的加载而执行)->构造器->静态方法调用.
//单例模式的代码实现
//饿汉模式
public class Singleton{
private static final Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
//单例模式的调用
public class Lesson7 {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
true
//单例模式的延迟加载代码
//懒汉模式
class SingletonLazy{
private static SingletonLazy instance = null;
private static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
}
//使用synchronized+方法来保证线程安全的单例模式延迟加载
//懒汉模式优化1,单锁
class SingletonLazy{
private static SingletonLazy instance = null;
private static synchronized SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
if(instance == null){
instance = new SingletonLazy();
}
}
return instance;
}
}
//使用synchronized+代码块实现线程安全的单例模式延迟加载
//懒汉模式优化2,单锁,双重校验
Class SingletonLazy{
private static SingletonLazy instance = null;
private staic SingletonLazy getInstance(){
if(instance == null){
synchronized(SingletonLazy.class){
if(instance == null){
instance = new SingletonLazy();
}
}
}
return instance;
}
}
//使用reentrantLock+代码块进行单例模式的实现
//相当于懒汉优化2,单锁,双重校验
Class SingletonLazy{
private static SingletonLazy instance = null;
Private static ReentrantLock lock = new ReentrantLock(false);
public static SingletonLazy instance(){
if(instance == null){
lock.lock();
try{
if(instance == null){
instance = new SingletonLazy();
}
} finally{
lock.unlock();
}
}
return instance;
}
}
//静态方法内部类方法实现
Class SingletonLazy{
private SingletonLazy(){};
private static class SingletonLAzy2{
private static final SingletonLazy instance = new SingletonLazy();
}
public static SingletonLazy getInstance(){
return SingletonLazy2.instance;
}
public static void mian(String[] args){
//
}
}
//枚举实现单例模式
public enum Persion1{
person1;
String name = new String("ssss");
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
public class test{
public static void main(String[] args){
Person1 person1 =Person1.person1;
Person1 person2=Person1.person1 ;
person1.setName("aaa");
person2.setName("bbb");
System.out.println(person1.getName());
System.out.println(person2.getName());
}
}
bbb bbb -
工厂模式?
工厂模式又称静态工厂方法模式,建立一个工厂,把实现了同一接口的一些类进行实例的创建。例如饮料机可以理解为工厂模式,你需要的饮料如咖啡,奶茶,可乐,他就会按你的要求进行生产。
优点:工厂类含有必要的逻辑判断,可以决定在什么时候创建哪一个产品类的实例。
缺点:不易扩展,一旦添加新的类型就需要修改类的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//工厂模式的代码实现
class Factory {
public static String createProduct(String product) {
String result = null;
switch (product) {
case "Mocca":
result = "摩卡";
break;
case "Latte":
result = "拿铁";
break;
default:
result = "其他";
break;
}
return result;
}
}- 抽象工厂模式?
抽象工厂模式是基于简单的工厂模式,将未来需要修改的代码抽象出来,通过继承的方式让子类去做决定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49public class AbstractFactoryTest {
public static void main(String[] args) {
// 通过抽象工厂的方式创建实例
String result = (new CoffeeFactory()).createProduct("Latte");
System.out.println(result); // output:拿铁
}
}
// 抽象工厂(相当于一个模板,一个集团公司的入口)
abstract class AbstractFactory{
public abstract String createProduct(String product);
}
// 啤酒工厂(子公司)
class BeerFactory extends AbstractFactory{
public String createProduct(String product) {
String result = null;
switch (product) {
case "Hans":
result = "汉斯";
break;
case "Yanjing":
result = "燕京";
break;
default:
result = "其他啤酒";
break;
}
return result;
}
}
//咖啡工厂(子公司)
class CoffeeFactory extends AbstractFactory{
public String createProduct(String product) {
String result = null;
switch (product) {
case "Mocca":
result = "摩卡";
break;
case "Latte":
result = "拿铁";
break;
default:
result = "其他咖啡";
break;
}
return result;
}
}- 观察者模式:观察者模式是定义对象间的一种一对多依赖关系,是的每当一个对象状态发生改变时,其相关依赖对象皆得到通知并自动更新。(
Tomcat
)
优点:观察者模式可以实现表示层与数据层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,是的可以有各种各样不同 的表示层作为具体观察者角色。
缺点:如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
如果在观察者和观察目标之间有循环依赖的话,观察目标会触发他们之间进行循环调用,导致系统奔溃。
- 装饰器模式:指动态地给一个对象增加一些额外的功能,同时又不改变其结构。(I/O 流)
- 代理模式:给某一个对象提供一个 dialing,并由代理对象控制原对象的引用。(AOP)
- 适配器模式:将一个类的接口变成客户端所期望的另一个接口,从而使原本接口不匹配而无法一起工作的两个类能够在一起工作。(I/O 流)
- 原型模式:用于创建重复对象,同时又能保证性能。当创建对象的代价比较大时,可以采用这种模式(Clone)。
-
# 二:JVM 相关
-
内存区域?
根据 JVM 规范,定义了
JVM
内存的 5 个部分,虚拟机栈、本地方法栈、程序计数器、方法区、堆。这五个区域也称为运行时数据区。其中,程序计数器是唯一一个不会出现内存溢出的情况,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
-
在
JDK1.8
之前,线程共享的是:堆、方法区、直接内存。而线程私有的是:虚拟机栈、本地方法栈、程序计数器。- 1.8 之后,方法区由元空间来实现,存储
.class
信息、类的信息、方法的定义、静态变量等,而常量池移到了堆中。
- 1.8 之后,方法区由元空间来实现,存储
-
堆与栈的区别?以及堆的结构。
Java
虚拟机中的堆结构。
在JDK1.8之前,Java虚拟机将内存划分为新生代、老年代和永久代,永久代是HotSpot虚拟机特有的概念,其实是方法区的实现,只不过方法区的叫法是JVM规范中定义的。在JDK1.8中,使用元空间(``metaspace``)代替了永久代。
1. 新生代
``HotSpot``虚拟机将新生代分为三块,一块是较大的``Eden``空间和两块较小的``Survivor``空间,默认比例为``8:1:1``。当``Eden``区内存空间不够时,虚拟机将发起一次``Minor GC``。
``HotSpot``新生代的垃圾回收算法采用的是复制算法。``GC``开始时,对象只会存在``Eden``区和``From Survivor``区,``To Survivor``区是空的。``GC``进行时,``Eden``区中所有存活的对象,会被复制到``To Survivor``区,而在``From Survivor``区中,根据存活对象的年龄来决定去留,当对象的年龄达到年龄阈值,(默认为``15``,每熬过一次``minor GC``,存活对象的年龄就会``+1``),就会被移到老年代中,没有达到阈值的对象就复制到``To Survivor``区中。接着清空``Eden``区和``From Survivor``区。接着,调换``From Survivor``区和``To Survivor``区的角色。若``To Survivor``区存放不下所有的存活对象,则会将剩下的对象放在老年代中。
2. 老年代
在新生代中经历了多次``GC``后任然存活下来的对象会进入老年代中,老年代的对象生命周期较长,存活率比较高,采用``Full GC``(标记整理算法)来回收对象。
3. 永久代
永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,其垃圾回收是与老年代及进行绑定,无论谁的内存满了,都会进行垃圾回收。
4. 元空间
``JDK1.8``之后的产物,存储类信息、静态变量、常量、编译后的代码等。
``Metaspace``的内存管理:
与永久代不同,在``metaspace``中,类和其元素的生命周期与其对应的类加载器相同,只要类的加载器是存活的,``Metaspace``中的类元素也是存活的,不能被回收。当``GC``发现某个类加载器不再存活,就会把对应的整个空间回收。
-
Java
虚拟机中堆与栈的主要区别?- 存储:栈存储的是变量、对象的引用;而堆存储的是实例对象。
- 速度:栈的存取速度快;堆的存储速度慢。
- 数据结构:栈的数据结构就是栈,而堆的数据结构是一颗完全二叉树。
-
对象创建的过程?
-
对象创建过程分为 5 个步骤。类加载检查 -> 分配内存 -> 初始化零值 -> 设置对象头 -> 执行
init
方法。-
类加载检查:虚拟机遇到一条
new
指令时,首先检查这个指令的参数是否在常量池中定位到这个类的引用,并且检查这个引用代表的类是否已经被加载过、解析过、初始化过,如果没有,那必须先执行相应的类加载过程。 -
分配内存:在类加载检查之后,接下来虚拟机将为新生对象分配内存,为对象分配空间的任务等同于把一块确定大小的内存从
java
堆中划分出来。分配的方式有两种:指针碰撞和空闲列表,选择哪种方式有Java
堆是否规整决定,而Java
堆时否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。-
指针碰撞:适用场合:堆内存规整的情况下。
-
空闲列表:使用场合:堆内存不规整的情况下。
-
内存分配并发问题
在创建对象的时候有一个很重要的问题,就是线程安全,虚拟机采用两种方式来保证线程安全:
-
CAS
+ 失败重试:CAS
是乐观锁的一种实现方式。虚拟机采用CAS
配上失败重试的方式保证更新操作的原子性。 -
TLAB
: 为每一个线程预先在Eden
区分配一块儿内存,JVM
在给线程中的对象分配内存时,首先在TLAB
分配,当对象大于TLAB
中的剩余内存或TLAB
的内存已用尽时,再采用上述的CAS
进行内存分配。
-
-
-
初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在
Java
代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 -
设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的
GC
分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 -
执行
init
方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以一般来说,执行new
指令之后会接着执行<init>
方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
-
-
-
对象访问定位的两种方式?
- 句柄:如果使用句柄的话,那么
Java
堆中将会划分出一块内存来作为句柄池,reference
中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; - 直接指针: 如果使用直接指针访问,那么
Java
堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference
中存储的直接就是对象的地址。
- 句柄:如果使用句柄的话,那么
-
如何判断对象是否死亡?
- 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象
objA
和objB
相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知GC
回收器回收他们。 - 可达性分析法:这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到
GC Roots
没有任何引用链相连的话,则证明此对象是不可用的。
- 引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象
-
强引用、软引用、弱引用、虚引用?
Reference 源码:
软引用 \ 弱引用 \ 虚引用都继承自 java.lang.ref.Reference
, 是直接子类。但引用对象是与垃圾收集器紧密协作而实现的,所以不能自定义引用类型.
Reference
有四种状态:
Active:
引用刚被创建时的状态,如果 GC 检测到该实例引用的对象的可达性发生了改变,状态则会变成 Pending
或者 Enqueued
. 如果, reference
在创建引用时注册了引用队列,则会进入 Pending
的状态,并将 Reference
加入到 pending-Reference
链表中,如果没有,则会进入 Enqueued
的状态.
Pending
: 当引用实例被放置在 pending-Reference
链表中时,该实例等待一个叫 Reference-handler
的线程进行 enqueued
操作.
Enqueued
: 当 Reference
从队列中移除的时候,就会进入此状态.
Inactive
: 一旦某个 Reference
处于此状态,他的状态将不会改变,同时说明该引用实例所执行放入实际对象一定会被 GC
回收.
-
强引用
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,
Java
虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。 -
软引用(
SoftReference
)如果一个对象只具有软引用,那就类似于可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(
ReferenceQueue
)联合使用,如果软引用所引用的对象被垃圾回收,JAVA
虚拟机就会把这个软引用加入到与之关联的引用队列中。 -
弱引用(
WeakReference
)如果一个对象只具有弱引用,那就类似于可有可无的生活用品。 不会活过下个垃圾回收。引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
实例:
weakHashMap
, 触发 GC 之后,就没有了值. -
虚引用(PhantomReference)
"虚引用" 顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用不能单独使用,也不能通过它访问对象,必须和引用队列 (
RefenceQueue
) 联合使用.虚引用的主要作用是跟踪对象垃圾回收的状态,设置虚引用的唯一目的就是在这个对象被回收器回收的时候收到一个系统通知或者后续添加进一步的处理.
-
引用队列
(ReferenceQueue)
用来配合引用工作的,没有引用队列一样可以运行。创建引用的时候,可以指定关联的队列,当
GC
释放对象内存的时候,将引用加入到引用队列中,如果发现某个虚引用已经被加入到引用队列中,那么就可以在所引用的对象的内存被回收之前采取必要的行动,这相当于一种通知机制.
-
-
垃圾收集算法。
-
标记清除
该算法分为 “标记” 和 “清除” 阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
缺点:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
-
-
复制算法
解决效率问题,将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
-
标记整理
标记过程仍然与 “标记 - 清除” 算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
-
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,根据对象存活周期的不同将内存分为几块。一般将
java
堆分为新生代和老年代,根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,选择复制算法。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择 “标记 - 清除” 或 “标记 - 整理” 算法进行垃圾收集。 -
垃圾收集器?
-
Serial
收集器:Serial(串行)
收集器。是一个单线程收集器了。使用一条垃圾收集线程去完成垃圾收集工作,该线程进行垃圾收集工作的时候必须暂停其他所有的工作线程("Stop The World"
),直到收集结束。新生代采用复制算法,老年代采用标记 - 整理算法。
-
``Serial`` 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
-
PerNew
收集器:ParNew
收集器其实就是Serial
收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial
收集器完全一样。新生代采用复制算法,老年代采用标记 - 整理算法。
并行和并发概念补充:- 并行(
Parallel
) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 - 并发(
Concurrent
):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个CPU
上。
- 并行(
-
CMS
收集器:CMS(Concurrent Mark Sweep)
收集器是 第一款真正意义上的并发收集器,实现了让垃圾收集线程与用户线程同时工作。CMS
收集器是一种 “标记 - 清除” 算法实现的。整个过程分为四个步骤:- 初始标记: 暂停所有的其他线程,并记录下直接与
root
相连的对象,速度很快 ; - 并发标记: 同时开启
GC
和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC
线程无法保证可达性分析的实时性。 - 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
- 并发清除: 开启用户线程,同时
GC
线程开始对为标记的区域做清扫。
- 初始标记: 暂停所有的其他线程,并记录下直接与
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:**并发收集、低停顿**。但是它有下面三个明显的缺点:
- **对 CPU 资源敏感;**
- **无法处理浮动垃圾;**
- **它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。**
-
G1
收集器:G1 (Garbage-First)
是一款面向服务器的垃圾收集器。它具备一下特点:
-
并行与并发:
G1
能充分利用CPU
、多核环境下的硬件优势,使用多个CPU
来缩短Stop-The-World
停顿时间。部分其他收集器原本需要停顿Java
线程执行的GC
动作,G1
收集器仍然可以通过并发的方式让java
程序继续执行。 -
分代收集:虽然
G1
可以不需要其他收集器配合就能独立管理整个GC
堆,但是还是保留了分代的概念。在内存空间上,G1
将内存划分为很多小块,每一个小块可以被标记为O\S\E
种的一个。从而使得垃圾回收变成更彻底。
-
-
可预测的停顿:这是
G1
相对于CMS
的另一个大优势,G1
除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。实现原理:
G1
的一个显著特点是他能够让用户设置应用的暂停时间,G1
收集器回收的第四步是选择一个内存来回收,而不是整代内存,这使得回收内存所需时间就取决于内存大小,以及实际垃圾的多少。所以,到底回收多少垃圾就取决于用户配置的暂停时间,时间多一点就多回收一些垃圾,实现伸缩的自如。G1
收集器的运作大致分为以下几个步骤:- 初始标记:在垃圾回收的最开始有一个短暂的时间段会停止应用。
- 并发标记:继续运行程序,同时
G1
开始标记。 - 最终标记:再次停止应用来做最后的标记。
- 筛选回收:根据
Garbage First
的原则,选择一个内存块进回收。
G1
收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region
。这种使用Region
划分内存空间以及有优先级的区域回收方式,保证了G1
收集器在有限时间内可以尽可能高的收集效率。
-
类加载过程与类加载器?
-
类的生命周期:加载 -> 连接 -> 初始化 -> 使用 -> 卸载。连接过程又分为:验证 -> 准备 -> 解析。
-
类加载器:
JVM
中内置了三个重要的ClassLoader
,除了BootstrapClassLoader
其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:-
BootstrapClassLoader
(启动类加载器) :最顶层的加载类,由 C++ 实现,负责加载%JAVA_HOME%/lib
目录下的 jar 包和类或者或被-Xbootclasspath
参数指定的路径中的所有类。 -
ExtensionClassLoader
(扩展类加载器) :主要负责加载目录%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 -
AppClassLoader
(应用程序类加载器) : 面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
-
-
Thread.currentThread.getContextLoader()
(上下文类加载器)。Java
提供了很多服务提供者接口(Service Provider Interface SPI
), 允许第三方为这些接口提供实现,例如JDBC、JNDI、JBI
等。线程上下文类加载器,可以打破双亲委派机制,实现逆向调用类加载器来加载当前线程中类加载其加载不到的类。
-
双亲委派:
# 双亲委派模型介绍
每一个类都有一个对应它的类加载器。系统中的
ClassLoder
在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求委派该父类加载器的loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器BootstrapClassLoader
中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null
时,会使用启动类加载器BootstrapClassLoader
作为父类加载器。
-
-
打破双亲委派机制
-
为了避免双亲委托机制,我们可以自己定义一个类加载器,然后重写
loadClass()
即可。 -
tomcat
中的打破双亲委派机制:先加载JVM
的类 ->tomcat
的类 -> 自己定义的类 -> 应用程序的类 -> 公共的类。顺序:1 使用
bootstrap
引导类加载器加载 (JVM
的东西 ) 2 使用
system
系统类加载器加载 (tomcat
的启动类Bootstrap
包) 3 使用
WebAppClassLoader
加载WEB-INF/classes
(应用自定义的class
) 4 使用
WebAppClassLoader
加载在WEB-INF/lib
(应用的依赖包) 5 使用
common
类加载器在CATALINA_HOME/lib
中加载 (tomcat
的依赖包,公共的,被各个应用共享的)
-
-
Minor GC
与Full FC?
Minor GC
用于新生代的垃圾回收,会有短暂的暂停,每一次minor GC
都会增加对象的生命,达到某个阈值进入老年代。而Full GC
用于回收大对象,用于老年代的回收。 -
GC
调优策略?策略 1: 将新对象预留在新生代,由于
Full GC
的成本远高于Minor GC
,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据GC
日志分析新生代空间大小分配是否合理,适当通过 “-Xmn
” 命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。** 策略 2:** 大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收来说简直就是噩梦)。
-XX:PretenureSizeThreshold
可以设置直接进入老年代的对象大小。** 策略 3:** 合理设置进入老年代对象的年龄,
-XX:MaxTenuringThreshold
设置对象进入老年代的年龄大小,减少老年代的内存占用,降低full gc
发生的频率。** 策略 4:** 设置稳定的堆大小,堆大小设置有两个参数:
-Xms
初始化堆大小,-Xmx
最大堆大小。** 策略 5:** 注意: 如果满足下面的指标,则一般不需要进行
GC
优化:MinorGC
执行时间不到50ms
;Minor GC
执行不频繁,约 10 秒一次;Full GC
执行时间不到1s
;Full GC
执行频率不算频繁,不低于 10 分钟 1 次。
# 三:集合 \ 容器
-
List,Set,Map
三者的区别?List
与Set
继承自Collection
, 基于List
实现的方法有ArrayList
-Vector
-LinkedList
; 基于Set
实现的方法有HashSet
-LinkedHashSet
-TreeSet
; 基于Map
实现的有HashMap
-LinkedHashMap
-HahTable
-TreeMap
;LIst
接口存储一组不唯一,有序的对象;Set
接口存储不允许重复的对象;Map
使用键值对存储.
-
ArrayList
与LinkedList
区别,以及ArrayLIst
与Vector
的区别?ArrayList
底层使用的时数组结构,LinkedList
底层使用的时双向链表数据结构.- 插入元素与删除元素时间复杂度受数组和链表的影响.
ArrayList
与LinkedList
是线程不安全的,而Vector
是线程安全的.
-
ArrayList
与HashMap
的扩容机制?1.
ArrayList
是使用懒加载的方式,每次扩容通过Arrays.copyOf(elementData, newCapacity)
来实现的,即将当前数组元素拷贝到新的数组中,每次扩容后的容量为原来容量的1.5
倍.
2.HashMap
底层采用的是数组+链表+红黑树
的数据结构来实现。扩容扩展的是数组的长度,无参构造时默认第一次扩容16单位
。阈值 =数组长度*负载因子
。默认的负载因子是0.75
, 例如,16X0.75 = 12
, 当元素个数超过12
时,容量扩容为原来的2
倍,当容量扩大至Integer.MAX_VALUE
时,容量设置为Integer.MAX_VALUE
, 且不在增长. -
HashMap
与HashTable
的区别,以及HashMap
与HashSet
的区别?- 线程是否安全:
HashMap
是非线程安全的,HashTable
是线程安全的,具体实现方式是通过synchronized
修饰. HashMap
中,null
值可以作为key
, 这压根的key
只能有一个,而值可以有多个或一个为null
.HashMap
与HashTable
的扩容机制不同以及初融容量不同,HashMap
初始容量是16
, 每次扩容为想在容量的两倍
,HashTable
的初始容量为11
, 每次扩容是现有容量的2n+1
.- 底层的数组结构,
HashMap
在链表长度大于 8 的时候会转为红黑树,而HashTable
没有这样的机制.
5.HashSet
基于HashMap
实现,实现的是Set
的接口.
- 线程是否安全:
-
HashMap
底层实现的原理?HashMap
底层是数组 + 链表 + 红黑树实现。为了减少Hash
碰撞,将数据尽量均匀分配,HashMap
的容量是2的幂次方
.Hash
值范围前后加起来大概有40亿
,直接使用,内存是放不下的,所以需要与数组下标 & 运算,这也就解释了为什么长度是2的幂次方
.
-
ConcurrentHashMap
和HashTable
的区别?-
在底层数据结构上,
ConcurrentHashMap
使用的是分段数组+链表+红黑树
的实现,HashTable
使用的是数组 + 链表. -
ConcurrentHashMap
与HashTable
的区别主要体现在实现线程安全的方式上不同。在JDK1.7
之前,ConcurrentHashMap
对整个数组进行了分割和分段,每一段有一把锁,在访问不同段里面的数据时就不会存在锁竞争。在JDK1.8
之后,ConcurrentHashMap
摒弃了数组分段的概念,直接是用数组+链表+红黑树
的结构,并发控制使用的是Synchronized
和CAS
来控制,看起来是优化过且线程安全的HashMap
.
-
-
红黑树
红黑树是一个自平衡的二叉查找树。但其与平衡二叉树
(ALV)
的区别在于平衡二叉树是严格的平衡,适用于插入和删除操作少,查询多的情况,而红黑树是一颗弱平衡二叉树,相同情况下,树的高度会高于平衡二叉树。-
特性:
- 每个节点或者是黑色或者是红色,且根节点是黑色,每个叶子节点(
NULL
空值节点)也是黑色。 - 如果一个节点是红色,则它的子节点必须是黑色。
- 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑色节点。
- 红黑树最长路径的长度不超过最短路径的两倍。
- 每个节点或者是黑色或者是红色,且根节点是黑色,每个叶子节点(
-
操作:
- 左旋
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
LEFT-ROTATE(T,x)
y <- right[x] //假设X节点的右孩子是y。
right[x] <- left[y] //将y节点的左孩子设为x节点的右孩子
P[left[y]] <- x //将x设置为y节点左孩子的父亲
p[y] <- p[x] //将x的父亲设置为y的父亲
if p[x] = nil[T] //情况1:如果x的父节点是空姐点,则将y设置为根节点
root[T] <- y
else if x = left[p[x]]
left[p[x]] <- y //情况2:如果x是其父节点的左孩子,则将y设置为其父节点的左孩子
else
right[p[x]] <- y //情况3:如果x是其父节点的右孩子,则将y设置为其父节点的额右孩子
left[y] <- x //将x设置为y的左孩子
p[x] <- y //将x节点的父亲设置为y
2. 右旋
1
2
3
4
5
6
7
8
9
10
11
12
Right-AOTATE(T,y)
x <- left[y] //设置x为y节点左孩子
left[y] <- right[x] //将x的右孩子设置为y的左孩子
p[right[x]] <- y //将y设置为x右孩子的父亲
p[x] <- p[y] //将y的父亲设为x的父亲
if p[y] = nil[T]
root[T] <- x //情况1,如果y的父亲是空节点,则将x设为根节点
else if y = right[p[y]]
then right[p[y]] ← x // 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的左孩子”
else left[p[y]] ← x // 情况3:(y是它父节点的左孩子) 将x设为“y的父节点的左孩子”
right[x] ← y // 将 “y” 设为 “x的右孩子”
p[y] ← x // 将 “y的父节点” 设为 “x”
3. 添加与删除节点操作
# 四:计算机网络
-
TCP/IP
各层的结构与功能,都有哪些协议?- 七层网络结构:物理层、数据链路层、网络层(
IP
)、传输层(TCP,UDP
)、会话层、表示层、应用层(HTTP,HTTPS,SMTP,DNS
)。
- 七层网络结构:物理层、数据链路层、网络层(
-
TCP
三次握手与四次握手-
三次握手的流程:
TCP
服务器进程先创建传输控制块,时刻准备着接受客户进程的连接请求,此时服务器就进入了LISTEN
状态。TCP
客户进程也是先创建传输控制块TCB
,然后像服务器发送连接请求报文,这时报文首部中的SYN=1
,同时产生一个初始序列号seq=x
,此时,TCP
客户端进程进入SYN-SENT
状态。TCP
服务器收到请求报文后,如果同意连接,则发送确认报文,这时确认报文中ACK=1,SYN=1
,确认号是ack=x+1
,同时自己也初始化一个序列号seq=y
,此时,TCP
服务器进程进入SYN-RCVD
状态。TCP
客户端进程收到确认后,还要向服务器给出确认,确认报文ACK=1,ack=1
,自己的序列号seq=x+1
,此时,TCP
连接建立,客户端进入ESTABLISHED
状态,- 当服务器收到客户端的确认后也进入
ESTABLISHED
状态,此后,双方就可以开始通信了。
-
TCP
四次握手流程:- 客户端进程发送连接释放报文,并且停止发送数据,释放数据报文首部,此时
FIN=1
,序列号为seq=u
,此时客户端进入FIN-WAIT-1
状态 - 服务器收到连接释放报文,发出确认报文,此时
ACK=1,ack=u+
,并且带上自己序列号seq=v
,此时服务器就进入CLOSED-WAIT
状态。 - 客户端收到服务器的确认请求后,客户端就进入
FIN-WAIT-2
状态,等待服务器发送释放连接的报文。 - 服务器将最后的数据发送完毕后,就像客户端发送连接释放报文,
FIN=1,ack=u+1
,此时的序列号为服务器就进入LAST-ACK
状态,等待客户端的确认。 - 客户端收到服务器的连接释放报文后,必须发送确认,
ACK=1,ack=w+1
,而自己的序列号是seq=u+1
,此时,客户端就进入了TIME-WAIT
状态。此时TCP
连接还没有释放,必须经过2*MSL(最长报文段寿命)
的时间后,当客户端撤销相应的TCB
后,才进入CLOSED
状态。 - 服务器只要收到客户端发送的确认,立即进入
CLOSED
状态,同样,撤销TCB
后,就结束了这次的TCP
连接。
- 客户端进程发送连接释放报文,并且停止发送数据,释放数据报文首部,此时
-
为什么客户端还要等待 2 个报文寿命时间?
- 第一:保证客户端发送的最后一个
ACK
报文能够到达服务器,因为这个ACK
报文可能丢失,而客户端就能在这个2MSL
时间内收到这个重传的报文,接着给出回应报,并且重计2MSL
时间。 - 防止类似与” 三次握手 “中提到的” 已经失效的连接请求报文段” 出现在本连接中。客户端发送完最后一个确认报文后,在这个
2MSL
时间中,就可以使本链接持续的时间内所产生的所有报文段都从网络中消失。
- 第一:保证客户端发送的最后一个
-
如果已经连接了连接,凡是客户端突然出现故障了怎么办?
TCP
还有一个保活计时器,如果客户端出现故障,服务器不能一直等下去。服务器每收到一次客户端的请求后都会重新复位这个计时器。时间一般为 2 小时,若两个小时都还没收到客户端的任何数据,服务器就会发送一个探测报文段,以后每个 75 秒发送一次,若连续发送 10 个探测报文段都没有反应,服务器就认为客户端出现了故障,接着关闭连接。
-
-
TCP
与UDP
的区别,以及使用场景?& TCP UDP 可靠性 可靠 不可靠 连接性 面向连接 连接 报文 面向字节流 面向报文 效率 效率低 效率高 双工性 一对一 一对多,多对一,多对多 拥塞控制 慢开始、拥塞避免、快重传 无 应用场景 对效率要求低 对传输效率要求高 常见使用场景 SMTP、HTTP、FTP
DNS
、视频连接 -
滑动窗口与流量控制、拥塞控制.
-
TCP
利用滑动窗口实现流量控制,流量控制是为了控制发送方发送速率,保证接收方来得及接受,接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率,将窗口字段设置为0
,则发送方不能发送数据。 -
拥塞控制:拥塞控制的四种算法:慢开始、拥塞控制、快重传、快恢复。
发送方从拥塞窗口为
1
开始发送数据,以指数的形式扩大发送的窗口值,当达到设置的阈值后,开始进入拥塞避免阶段,开始以线性增加窗口值,直到发送的收据出现丢失,出现拥塞的现象,然后从慢开始重新传递。并更新阈值。快速重传:发送方丢失报文,接收方连续三次重复确认丢失报文之前的报文段,发送方立即重新传递丢失的报文,接收方确认。
快恢复:接收方收到三个相同的确认后,立即快重传,之后进入快恢复阶段,阈值更新为目前的 1/2 然后进入拥塞避免的阶段。
-
-
浏览器输入
URL
之后的流程DNS
解析 ->TCP
连接 -> 发送HTTP
请求 -> 服务器处理请求并返回HTTP
报文 -> 浏览器解析渲染画面 -> 连接结束。-
DNS
解析过程:- 在浏览器中输入域名,操作系统会先检查自己本地的
hosts
文件是否含有这个网址映射关系,如果有,就先调用这个IP
地址映射,完成域名解析. - 如果
hosts
里灭有这个域名的映射,则查找本地DNS
解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析. - 如果,本地
hosts
文件和本地DNS
缓存没有对应的网址映射关系,则会查找本地DNS
服务器,如果要查询的域名包含在本地DNS
区域资源中,则返回解析结果给客户端,完成域名解析. - 如果本地
DNS
服务器\本地缓存文件\hosts
文件都失效了,则根据本地DNS
服务器的设置进行查询,如果没有用转发模式,本地DNS
服务器就把请求发送至13
台根DNS
服务器,根服务器会根据域名的信息来判断是谁来授权管理这个域名的服务器,将请求交给管理者DNS
服务器,如果自己无法解析,就会找下一级的DNS
服务器地址给本地DNS
服务器. - 如果用的是转发模式,本地
DNS
服务器会把请求转发至上一级DNS
服务器,又上一级服务器进行解析,如果上一级服务器不能解析,再找上上级服务器,以此循环.
- 在浏览器中输入域名,操作系统会先检查自己本地的
-
-
常见的状态码
& * $ ^ 1XX 信息性状态码 处理的请求正在处理 100 客户端继续发送请求,部分请求服务器已经接受 2XX 成功状态码 请求正常处理完毕 200 服务器已经成功接收请求 202 , 204 202 服务器已经接受请求但不知道会不会处理 204 服务器已经处理了请求,但没有返回任何的内容。 3XX 重定向状态码 需要进行附加操作以完成请求 301 客户端请求的网页已经永久移动到新的位置 4XX 客户端错误状态码 服务器无法处理请求 404 请求失败,客户端请求的资源没有找到或者不存在 5XX 服务器错误状态码 服务器处理请求出错 500 服务器遇到未知的错误,导致无法完成客户端的请求 503 503 服务器由于临时服务器过载或者是维护无法完成当前的请求 -
Http
的长连接与短链接Http/1.0
默认使用的是短连接,1.1
起默认使用长连接,用以保持连接的HTTP
特性
-
Cookie
的作用的是什么,和Session
的作用的区别?Cookie
和Session
都是用来跟踪浏览器用户身份的会话方式,Cookie
是浏览器中保留的数据,而Session
是服务器中保留的数据。Session
的安全性更高。
-
单个
cookie
保存的数据不能超过 4k,而session
随想没有对存储数据量的限制。 -
Http
与Https
的区别? -
端口:
HTTP
默认使用的是 80 端口,而HTTPS
默认使用的是443
端口 -
安全性和资源消耗,
HTTP
运行于TCP
之上,所传输的内容是明文;HTTPS
运行在SSL/TLS
之上,所传输的内容经过加密,加密的方式采用对称加密,但对称加密的密钥用服务器放的证书进行了非对称加密。- 对称加密:密钥只有一个,加密解密为同一个密码,且加密解密速度快,典型的算法有
DES
和AES
- 非对称加密:密钥成对出现,加密解密使用不同的密钥,相对于对称加密读书较慢,典型的非对称加密算法有
RSA、DSA
等
- 对称加密:密钥只有一个,加密解密为同一个密码,且加密解密速度快,典型的算法有
# 五: 操作系统(并发,多线程)
-
简述线程、进程、协程的概念,以及他们之间的关系?
- 进程:是程序运行的基本单位,也是操作系统分配资源的基本单位。
- 僵尸进程
- 孤儿进程
- 线程:一个进程可以产生多个线程,是比进程更小的执行单位,是
CPU
时间分配的基本单位。 - 协程:比线程更轻量级的存在,子程序的切换不是线程的切换,因此,没有线程切换的开销。第二个优势是没有多线程的锁机制,也不存在写冲突。
- 在进程进行上下文切换的时候需要保存寄存器、用户栈等信息。如果是线程进行上下文切换,则分两种情况,第一种若是线程不属于同一进程,切换过程和进程上下文切换一样。若线程属于同一进程,则只需要切换线程私有的数据、寄存器等不共享的数据。
- 进程:是程序运行的基本单位,也是操作系统分配资源的基本单位。
-
内核态与用户态的区别?
- 内核态:指操作系统在系统态运行,运行操作系统本身的程序。特权指令:指的是在系统态下运行的指令。
- 用户态:指运行应用程序,而应用程序只能在用户态运行。非特权指令:指的是用户用运行下的指令。
- 状态切换:用户态切换到内核态的唯一途径只有中断、异常、陷入这三种方式。而内核态切换到用户态的途径通过设置程序。
-
进程间通信的几种方式?
- 管道(
pipe
)及命名管道(named pipe
):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信; - 信号(
signal
):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生; - 消息队列:消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;
- 共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等;
- 信号量:主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段;
- 套接字:这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。
- 管道(
-
线程的运行状态?
-
新建 -> 可运行状态 -> 运行状态 -> 阻塞状态 -> 死亡状态。
阻塞状态分为:等待阻塞(
wait
),同步阻塞(多线程锁),其他阻塞。
-
-
Wait()
与Sleep()
方法的区别?- 来自不同的类,一个是
Thread
,一个是Object
。 Wait
方法释放了锁,Sleep
方法没有释放锁。
- 来自不同的类,一个是
-
什么是死锁,如何避免死锁?
- 在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲,就是两个或多个进程无限期的阻塞、相互等待的一种状态。
- 打破四个必要条件
- 循环等待
- 不剥夺
- 互斥
- 请求与保持
-
操作系统进程调度策略?
-
FCFS
(先来先服务,队列实现,非抢占的):先请求CPU
的进程先分配到CPU
- SJF (最短作业优先调度算法):平均等待时间最短,但难以知道下一个
CPU
区间长度 - 优先级调度算法 (可以是抢占的,也可以是非抢占的):优先级越高越先分配到
CPU
,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU
,会导致无穷阻塞或饥饿;解决方案:老化 - 时间片轮转调度算法 (可抢占的):队列中没有进程被分配超过一个时间片的
CPU
时间,除非它是唯一可运行的进程。如果进程的CPU
区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。 - 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。
- 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多
CPU
时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。
-
-
页面置换算法?
-
FIFO
先进先出算法:在操作系统中经常被用到,比如作业调度(主要实现简单,很容易想到); -
LRU(Least recently use)
最近最少使用算法:根据使用时间到现在的长短来判断; -
LFU(Least frequently use)
最少使用次数算法:根据使用次数来判断; -
OPT(Optimal replacement)
最优置换算法:理论的最优,理论;就是要保证置换出去的是不再被使用的页,或者是在实际内存中最晚使用的算法。
-
-
Synchronized
关键字原理?synchronized
关键字用于解决多线程之间访问资源的同步性,可以在实例方法、静态方法、代码块使用,在JDK1.6
之前,synchronized
是一种重量级锁,在 1.6 之后,Java
对其进行了优化,引入了如自旋锁、适应性自旋锁、锁销除、锁粗化、偏向锁、轻量级锁等技术,减少锁操作的开销。** 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。** 他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。- 详细的优化?
-
自旋锁:就是让该线程等待一段时间,不会被立即挂起(就是不让前来获取该锁(已被占用)的线程立即阻塞),看持有锁的线程是否会很快释放锁。
-
适应自旋锁:线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
-
锁消除:为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,
JVM
检测到不可能存在共享数据竞争,这是JVM
会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。 -
锁粗化:就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
-
轻量级锁:轻量级锁在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的指向和状态。如果没有竞争,轻量级锁使用
CAS
操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS
操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。 -
偏向锁:偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。
-
重量级锁: 重量级锁通过对象内部的监视器(
monitor
)实现,其中monitor
的本质是依赖于底层操作系统的Mutex Lock
实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。 -
CAS
?compare and swap
,CAS
操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。有三个参数V
,except,update
。ABA
:使用CAS
乐观锁会造成ABA
的问题,什么ABA
,简单来说,就是一个线程对数据进行了修改,然后又修改回原来的数据,此时,一个正在执行的线程在修改前和修改后取到了相同的数据。什么方法避免
ABA
问题: 使用
AtomicStampReference
,版本戳来解决。 -
synchronized
作用于代码块和方法上的区别?- 简单来说就是粗粒度锁和细粒度多的区别,作用于代码块,被同步的方法一次只能有一个线程进入,其他线程等待,而作用于代码块,则会更加灵活,通过锁定对象。
-
synchronized
与reentrantLock
的异同? -
两者都是可重入锁。自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
-
synchronized
依赖于JVM
而ReentrantLock
依赖于API
synchronized
是依赖于JVM
实现的,前面我们也讲到了 虚拟机团队在JDK1.6
为synchronized
关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock
是JDK
层面实现的(也就是API
层面,需要lock()
和unlock()
方法配合try/finally
语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 -
synchronized
底层原理?通过 Javap 命令查看字节码相关文件可以看到,
synchronied
同步语句块实际上是使用了monitorenter
和monitorexit
指令。当线程获取锁之后,锁计数器会 + 1,当退出锁之后,锁计数器会 - 1.
-
- 详细的优化?
-
ReentrantLock
锁详解。ReentrantLock
除了实现synchronized
的功能外,还实现了额外的功能。-
公平锁
公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权,而非公平锁是随机分配这种使用权,默认情况下是非公平锁,在创建
ReentrantLock
的时候通过传入参数true
来设置公平锁。虽然非公平锁的性能会更好,但是会导致饥饿的问题。 -
可响应中断
当使用
synchronized
实现锁时,如果造成死锁,在外界不干扰的情况下,将一直等待,而ReentrantLock
可以实现响应中断。 -
获取锁时限时等待
ReentrantLock
还给我们提供了获取锁限时等待的方法tryLock()
, 可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true
表示获取锁成功,false
表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。
结合
Condition
实现等待通知机制 使用
synchronized
结合Object
上的wait
和notify
方法可以实现线程间的等待通知机制,ReentrantLock
结合Condition
接口同样可以实现这个功能。而且相比前者使用起来更加清晰。Condition
接口在使用前必须先调用ReentrantLock
的lock()
方法获得锁,之后调用Condition
接口的await()
将释放锁,并且在该Condition
上等待,直到有其他线程调用Condition
的signal()
方法唤醒线程。使用
Condition
实现简单的阻塞队列入队和出队线程安全,当队列满时,入队线程会被阻塞,当队列为空时,出队线程会被阻塞。
-
-
Volatile
关键字JMM
内存模型?-
JMM
内存模型?Java
内存模式是一种虚拟机规范。Java
虚拟机规范中定义了Java
内存模型(Java Memory Model
,JMM
),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java
程序在各种平台下都能达到一致的并发效果,JMM
规范了Java
虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。 -
Volatile
(原子性(不保证),可见性,顺序性):线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。volatile
关键字的主要作用就是保证变量的可见性(也就是当一个线程修改了一个被volatile
修饰共享变量的值,新值总数可以被其他线程立即得知)然后还有一个作用是防止指令重排序。-
内存屏障?
内存屏障,又称内存栅栏,是一个
CPU
指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile
的内存可见性)。
-
-
-
synchronized
与volatile
之间的区别?-
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好。但是 **volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块 **。 - 多线程访问
volatile
关键字不会发生阻塞,而synchronized
关键字可能会发生阻塞 -
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证。(例如两个线程同时 i++,自增操作是多个原子性操作,还没有完成操作就阻塞了) -
volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性。
-
-
ThreadLocal
面试题?ThreadLocal
是除了加锁这种同步方式之外的一种规避多线程访问同一变量造成线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候都是自己的变量,这样就不会存在线程不安全的问题.-
ThreadLocal
实现原理:Thread 类中有两个变量
threadLocals
和ingeritableThreadLocals
, 两者都是ThreadLocal
内部类ThreadLocalMap
类型的变量。线程的本地变量不是存放在ThreadLocal
的实例中,而是放在调用线程的ThreadLocals
变量里面。也就是说,使用set
赋值,是给自己线程里面的变量赋值而不是给实例赋值. -
ThreadLocal
不支持继承同一个
ThreadLocal
变量在父线程中被设置值后,在子线程中是获取不到的. -
InheritableThreadLocal
实现子线程访问父线程.InheritableThreadLocal
类继承了ThreadLocal
类,并重写了childValue
、getMap
、createMap
三个方法。其中createMap
方法在被调用(当前线程调用set
方法时得到的map
为null
的时候需要调用该方法)的时候,创建的是inheritableThreadLocal
而不是threadLocals
。同理,getMap
方法在当前调用者线程调用get
方法的时候返回的也不是threadLocals
而是inheritableThreadLocal
。 -
ThreadLocal
使用不当造成的内存泄漏从源代码中可以知道,
ThreadLocalMap
是一个类似于hashmap
的数据结构,而且其key
的引用是弱引用,. 当线程调用ThreadLocal
的set
方法设置变量的时候,当前线程的ThreadLocalMap
就会存放着一个记录,这个记录的key
值为ThreadLocal
的弱引用,value
通过set
方法设置。接下去的情况就与弱引用的weakhashMap
一样,当key
值被GC
回收的时候,value
值还存在,从而造成内存的泄漏.
-
-
多线程启动的三种方式,以及
Runnable
接口与Callable
接口的区别?-
继承
Thread
类,新建一个当前类对象,并且运行其start()
方法。 -
实现
Runnable
接口,然后新建当前类对象,接着新建Thread
对象时把当前类对象传进去,最后运行Thread
对象的start()
方法。 -
实现
Callable
接口,新建当前类对象,在新建FutureTask
类对象时传入当前类对象,接着新建Thread
类对象时传入FutureTask
类对象,最后运行Thread
对象的start()
方法。 -
callable
提供了比Runnable
更强大的功能。1.
Callable
可以在任务结束的时候提供一个返回值,Runnable
无法提供这个功能2.
Callable
的call
方法分可以抛出异常,而Runnable
的run
方法不能抛出异常。
-
-
ThreadPoolExecutor
7 大参数 作用 流程1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
参数含义:
corePoolSize: 1 线程池核心线程数
maximumPoolSize: Integer.Max_VALUE 线程池最大数
keepAliveTime: 60s 空闲线程存活时间
unit: TimeUnit.SECONDS 时间单位
workQueue: Integer.MAX_VALUE 线程池所使用的缓冲队列
threadFactory: 线程池创建线程使用的工厂
handler: AbortPolicy() 线程池对拒绝任务的处理策略线程池处理流程:
当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数时,且任务队列未满,将任务放入到任务对列中。
当线程数大于等于核心线程数时,且任务队列已满
若线程数小于最大线程数,则创建线程。
若线程数等于最大线程数,抛出异常,拒绝任务。
-
Future
机制Future
表示异步计算的结果,Future
接口提供了检查计算是否完成、检查计算是否被取消、等待计算完成并获取计算结果等方法。Future
是一个接口,有多种实现方式,比较常见的一种是FutureTask
,这个类继承了RunnabelFuture
,然后RunnabelFuture
又实现了Future
接口。在成功执行run()
方法就,可以通过Future
访问执行结果,通过get()
方法,当未执行完时,会进行阻塞,等待执行结束。 -
阻塞队列
Queue
,7 种阻塞队列, 消费者生产者模式阻塞队列是一个支持两个附加操作的队列,在队列为空时,获取元素的线程会等待会裂变为非空,当队列满时,存储元素的线程会等待队列可用。
七种阻塞队列:
ArrayBlockingQueue
:是一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO
)的原则对元素进行排序。支持公平锁和非公平锁。【注:每一个线程在获取锁的时候可能都会排队等待,如果在等待时间上,先获取锁的线程的请求一定先被满足,那么这个锁就是公平的。反之,这个锁就是不公平的。公平的获取锁,也就是当前等待时间最长的线程先获取锁】LinkedBlockingQueue
:一个由链表结构组成的有界队列,此队列的长度为Integer.MAX_VALUE
。此队列按照先进先出的顺序进行排序。PriorityBlockingQueue
: 一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()
方法来指定元素排序规则,不能保证同优先级元素的顺序。DelayQueue:
一个实现PriorityBlockingQueue
实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。(DelayQueue
可以运用在以下应用场景:1. 缓存系统的设计:可以用DelayQueue
保存缓存元素的有效期,使用一个线程循环查询DelayQueue
,一旦能从DelayQueue
中获取元素时,表示缓存有效期到了。2. 定时任务调度。使用DelayQueue
保存当天将会执行的任务和执行时间,一旦从DelayQueue
中获取到任务就开始执行,从比如TimerQueue
就是使用DelayQueue
实现的。)SynchronousQueue:
一个不存储元素的阻塞队列,每一个put
操作必须等待take
操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue
的一个使用场景是在线程池里。Executors.newCachedThreadPool()
就使用了SynchronousQueue
,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了 60 秒后会被回收。LinkedTransferQueue:
一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue
队列多了transfer
和tryTransfer
方法。LinkedBlockingDeque:
一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。
-
AQS
(AbstractQueuedSynchronizer
) 共享锁CountDownLatch
,排他锁ReentrantLock
。AQS(AbstractQueuedSynchronizer)
抽象队列同步器,为线程的同步和等待操作提供一个基础模板类,即该类使用的时模板模式。通过子类继承该模板实现自己的功能即可,例如CountDownLatch
和ReentrantLock
。AQS
中有两个重要的概念,State
和CLH
队列,State
表示锁的状态,CLH
表示线程的阻塞队列。其中state
被volatile
修饰,表证了内存的可见性。CLH
队列是一个双向链表的数据结构。根据节点类型的不同可以实现上述的不同类型的锁。- 独占资源
# 六:数据库
# Mysql
-
MyISAM
和InnoDB
引擎的区别?MyISAM
是MySQL
的默认数据库引擎(5.5版
之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM
不支持事务机制和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。5.5版本
之后,MySQL
引入了InnoDB
。大多数时候我们使用的都是InnoDB
存储引擎,但是在某些情况下使用MyISAM
也是合适的比如读密集的情况下。
-
索引?(重点)
-
为什么要使用索引,索引的缺点?
- 通过创建唯一性索引,保证表中每一行数据的唯一性。
- 加快数据的检索速度。
- 将随机
IO
变成顺序IO
。 - 帮助服务器避免排序。
缺点:占用物理空间,对标进行改动时,维护成本高。
-
Mysql
中使用的是B+Tree
索引,在MYSIAM
和InnoDB
索引的实现方式上有些区别?MyISAM
:使用的是非聚簇索引,B+Tree
叶节点的data
区域存放的是数据记录的地址,然后找到对应的地址读取数据记录。InnoDB
: 使用的是聚簇索引,其数据文件本身就是索引文件,叶节点data
域保存了完整的数据记录,这个索引的key
是数据表的主键。
-
B+树
比B树
的好处B+树
的数据都存储在叶子节点,而B树
存储在各层节点中,利用上下文局部性的原理查询效率高。B+树
比B树
矮,IO
更少。B+树
的叶子节点之间有索引,有利于数据库的扫描。
-
索引的类型?
- 种类:
- 聚簇索引:叶子节点存储的是数据行。
- 非聚簇索引:叶子节点存储的是数据行的地址。
- 回表操作:指在非聚簇索引中找到了目标数据的地址,然后在根据地址去查询其他索引。
- 索引覆盖:将查询的字段联合索引里面去。
UNIQUE
唯一性索引。
- 种类:
-
索引的优化?(重要)
- 索引失效情况:
- 范围查询的列之后还有查询条件。
- 对列进行了运算操作。
- 字符串没有加
‘ ’
;
- 尽量使用覆盖索引,避免使用
select *
;需要回表查询。 - 对于
or
语句分割的条件,如果or
前的条件中的列有索引,中间是or
进行连接的是不走索引的。 - 模糊匹配,
%
加在 条件之前,会造成索引失效,可以通过覆盖索引来解决。 - 尽量使用符合索引,减少使用单列索引。
- 索引失效情况:
-
-
事务?四大特性?(重点)
原子性A
:事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;一致性C
: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;隔离性I
: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;持久性D
:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
原子性实现原理:
undo log
实现原子性的关键在于
undo log
日志。当事务回滚时能够撤销所有已经成功执行的sql
语句,当事务对数据库进行修改时,InnoDB
会生成对应的undo log
,与操作相反的语句。undo log
属于逻辑日志。持久性实现原理:
redo log
当数据修改时,除了修改
buffer
中的数据,还会生成redo log
日志记录,当事务提交时,会调用fsync
接口对redo log
进型更新。当数据库宕机重启时,读取redo log
中的数据,对数据库进行恢复操作。 除了
redo log
还有一个bin log
,也可以用来记录操作信息,bin log
是二进制日志,存储二进制的信息,也是用来主从复制的关键。隔离性实现原理:
与第 4 点隔离机制一样。
MVCC
多版本并发控制协议实现原理:
MVCC
最大的优点是读不加锁,并发性能好,主要是通过以下技术和数据结构: 1. 隐藏列:
Innodb
中每行数据都有隐藏列,存储着本行数据的事务id
,指向undo log
的指针等信息。 2. 基于
undo log
的版本链:每条undo log
也会指向更早版本的undo log
,从而形成一条版本链。 3.
ReadView
:通过隐藏列和版本链,Mysql
可以将数据恢复到指定的版本,具体恢复到哪个版本就是通过ReadView
(事务系统快照)来确定。事务进行读操作的时候,会将读取到事务ID
与快照找的信息进行比较,从而判断数据对该ReadView
是否可见。 -
事务隔离机制,以及脏读 \ 不可重复读 \ 幻读的区别?
-
数据出现的错误情况:
脏读: 指一个事务修改了数据还没有提交,另一个线程来读取了修改后未提交的数据,导致数据不是真实的.
不可重复读: 指一个事务还没有结束,另一个事务读取并修改了数据,导致,当前事务的数据不一致.
幻读: 指一个事务执行操作的时候 (有关全表数据的操作), 另一个事务进行了操作,这种操作可能是
增加/删除
等,导致第一个事务发现数据表中出现了没有修改的数据行,就像发生了幻觉一样. -
隔离机制
-
READ-UNCOMMITTED
(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。 -
READ-COMMITTED
(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。 -
REPEATABLE-READ
(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,通过 MVCC 版本控制实现,可以阻止脏读和不可重复读,但幻读仍有可能发生。(Mysql 的隔离机制) -
SERIALIZABLE
(可串行化): 最高的隔离级别,完全服从ACID
的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
隔离级别 脏读 不可重复读 幻影读 READ-UNCOMMITTED
√ √ √ READ-COMMITTED
× √ √ REPEATABLE-READ
× × √ SERIALIZABLE
× × × -
-
如何解决幻读?
Mysql
在RR
级别下确实解决了幻读,但是有两种情况.- 在快照读的情况下,
MySQL
通过MVCC
来避免. - 在当前读的情况下,
Mysql
通过间隙锁 + 行锁实现.
此外,通过序列化的隔离机制也可以解决幻读.
- 在快照读的情况下,
-
-
Mysql
中的锁机制与InnoDB
锁算法?MyISAM
和InnoDB
存储引擎使用的锁:MyISAM
采用表级锁 (table-level locking
)。- InnoDB 支持行级锁 (
row-level locking
) 和表级锁,默认为行级锁.
-
冷热数据分离?
- 设计分库:比如有一个订单表,日均写入几万几十万数据进去,存储数据的时候存储双份,
order
表存一份数据,order_history
表同样也存一份数据,然后,order
表设置一个定时任务,每天定期删除 30 天以上的数据,order_history
表不做任何删除操作,只插入新的数据,保留最原始的数据。 - 优化?
- 限定数据的范围。
- 读 / 写分离。
- 设计分库:比如有一个订单表,日均写入几万几十万数据进去,存储数据的时候存储双份,
-
sql
优化- 对于查询的
sql
语句优化:- 查看
sql
语句执行频次,通过命令show status like
; - 定位低效率执行的
sql
,有两种方式,根据需求不同,选择不同。- 通过慢查询日志。
show processlist
;命令,查看实时执行时间。
- 通过
explain
命令,查看Extra
字段,如果存在Using File Sort
,或者Using temporary
字段,则需要进行优化。
- 查看
- 对于大量数据插入的
sql
语句优化load
命令: - 尽量使用主键顺序插入。
- 关闭唯一性校验。
- 手动提交事务。
- 对于
insert
语句的优化:- 将多次插入改为一条语句,或者将事务提交改为手动提交。
- 对于
order by
语句的优化:- 两种排序模式,
Using FileSort,Using index
。- 对于
USing fileSort
排序有两种实现方式,一次扫描排序,两次扫扫描排序,对于使用哪种算法,会根据一些字段来比较。
- 对于
- 两种排序模式,
- 对于查询的
-
解释一下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池?
我们常见的如
java
线程池、jdbc
连接池、redis
连接池等就是这类设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好比你去食堂打饭,打饭的大妈会先把饭盛好几份放那里,你来了就直接拿着饭盒加菜即可,不用再临时又盛饭又打菜,效率就高了。
# Postgres
- 与
Mysql
相比,优势何在?Mysql
使用多线程,Postgresql
使用多进程。PG
的稳定新极强,在高并发读写,负载极限的情况下,PG
还能稳定速率不下降。- 数据同步方式不同,
MySQL
时异步同步的方式,而且是基于binlog
的逻辑复制,pg
可以实现同步复制、异步复制、半同步复制,而且基于wal
复制。 PG
是志愿者在维护,Mysql
是商业公司在维护。
# Redis
-
CAP?Nosql
中的特性。- C:
Consistency
(一致性):即访问所有的节点得到的数据应该是一样的。 - A:
Availability
(可用性):所有的节点都保持高可用性。 - P:
Partition Tolerance
(分区容错性):这里指网络意义的分区,在节点不能通信的时,要保证系统可以继续正常服务。
一个分布式系统,不可能同时满足
C、A、P
这3个条件。 - C:
-
为什么要用
redis
做缓存?- 高性能:
redis
运行于内存中,相比于传统的运行于磁盘的数据库非常的高效。 - 高并发:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
优点:
- 读写性能优异,
Redis
能读的速度是 110000 次 /s,写的速度是 81000 次 /s。 - 支持数据持久化,支持
AOF
和RDB
两种持久化方式。 - 支持事务,
Redis
的所有操作都是原子性的,同时Redis
还支持对几个操作合并后的原子性执 行。 - 数据结构丰富,除了支持
string
类型的value
外还支持hash、set、zset、list
等数据结构。 - 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点:
- 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此
Redis
适合的场景主 要局限在较小数据量的高性能操作和运算上。 Redis
不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要 等待机器重启或者手动切换前端的IP
才能恢复。- 主机宕机,宕机前有部分数据未能及时同步到从机,切换
IP
后还会引入数据不一致的问题, 降低了系统的可用性。 Redis
较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问 题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。
- 高性能:
-
redis
应用场景- 计数器
- 缓存
- 会话缓存
- 全页缓存(
FPC
) - 查找表
-
为什么要用
redis
而不用map/guava
做缓存呢?缓存分为本地缓存和分布式缓存。以
Java
为例,使用自带的map
或者guava
实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm
的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。使用redis
或memcached
之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持redis
或memcached
服务的高可用,整个程序架构上较为复杂。 -
redis
常见的数据结构?-
String
:String
数据结构是简单的key-value
类型,value
其实不仅可以是String
,也可以是数字。 常规key-value
缓存应用; 常规计数:微博数,粉丝数
等。 -
Hash:hash
是一个string
类型的field
和value
的映射表,hash
特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以hash
数据结构来存储用户信息,商品信息
等等。 -
List
:list
就是链表,Redis
list
的应用场景非常多,也是Redis
最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis
的list
结构来实现。Redis
list
的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。另外可以通过
lrange
命令,就是从某个元素开始读取多少个元素,可以基于list
实现分页查询,这个很棒的一个功能,基于redis
实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。 -
Set
:set
对外提供的功能与list
类似是一个列表的功能,特殊之处在于set
是可以自动排重的。当你需要存储一个列表数据,又不希望出现重复数据时,
set
是一个很好的选择,并且set
提供了判断某个成员是否在一个set
集合内的重要接口,这个也是list
所不能提供的。可以基于set
轻易实现交集、并集、差集的操作。比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。
Redis
可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。 -
Sort Set
:和set
相比,sorted set
增加了一个权重参数score
,使得集合中的元素能够按score
进行有序排列。原理:使用字典(
dict
)+ 跳表(skiplist
),当数据比较少的时候用ziplist
编码结构存储。
-
``ziplist``编码的Zset使用紧挨在一起的压缩列表节点来保存,第一个节点保存member,第二个节点爆粗你``score``。``ziplist``内的集合元素按``score``从小到大排序。
跳跃表十一总有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
-
redis
设置过期时间1
2
3
4
5
6
7
8
9
10
11
12
13
14redis> SET cache_page "www.google.com"
OK
redis> EXPIRE cache_page 30 # 设置过期时间为 30 秒
(integer) 1
redis> TTL cache_page # 查看剩余生存时间
(integer) 23
redis> EXPIRE cache_page 30000 # 更新过期时间
(integer) 1
redis> TTL cache_page
(integer) 29996- 定期删除:
redis
默认是每隔100ms
就随机抽取一些设置了过期时间的key
,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如redis
存了几十万个key
,每隔 100ms 就遍历所有的设置过期时间的key
的话,就会给CPU
带来很大的负载! - 惰性删除:定期删除可能会导致很多过期
key
到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期key
,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个key
,才会被redis
给删除掉。这就是所谓的惰性删除! - 定时删除:超过设置时间的数据就会立马删除,占用
cpu
资源。
- 定期删除:
-
redis
内存淘汰机制,当内存不够时进行的操作。- 检测易失数据:
volatile-lru
:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。volatile-ttl
:从已设置过期时间的数据集中挑选将要过期的数据淘汰。volatile-random
:从已设置过期时间的数据集中随机选择数据淘汰。vlotile-lfu
:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
- 检测全库数据:
allkeys-lru
:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key。allkeys-random
:从数据集中任意选择数据淘汰。allkeys-lfu
:当内存不足以容纳新写入数据时,在键空间中,一处最不经常使用的 key。
n o-eviction
:静止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
- 检测易失数据:
-
redis
持久化机制。- 快照(
RDB
):Redis
可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis
创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis
主从结构,主要用来提高Redis
性能),还可以将快照留在原地以便重启服务器的时候使用。- 通过
save
指令保存rdb
文件,但是会占用线程,阻塞。 - 通过
bgsave
指令进行后端保存,不占用线程。bgsave
发送指令后,返回消息,然后调用 fork 函数生成子进程,通过子进程创建rdb
文件。
- 自动执行,配置文件。
- 在
conf
文件中,命令save 60 10000
,在60
秒内有10000
个操作就进行
- 在
- 通过
- 追加文件(
AOF
):与快照持久化相比,AOF
持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis
没有开启AOF
(append only file
)方式的持久化,可以通过appendonly
参数开启。将指令先加到缓存中。- 写数据三种策略(
appendfsync
)。always
每次,不建议使用。everysecond
每秒,默认配置。no
系统控制,有系统控制,不可控。
AOF
重写- 规则:对于进程内已经超时的数据不在写入文件;忽略无效指令,合并
set
指令。 - 调用
bgrewriteaof
指令。与bgsave
指令一样时fork
一个子进程进行重写。 - 自动调用,通过配置文件设置,参数,
- 规则:对于进程内已经超时的数据不在写入文件;忽略无效指令,合并
- 写数据三种策略(
- 快照(
-
数据库与缓存一致性问题。
提供了两种解决方法,结合业务和技术代价选择使用。
-
采用延时双删策略
在写库前后都进行
redis.del(key)
操作,并且设置合理的超时时间。 -
异步更新缓存
基于订阅
bin log
日志的同步机制,增删改对MySQL
进行操作之后,通过bin log
分析,利用消息队列推送至各台的redis
缓存数据。 -
redis
事务?
redis
具有原子性,一致性,隔离性,并且放 redis
运行在某种特定的持久化模式下,也就有持久性。
-
缓存雪崩和缓存击穿?
-
主从复制
-
什么是主从复制?
主从复制是用于读写分离的一种实现,将数据操作的读写用到不同的
redis
服务器上,一般的是master
主机用于写,slave
从机用于读数据。有效的提高redis
服务器的性能。 -
主从复制的流程:
- 首先是
slave
服务器向master
服务器发起连接请求,告诉master
有一个slave
需要连接,master
接收到后,将id
和指令缓冲队列的offset
发送给slave
。建立一个socket
连接。 - 之后
slave
开始rdb
数据的复制。全量复制。 - 全量复制结束之后,通过判断
offset
的值是否相等,如果相等则开始局部复制,而这个复制的是命令缓冲队列里面的命令,到slave
上在执行命令。
- 首先是
-
心跳机制:
- 为了保证集群的高可用,
master
与slave
之间通过ping
命令来相互访问,确定master
节点和slave
节点还存活。
- 为了保证集群的高可用,
-
-
哨兵机制
- 哨兵机制用于监督集群的运行状态,若
master
服务器宕机了,通过哨兵会发起一个选举机制,一般哨兵的数量是奇数个,当有n/2+1
个哨兵检测到master
节点宕机,则开始重新选举master
。
- 哨兵机制用于监督集群的运行状态,若
-
缓存雪崩:缓存同一时间大面积的失效,请求直接落到数据库上,造成数据库短时间内承受大量请求而崩掉。
-
缓存击穿:同一时间,大量的连接访问同一个
key
,这个key
在mysql
数据库上,这样就会造成缓存的击穿。- 对于缓存的击穿的解决有两类方式,通过道和术。
-
缓存穿透:
key
对应的数据在数据源并不存在,每次针对此key
的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。 -
布隆过滤器?
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断
key
是否合法,把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。 原理:
布隆过滤器的核心是实现一个超大位数组和几个哈希函数.假设为数组的长度为``m``,哈希函数的个数为``k``.具体操作流程如下:
> 将数组进行初始化,每个位置设置为``0``.
> 对于集合中的每一个元素,将元素依次通过``k``个哈希函数映射.每次映射都会长生一个哈希值,这个值对应位数组上面的一个点,并 将该点编注为1.
> 查询``W``元素是否存在集合中的时候,将``W``一次通过``k``个哈希函数的计算,如果k个点有一个不为1,那么该元素一定不在集合中.
> 存在误判率,假设``w``经过``k``个哈希函数映射出来的值并不是来自一个已存在的映射,就是误判.
-
如何解决
redis
的并发竞争Key
问题?同一时间,多个请求需要堆同一个
key
值处理,不同的处理顺序结果也不相同,可用的处理结果如下.- 可以通过分布式锁 (
redis
自身的分布式锁或者Zookeeper
)+ 时间戳来实现。 - 利用消息队列,将请求进行串行化.
- 可以通过分布式锁 (
# 七:消息队列
# Kafka
Kafka
是一个分布式的,基于发布 / 订阅模式的消息队列。
-
应用场景:
- 缓冲和削峰:有过多数据时,把消息暂存于
kafka
中,达到一个缓冲的作用. - 解耦于扩展性:解耦重要的业务流程.
- 冗余:可以采用一对多的方式,一个生产者发布消息,可以被多个订阅的
topic
的服务消费到,供多个毫无关联的业务使用. - 健壮性:即使业务端短时间挂机,消息队列中的消息不会丢失.
- 异步通信:用户不想也不需要立即处理的消息,将其放入
kafka
中,等到需要处理的时候在处理.
- 缓冲和削峰:有过多数据时,把消息暂存于
-
kafka
消息丢失与重复消费?-
保证消息不丢失?
-
生产者丢失消息的情况。
消息可能因为网路问题没有发送出去,所以在
send
方法之后,不能确定消息是否发送成功。可以添加回调函数的形式,如果消息发送失败,就检查失败的原因,重新发送。‘ -
消费者丢失的情况。
消息被加到
Partition
时,会分配一个偏移量,当消费者拉取到分区的消息之后,消费者会自动提交offset
,加入此刻,突然挂机,消息还没有被消费,而offset
已经提交,造成数据丢失的情况。若关闭自动提交,则在消费完消息后突然挂机,则会重复消费。 -
Kafka
丢失消息。分区的副本有很多,有
leader
和follower
,而倘若leader
突然挂机,follower
副本还没有完成全部消息的复制,则就会造成消息的丢失。
-
-
调优
broker
参数防止消息丢失。
-
-
lsr
?producer 的deliver guarantee
可以通过request.required.acks
参数的设置来进行调整:0
,相当于异步发送,消息发送完毕即offset
增加,继续生产;相当于At most once
1
,leader
收到leader replica
对一个消息的接受ack
才增加offset
,然后继续生产;
-1
,leader
收到所有replica
对一个消息的接受ack
才增加offset
,然后继续生产Kafka
结合同步复制和异步复制,使用ISR
(与Partition
Leader
保持同步的Replica
列表)的方式在确保数据不丢失和吞吐率之间做了平衡。Producer
只需把消息发送到Partition
Leader
,Leader
将消息写入本地Log。Follower
则从Leader pull
数据。Follower
在收到该消息向Leader
发送ACK
。一旦Leader
收到了ISR
中所有Replica
的ACK
,该消息就被认为已经commit
了,Leader
将增加HW(high watermark
,最后commit
的offset
)并且向Producer
发送 ACK。这样如果leader
挂了,只要Isr
中有一个replica
存活,就不会丢数据。 -
解释
Kafka
的Zookeeper
是什么?我们可以在没有Zookeeper
的情况下使用 Kafka 吗?Zookeeper
是一个开放源码的、高性能的协调服务,它用于Kafka
的分布式应用。
不,不可能越过Zookeeper
,直接联系Kafka broker
。一旦Zookeeper
停止工作,它就不能服务客户端请求。
Zookeeper
主要用于在集群中不同节点之间进行通信
在Kafka
中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取
除此之外,它还执行其他活动,如:leader
检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等
# 八: Elasticsearch+ELK
-
Elasticsearch
是什么?ES
是一个基于分布式的,近实时的,具有RestFul
风格的企业级搜索引擎。 -
倒排索引?
倒排索引是
ES
搜索引擎的核心,也是ES
搜索速度快的原因。倒排索引是一种像散列图的数据结构,传统的检索是通过文章逐个遍历找到对应的关键词,而倒排索引是通过分词策略,形成词与文章的映射关系表,这个词 + 映射表的模式就是倒排索引。我们可以通过term
字典,找到对应的docid
,在通过相应的得分计算,获得权重进行排序。s -
es
数据写入与数据读取的过程,数据搜索的过程?- 数据写入:客户端将请求发送到协调节点,再由协调节点发送到对应的
node
节点,node
节点处理请求后,将数据通过到分片,处理完后,将响应结果返回给客户端。 - 数据读取:同理,客户端发送请求到协调节点,协调节点在路由至对应的节点上,此时,数据的读取可以是分片也可以是主节点上的数据,请求负载均衡,然后将结果返回给客户端。
- 数据搜索:客户端发送请求到协调节点,协调节点将搜索请求转发到所有的
shard
对应的primary shard
和replica shard
。每个shard
将自己的搜索结果返回给协调节点,再由协调节点进行数据的合并、排序、分页等操作,最后产生结果。
- 数据写入:客户端将请求发送到协调节点,再由协调节点发送到对应的
-
es
数据删除和更新过程?- 磁盘上的每个段都有一个相应的
.del
文件。当删除请求发送后,文档并没有真的被删除,而是在.del
文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del
文件中被标记为删除的文档将不会被写入新段。 - 在新的文档被创建时,
Elasticsearch
会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del
文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。
- 磁盘上的每个段都有一个相应的
-
es
的master
选举过程,如何解决脑裂问题?- 发起选举的时间点是集群初始化启动、
Master
节点奔溃或者是Master
节点没有得到n/2+1
节点的认可。 - 第一步,通过
ping
,获取存活的Master
节点,筛选activeMasters
,并不包括本地节点。筛选masterCandidates
列表,是当前集群有资格成为Master
的节点。 - 第二步,判断
activeMaster
列表是否为空,如果不为空,则从active Master
种选举,如果为空,则从MasterCandidate
种选举。 - 第三步:上述两种情况中的任何一种产生的
Master
,如果是本地节点,则等待集群的其他成员投票,超过n/2+1
则称为master
,若果不是本地节点,则向Master
节点申请加入集群的请求。 - 避免脑裂,需要得到
n/2+1
上的票才可以成为Master
,如果只剩两节点集群,则集群就挂了。
- 发起选举的时间点是集群初始化启动、
-
es
在数据量很大的时候如何提高性能?- 写优化:处理大量数据写入的时候
- 批量提交,
bulk API
- 优化存储设备
- 合理使用合并,提高段合并速度
- 减少
Refresh
使用的次数 - 减少副本的数量
- 批量提交,
- 读优化,适用于读取大量数据的情况
- 使用
Scroll
:将分页查询的数据一次性读取,减少查询和排序的次数。 - 选择合适的路由,避免查询所有的分片。
- 定期删除:删除
.del
文件。 - 设置堆大小,初始堆内存太小。
- 使用
- 写优化:处理大量数据写入的时候
# 九:框架
# Spring
-
什么是
Spring
框架?Spring
是一种轻量级开发框架,用于提高开发人员的开发效率以及系统的可维护性。我们一般说Spring
框架指的都是Spring Framework
,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。
-
Spring
有哪些模块?-
Spring Core
: 基础,可以说Spring
其他所有的功能都需要依赖于该类库。主要提供 IoC 依赖注入功能。 -
Spring Aspects
: 该模块为与AspectJ
的集成提供支持。 -
Spring AOP
:提供了面向切面的编程实现。 -
Spring JDBC
:Java
数据库连接。 -
Spring JMS
:Java
消息服务。 -
Spring ORM
: 用于支持Hibernate
等ORM
工具。 -
Spring Web
: 为创建Web
应用程序提供支持。 -
Spring Test
: 提供了对JUnit
和TestNG
测试的支持。
-
-
@
RestController
与 @Controller
的区别?@
RestController=@ResponseBody+@Controller
。使用@Controller
返回的是jsp
或者html
页面,如果在方法前加上@ResponseBody
则返回的是json
或者xml
数据。 -
Spring IOC
与AOP
?IOC:IOC
是一中设计思想,就是将原本在程序中需要手动创建对象的控制权,交由Spring
框架管理,并由IoC
容器完成对象的注入。IoC
的实现有两种方式,一:配置XML
文件来注入到Bean
中,二:通过注解配置的模式完成注入。AOP
:面向切面编程,将那些与业务无关的,却为业务模块所共同调用代码封装起来,例如:日志管理,权限管理等。这样有利于减少系统的重复代码,降低模块之间的耦合度。AOP
实现的关键在于代理模式,AOP
代理主要分为静态代理和动态代理,静态代理以AspectJ
为例,而动态代理则以Spring AOP
代表。- 静态代理:也称编译时增强,
AOP
框架会在编译阶段生成AOP
代理类,并将AspectJ
织入到Java
字节码文件中,运行的时候就是增强之后的AOP
对象。 - 动态代理:也称运行时增强,其就是不会去修改
AOP
的字节码,而是每次运行在内存中临时为方法生成一个AOP
对象,这个 AOP 对象包含目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。JDK
动态代理:JDK
代理只提供接口的代理,不提供类的代理,要求被代理的类实现接口。CgLib
动态代理: 如果被代理类没有实现接口,那么Spring AOP
会选择使用CGLIB
来动态代理目标类。CGLIB
(是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP
。CGLIB
是通过继承的方式做的动态代理,因此如果某个类被标记为final
,那么它是无法使用CGLIB
做动态代理的。
- 静态代理:也称编译时增强,
-
Spring AOP
与AspectJ AOP
有什么区别?静态代理与动态代理区别在于生成
AOP
代理对象的时机不同,相对来说AspectJ
的静态代理方式具有更好的性能,但是AspectJ
需要特定的编译器进行处理,而Spring AOP
则无需特定的编译器处理。 -
Spring
中的bean
作用域以及生命周期?- 简单来说,
Spring Bean
的生命周期只有四个阶段:实例化 -> 属性赋值 -> 初始化 -> 销毁。 Spring Bean
的作用域。singleton
:默认作用域,单例bean
,每个容器中只有一个bean
的实例。prototype
:为每一个bean
请求创建一个实例。request
:为每一个request
请求创建一个实例,在请求完成以后,bean
会失效并被垃圾回收器回收。session
:与request
范围类似,同一个session
会话共享一个实例,不同会话使用不同的实例。global-session
:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session
中。
- 简单来说,
-
Spring
如何解决循环依赖?循环依赖问题在
Spring
中主要有三种情况:(1)通过构造方法进行依赖注入时产生的循环依赖问题。
(2)通过setter
方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
(3)通过setter
方法进行依赖注入且是在单例模式下产生的循环依赖问题。
在Spring
中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。这是因为:第一种构造方法注入的情况下,在 new 对象的时候就会堵塞住了,其实也就是” 先有鸡还是先有蛋 “的历史难题。
第二种setter
方法(多例)的情况下,每一次getBean
() 时,都会产生一个新的Bean
,如此反复下去就会有无穷无尽的Bean
产生了,最终就会导致OOM
问题的出现。
Spring
在单例模式下的setter
方法依赖注入引起的循环依赖问题,主要是通过二级缓存和三级缓存来解决的,其中三级缓存是主要功臣。解决的核心原理就是:在对象实例化之后,依赖注入之前,Spring
提前暴露的Bean
实例的引用在第三级缓存中进行存储。 -
有哪些注解?
spring Bean
相关:@Autowired
@Component
:通用的注解,可标注任意类为Spring
组件。如果一个 Bean 不知道属于哪个层,可以使用@Component
注解标注@Repository
:对应持久层即Dao
层,主要用于数据库相关操作@Service
:对应服务层,主要涉及一些复杂的逻辑,需要用到Dao
层@Controller
:对应Spring MVC
控制层,主要用户接受用户请求并调用Service
层返回数据给前端页面
- 常见处理 HTTP 请求
@GetMapping
:GET
请求、@PostMapping
:POST
请求。@PutMapping
:PUT
请求。@DeleteMapping
:DELETE
请求。
-
SpringMVC?
-
Model1
时代:在Model1
模式下,整个Web
应用几乎全部用JSP
页面组成,只用少量的JavaBean
来处理数据库连接、访问等操作。这个模式下JSP
即是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低; -
Model2
时代 :学过Servlet
并做过相关Demo
的朋友应该了解“Java Bean(Model)+ JSP(View,)+Servlet(Controller) ”
这种开发模式,这就是早期的JavaWeb MVC
开发模式。Model
: 系统涉及的数据,也就是dao
和bean
。View
:展示模型中的数据,只是用来展示。Controller
:处理用户请求 ,然后返回数据给JSP
并展示给用户。Struts2.
-
SpringMVC
:MVC
是一种设计模式,Spring MVC
是一款很优秀的MVC
框架。Spring MVC
可以帮助我们进行更简洁的Web
层的开发,并且它天生与Spring
框架集成。Spring MVC
下我们一般把后端项目分为Service
层(处理业务)、Dao
层(数据库操作)、Entity
层(实体类)、Controller
层 (控制层,返回数据给前台页面)。
-
-
Spring
框架中的设计模式?- 工厂设计模式 :
Spring
使用工厂模式通过BeanFactory
、ApplicationContext
创建 bean 对象 - 代理设计模式 :
Spring AOP
功能的实现。 - 单例设计模式 :
Spring
中的Bean
默认都是单例的。 - 模板方法模式 :
Spring
中jdbcTemplate
、hibernateTemplate
等以Template
结尾的对数据库操作的类,它们就使用到了模板模式。 - 包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式:
Spring
事件驱动模型就是观察者模式很经典的一个应用。 - 适配器模式 :
Spring AOP
的增强或通知 (Advice
) 使用到了适配器模式、spring MVC
中也是用到了适配器模式适配Controller
。
- 工厂设计模式 :
-
Spring
事务?-
编程式事务:在代码中硬编码。
-
声明式事务:在配置文件中配置。
- 基于
XML
的声明式事务。 - 基于注解的声明式事务。
- 基于
-
Spring 事务中的隔离级别有哪几种?
TransactionDefinition
接口中定义了五个表示隔离级别的常量:-
TransactionDefinition.ISOLATION_DEFAULT
: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别. -
TransactionDefinition.ISOLATION_READ_UNCOMMITTED
: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 -
TransactionDefinition.ISOLATION_READ_COMMITTED
: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 -
TransactionDefinition.ISOLATION_REPEATABLE_READ
: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 -
TransactionDefinition.ISOLATION_SERIALIZABLE
: 最高的隔离级别,完全服从ACID
的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
-
-
# Spring Boot
-
优点?
- 开发基于
Spring
的应用程序很容易。 Spring Boot
项目所需的开发或工程时间明显减少,通常会提高整体生产力。Spring Boot
不需要编写大量样板代码、XML
配置和注释。Spring
引导应用程序可以很容易地与Spring
生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security
等。Spring Boot
遵循 “固执己见的默认配置”,以减少开发工作(默认配置可以修改)。Spring Boot
应用程序提供嵌入式HTTP
服务器,如Tomcat
和Jetty
,可以轻松地开发和测试web
应用程序。(这点很赞!普通运行 Java 程序的方式就能运行基于Spring Boot web
项目,省事很多)Spring Boot
提供命令行接口 (CLI
) 工具,用于开发和测试Spring Boot
应用程序,如Java
或Groovy
。Spring Boot
提供了多种插件,可以使用内置工具 (如Maven
和Gradle
) 开发和测试Spring Boot
应用程序。
- 开发基于
-
介绍一下
@SpringBootApplication
注解?可以看出大概可以把
@SpringBootApplication
看作是@Configuration
、@EnableAutoConfiguration
、@ComponentScan
注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:@EnableAutoConfiguration
:启用 SpringBoot 的自动配置机制@ComponentScan
: 扫描被@Component
(@Service
,@Controller
) 注解的 bean,注解默认会扫描该类所在的包下所有的类。@Configuration
:允许在上下文中注册额外的 bean 或导入其他配置类。
# Mybatis
-
#{}
和${}
的区别是什么?${}
是Properties
文件中的变量占位符,它可以用于标签属性值和sql
内部,属于静态文本替换,比如${driver}
会被静态替换为com.mysql.jdbc.Driver。
#{}
是 sql 的参数占位符,Mybatis
会将sql
中的#{}
替换为?
号,在sql
执行前会使用PreparedStatement
的参数设置方法,按序给sql
的?
号占位符设置参数值,比如ps.setInt(0, parameterValue),#{item.name}
的取值方式为使用反射从参数对象中获取item
对象的name
属性值,相当于param.getItem().getName()
。在预编译处理,可以有效的防止SQL
注入。
-
XML
映射文件中,除了常见的select|insert|update|delete
标签之外,还有哪些标签?- 还有很多其他的标签,
<resultMap>
、<parameterMap>
、<sql>
、<include>
、<selectKey>
,加上动态sql
的 9 个标签,trim|where|set|foreach|if|choose|when|otherwise|bind
等,其中为sql
片段标签,通过<include>
标签引入sql
片段,<selectKey>
为不支持自增的主键生成策略标签。
- 还有很多其他的标签,
-
Mybatis
是如何实现分页的?分页插件的原理是什么?RowBounds
:在mapper.java
中传入RowBounds
对象,它是针对ResultSet
结果集执行的内存分页,而非物理分页。PageHelper
插件:使用Mybatis
提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql
,然后重写sql
,根据dialect
方言,添加对应的物理分页语句和物理分页参数
-
Mybatis
能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及他们之间的区别?能,
Mybatis
不仅可以执行一对一、一对多的关联查询,还可以执行多对一,多对多的关联查询,多对一查询,其实就是一对一查询,只需要把selectOne()
修改为selectList()
即可;多对多查询,其实就是一对多查询,只需要把selectOne()
修改为selectList()
即可。关联对象查询,有两种实现方式,一种是单独发送一个
sql
去查询关联对象,赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join
查询,一部分列是A
对象的属性值,另外一部分列是关联对象B
的属性值,好处是只发一个sql
查询,就可以把主对象和其关联对象查出来。那么问题来了,
join
查询出来 100 条记录,如何确定主对象是 5 个,而不是 100 个?其去重复的原理是<resultMap>
标签内的<id>
子标签,指定了唯一确定一条记录的 id 列,Mybatis 根据列值来完成 100 条记录的去重复功能,<id>
可以有多个,代表了联合主键的语意。同样主对象的关联对象,也是根据这个原理去重复的,尽管一般情况下,只有主对象会有重复记录,关联对象一般不会重复。
-
Mybatis
的一级、二级缓存:(1)一级缓存:基于
PerpetualCache
的HashMap
本地缓存,其存储作用域为Session
,当Session flush
或close
之后,该Session
中的所有Cache
就将清空,默认打开一级缓存。(2)二级缓存与一级缓存其机制相同,默认也是采用
PerpetualCache,HashMap
存储,不同在于其存储作用域为Mapper(Namespace)
,并且可自定义存储源,如Ehcache
。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable
序列化接口 (可用来保存对象的状态), 可在它的映射文件中配置; (3)对于缓存数据更新机制,当某一个作用域 (一级缓存
Session/
二级缓存Namespaces
) 的进行了C/U/D
操作后,默认该作用域下所有select
中的缓存将被clear
掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。 -
Mybatis
的延迟加载?
Mybatis
仅支持 association
关联对象和 collection
关联集合对象的延迟加载, association
指的就是一对一, collection
指的就是一对多查询。在 Mybatis
配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。
它的原理是,使用 CGLIB
创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName()
,拦截器 invoke()
方法发现 a.getB()
是 null
值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b)
,于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()
方法的调用。这就是延迟加载的基本原理。
-
Mybatis
如何执行批处理?使用
BatchExecutor
完成批处理。 -
Mybatis
为什么是半自动ORM
映射框架,与全自动映射区别在哪?Hibernate
属于全自动ORM
映射工具,使用Hibernate
查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis
在查询关联对象或关联集合对象时,需要手动编写sql
来完成,所以,称之为半自动ORM
映射工具。
# 十: Linux
# 十一:Go
# 十二:算法
# 链表
- 空间代价为 O(1)去除链表种重复的节点。
- 判断链表有无环。
- 最小连续子序列之和 >=target.
- 堆排序
- 桶排序
- 快速排序
- 二分查找
- K 个一组进行链表反转.
- Z 型遍历二叉树
- 跳台阶
- 最大乘积的子数组
# 十三:场景题
select…for update 会锁表还是锁行。 select 查询语句是不会加锁的,但是 select for update 除了有查询的作用外,还会加锁呢,而且它是悲观锁。 那么它加的是行锁还是表锁, 没用索引 / 主键的话就是表锁,否则就是是行锁。