注:有的同学反馈掘金的格式不好看,所以我把这个文档放到了飞书上,格式好看了很多,而且可以通过评论一起讨论,申请权限即可观看,链接如下: bytedance.feishu.cn/docx/YWCFd3…
Java 基础
语言特性
Q1:Java 语言的优点?
① 平台无关性,摆脱硬件束缚,"一次编写,到处运行"。(跨平台特性)
② 相对安全的内存管理和访问机制,避免大部分内存泄漏和指针越界。(安全性)
③ 热点代码检测和运行时编译及优化,使程序随运行时间增长获得更高性能。(优化性)
④ 完善的应用程序接口,支持第三方类库。(扩展性)
Q2:Java 如何实现平台无关?
JVM ( Java Virtual Machine(Java虚拟机) ) : Java 编译器(javac)可生成与计算机体系结构无关的字节码(.class)指令,字节码文件由 JVM 动态地转换成本地机器代码,因为不同的操作系统使用不同的 JVM 映射规则,屏蔽了不同操作系统的差异。
语言规范: 基本数据类型大小有明确规定,例如 int 永远为 32 位,而 C/C++ 中可能是 16 位、32 位,也可能是编译器开发商指定的其他大小。Java 中数值类型有固定字节数,二进制数据以固定格式存储和传输,字符串采用标准的 Unicode 格式存储。
Q3:JDK 和 JRE 的区别?
JDK:Java Development Kit,开发工具包。提供了编译运行 Java 程序的各种工具,包括编译器、JRE 及常用类库,是 JAVA 核心。
JRE:Java Runtime Environment,运行时环境。运行 Java 程序的必要环境,包括 JVM、核心类库、核心配置工具。
JDK包含JRE包含JVM
Renntranlock就是JDK实现的。
(JDK提供运行工具,JRE提供运行环境)
Q4:Java 按值调用还是引用调用?
按值调用: 指方法接收调用者提供的值的拷贝。
按引用调用: 指方法接收调用者提供的变量地址值。
Java 总是按值调用(这里的按值调用是指按照基本数据类型值的拷贝和引用类型数据地址值的拷贝来调用的),方法得到的是所有参数值的副本,传递对象时实际上方法接收的是对象引用的副本。方法不能修改基本数据类型的参数,如果传递了一个 int 值 ,改变值不会影响实参,因为改变的是值的一个副本。
可以改变对象参数的状态,但不能让对象参数引用一个新的对象。如果传递了一个 int 数组,改变数组的内容会影响实参,而改变这个参数的引用并不会让实参引用新的数组对象。
Q5:浅拷贝和深拷贝的区别?
浅拷贝: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。修改克隆对象可能影响原对象,不安全。
深拷贝: 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象,这2个对象是相互独立的,也就是2个不同的地址,安全。
(深拷贝和浅拷贝最根本的区别在于拷贝引用类型的对象时,是否开辟一块儿新的内存存放新对象的新地址,而不是引用。)
Q6:什么是反射?
在运行状态中(运行期),对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。缺点是破坏了封装性以及泛型约束。反射是框架的核心,Spring 大量使用反射。
Q7:Class 类的作用?如何获取一个 Class 对象?
在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机(JVM)利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。(主要应用于反射中)
获取 Class 对象的方式:
① 类名.class 。
②对象的 getClass方法。
③ Class.forName(类的全限定名)。
Q8:什么是注解?什么是元注解?
注解 (也被称为元数据(Annonation)): 可以把注解理解为代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。(是对源代码的一种补充)
元注解是自定义注解的注解,例如:
@Target:约束作用位置。
@Retention:约束生命周期。
@Documented:表明这个注解应该被 javadoc 工具提取成文档。
Q9:什么是泛型,有什么作用?
泛型:本质是参数化类型,也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。然后在使用/调用时传入具体的类型(类型实参)。
泛型在定义处只具备执行 Object 方法的能力。
泛型的好处:
① 类型安全,放置什么出来就是什么,不存在 ClassCastException。(安全性) 类转化异常
② 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。(可读性) 写代码的时候就知道了类型。
③ 代码重用,合并了同类型的处理代码。(重用性)
Q10:泛型擦除是什么?
泛型信息只存在于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象(即类型擦除会发生在运行阶段),所有对象都属于普通类。例如定义 List<Object> ****或 List<String> ,在编译后都会变成 ****List 。(在运行阶段,在执行list.add(“a”)后,list将被指定为String类型)
好处:泛型擦除是泛型能够与JDK5版本之前代码兼容共存的原因。
Q11:JDK各个版本新特性有哪些?
略,基本了解下JDK8就好
Q12:异常有哪些分类?
所有异常都是 Throwable 的子类,分为 Error 和 Exception。
Error: 是 Java 运行时系统的内部错误和资源耗尽错误,例如 StackOverFlowError(内存泄漏) 和 OutOfMemoryError(内存溢出),这种异常程序无法处理。(程序本身不能处理的错误)
Exception: (程序自身能处理的错误)****
可查异常:(在编译期出现,如果不改正则无法运行)
① 无能为力型,如字段超长导致的 SQLException。
② 力所能及型,如未授权异常 UnAuthorizedException,程序可跳转权限申请页面。常见受检异常还有 FileNotFoundException、ClassNotFoundException、IOException等。
非受检异常:(包括运行期异常和error)
① 可预测异常,例如 IndexOutOfBoundsException、NullPointerException、ClassCastException 等,这类异常应该提前处理。
② 需捕捉异常,例如进行 RPC 调用时的远程服务超时,这类异常客户端必须显式处理。
③ 可透出异常,指框架或系统产生的且会自行处理的异常,例如 Spring 的 NoSuchRequestHandingMethodException,Spring 会自动完成异常处理,将异常自动映射到合适的状态码。
Q13:静态内部类会被编译成几个class?为什么内部类可以访问外部类的private的方法?
两个;1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用;
2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为1中添加的成员变量赋值;
3 在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用。
Q14:finally方法一定会被执行吗?
一般情况下,finally会在try和catch之后执行,但是以下情况例外:
- 执行了Thread.currentThread().suspend();
- 执行了 System.exit()
- 执行了 Runtime.getRuntime().halt(exitStatus) 、 Runtime.getRuntime().exit(0)
- JVM 崩溃
- OS(操作系统)结束了JVM线程。比如在Unix上使用 kill 指令
- 操作系统死掉。比如突然断电
以下情况finally会继续执行:
- Thread.currentThread().interrupted(); Thread.currentThread().destroy(); Thread.currentThread().stop()
- Thread.sleep(10);
- Runtime.getRuntime().addShutdownHook(Thread.currentThread());
\
数据类型
Q1:自动装箱/拆箱是什么?装箱/拆箱用了什么方法?为什么需要包装类?对性能有影响吗?
发生在编译时期。
每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可。
自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。(自动将int类型转换成Integer类型)
自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。
Integer[] in=new Integer{“good”}; int a=in[0];
比较两个包装类数值要用 equals ,而不能用 == 。(equals比较的是值,==作用于基本类型时比较的是值,其他情况下比较的是地址)
拆箱/装箱用了什么方法?对性能有影响吗?
Integer integer=Integer.valueOf(1); 装箱
int i=integer.intValue(); 拆箱
对性能有影响,要避免。
为什么要有包装类?
因为Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是Object类型。
为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
Q2:字符串拼接的方式有哪些?
① 直接用 + ,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 + 拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。
② 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +。
Arrays.copyof //产生新的数组
system.arraycopy //原来位置拷贝
Array.copyOf()可以看作是受限的System.arraycopy(),它主要是用来将原数组全部拷贝到一个新长度的数组,适用于数组扩容。
③ 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。
Q3:String 是不可变类为什么值可以修改?
不可变:String的底层是用一个字节型的value数组储存的。这个数组由final修饰,并且String内部没有改变value数组值的方法。String本身使用final修饰的,无法被继承破坏。
可以被修改:创建了一个新的String值为“abc”,实质是将其指向了一个堆中值为“abc”的字符串。而修改它的值,是将其指向了堆中其他的字符串。而“abc”的值没有发生改变。
(String a=”hello”; a=”world”;在这里,a只是一个指向hello字符串的引用,hello一旦创建其地址就不可改变了,而string中没有改变其内部数组数据的方法,故而string类型数据一旦创建无法改变,a=”world”是将引用指向了一个新的字符串,而原先的”hello”并没有被改变)
Q4:String a = "a" + new String("b") 创建了几个对象?
(一共创建了4个对象)常量和常量拼接仍是常量,结果在常量池(基本数据类型和final修饰的变量,在编译期都存储在常量池中),只要有变量参与拼接结果就是变量,存在堆。
使用字面量时只创建一个常量池中的常量,使用 new 时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量。因此 String a = "a" + new String("b") 会创建四个对象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。
(new String("b")的创建过程:在常量池中创建”b”对象,在堆中创建一个匿名对象引用常量池中的”b”对象)
Q5:String Stringbuilder Stringbuffer 区别
运行速度方面: Stringbuilder>Stringbuffer>String
String是final类不能被继承且为字符串常量,每次拼接时都要创建新对象,运行速度最慢;Stringbuilder和Stringbuffer为字符串变量,可以更改,并通过append方法实现拼接,运行速度较快;但是Stringbuffer中的大多数方法是由synchronized修饰的,运行速度受到同步影响,相较Stringbuilder更慢。
线程安全的方面:
String是final类,一旦创建不可改变,所有的操作都不会对其作出改变,是线程安全的。
Stringbuffer中的大多数方法是由synchronized修饰的,是线程安全的。
Stringbuilder中没有final修饰也没有synchronized来修饰方法,是非线程安全的。
总结:
String适用于少量的字符串操作;Stringbuffer适用于多线程大量字符串操作;Stringbuilder适用于单线程大量字符串操作。
Q6:Static关键字
Static可以用于修饰成员变量,成员方法,代码块(静态代码块):
成员变量: Static修饰的成员变量和类绑定,不再由对象来管理,由同一个类创建的多个对象共享同一个static成员变量
成员方法: Static修饰的成员方法同样和类绑定,调用成员方法时不再需要创建对象,直接类名 . 方法名即可(静态方法只能调用静态方法)
代码块: Static修饰的代码块称为静态代码块,在初始化过程中被先加载,只初始化一次。
Q7:Final关键字
不可变。具体修饰不同的东西,有以下情况:
Final可以用来修饰类,成员变量和成员方法:
类: 用final修饰的类被称为最终类,不可以被继承
成员变量: 被final修饰的变量,如果是一个基本类型的变量,则这个基本类型变量的值不可以改变;如果是一个引用类型的变量,则这个引用类型变量的地址不可以改变,但是保存在该地址下的内容可以改变(被final修饰的变量会在常量池中保存一个副本)
成员方法: 被final变量修饰的成员方法不可以被重写
Q8:Private关键字
private可以用来修饰成员变量和成员方法:
成员变量: Private修饰的成员变量在外部类中只能通过get/set方法进行访问
成员方法: Private修饰的成员方法不会被外部类访问到
面向对象
Q1:谈一谈你对面向对象的理解(什么是面向对象)
(面向对象编程即OOP,Object Oriented Programming)面向对象是把整个需求按照特点、功能划分,将这些存在共性的部分封装成对象,创建了对象不是为了完成某一个步骤,而是描述某个事物在解决问题的步骤中的行为。
(面向过程强调一步步按流程来完成,但是不易维护,复用和扩展,面向对象具有低耦合性,但每个类在调用时都要先实例化,开销较大,但是便于维护和扩展)
Q2:面向对象的三大特性?
(封装,继承,多态)
封装:就是把对象的属性和操作(或服务)结合为一个独立的整体,形成一个黑箱,并尽可能隐藏对象的内部实现细节。
比如我们的USB接口。如果我们需要外设且只需要将设备接入USB接口中,而内部是如何工作的,对于使用者来说并不重要。而USB接口就是对外提供的访问接口。
封装的思想保证了类内部数据结构的完整性,使用户无法轻易直接操作类的内部数据,这样降低了对内部数据的影响,提高了程序的安全性和可维护性。
(对不想公开的属性和方法采用private修饰,可以设置getter/setter来调用private属性)
继承: 使用extends关键字,子类可以使用父类中定义的一些属性和方法,java只允许单继承,即一个子类只能有一个父类。有利于代码的扩展和复用。
多态:把子类对象主观的看作是其父类型的对象,那么父类型就可以是很多种类型。多态指在编译层面无法确定最终调用的方法体,在运行期由 JVM 动态绑定,调用合适的重写方法。
(多态一般和父类方法重写相关联,如有,a,b,c均继承自d,d中有run()方法,子类中均重写了run()方法,将这些子类封装到一个list数组中,在遍历这些子类时,无法确定子类类型,只能确定父类类型。可以通过多态来调用各自的run方法)
Q3:重载和重写的区别?
重写:子类覆盖父类的方法,要求方法名,参数列表都相同,方法体可以不同。是在两个类中发生的。
重载:一个类中有两个或两个以上的方法,方法名相同,但参数和方法体不同的方法。是在同一个类中的。
(父类中的构造方法可以被重载,但是不能被重写,类的构造方法必须和类名一致,但子类和父类类名不同,所以不能被重写。重载是在同一个类中发生的,所有可以实现重载。)
Q4:类之间有哪些关系?
继承:子类和父类之间的关系。
实现:接口和实现类之间的关系。
组合:头部和身体之间的关系,要在身体的构造方法中创建头部对象。
聚合:暂时组装,狗和绳子的关系,在构造方法中使用,但不在构造方法中创建。
关联:平等的关系,不在构造方法中使用。
依赖:在普通方法中使用。
Q5:内部类的作用是什么,有哪些分类?
作用:内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。(封装性)也可以用于实现多继承。
静态内部类: 和静态方法并列,只加载一次。作用域仅在包内,可通过 外部类名.内部类名 直接访问,类内只能访问外部类所有静态属性和方法。
成员内部类: 就是位于外部类成员位置的类。与外部类的属性、方法并列,可访问外部类的所有内容。
局部内部类: 定义在一个方法或者一个作用域里面的类。局部内部类中不可定义静态变量,可以访问外部类的局部变量(即方法内的变量),但是变量必须是final的。
匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。(本质是继承该类或者实现接口的子类匿名对象。)
Q6:接口和抽象类的异同?
相同点:都不能被实例化(因为他们的抽象方法中没有方法体,JVM无法为其分配具体的内存空间,为了安全性,不能被实例化)。抽象方法都必须被重写。
不同点:
1.抽象类和子类是IS-A关系,要满足里氏替换原则,即子类对象必须可以完全替换所有的父类对象。接口和子类更像一种LIKE-A关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
接口理解成一种规范,一种协议,告诉你要做这些事。
2.从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
3.接口的字段(成员变量)只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
4.接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。****
接口理解成一种规范,一种协议,告诉你要做这些事。
****里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。即父类出现的地方,子类也可以出现,注意区分抽象类和子类。不能直接创建抽象类对象,必须用一个子类来继承抽象父类。子类必须覆盖重写父类中的所有抽象方法。创建子类对象进行应用。
Q7:子类初始化的顺序
① 父类静态代码块和静态变量。
② 子类静态代码块和静态变量。
③ 父类普通代码块和普通变量。
④ 父类构造方法。
⑤ 子类普通代码块和普通变量。
⑥ 子类构造方法。
(按照先父后子,先静态,后普通的方式,静态块和静态变量是并列的,谁在前谁最先。接下来,除了main 静态方法都不会执行)
Q8:单例模式
单例模式的5种实现方法及优缺点
单例模式是GoF23种设计模式中创建型模式的一种,也是所有设计模式中最简单的一种。
但是最简单的设计模式,也有很多小细节可以讲。
单例模式是用来保证系统中某个资源或对象全局唯一的一种模式,比如系统的线程池、日志收集器等。它保证了资源在系统中只有一份,一方面节省资源,另一方面可以避免并发时的内存安全问题。
(1)饿汉模式
public class Singleton {
//必须构造方法私有化,这样的话,才能保证单例,final保证不能修改
private static final Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
饿汉模式是在类加载的时候直接创建了对象,不存在线程安全问题,它的优点是简单方便,缺点是如果这个类在不需要创建单例对象的时候就会装载(比如这个类有其它静态方法被调用)的话,会造成浪费,不过只要遵循单一职责原则,一般不会有这种问题。
所以只要不存在奇怪情况,推荐用饿汉模式。
(2)懒汉模式(不推荐)
public class Singleton {
private static Singleton INSTANCE;//不加final是因为,如果是final static 成员变量,必须直接赋值 或者在 静态代码块中赋值。
//必须构造方法私有化,这样的话,才能保证单例
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
与饿汉模式相比,变量定义上的final被去掉,getInstance()方法中增加了一次null判断,很显然,有线程安全问题,与饿汉模式相比,懒汉模式可以在实例化过程中通过外部传递参数的形式控制实例化对象。 宁愿用饿汉模式也不要用懒汉模式,不推荐使用。
(3)双重锁懒汉模式
public class Singleton {
//volatile
private volatile static Singleton INSTANCE;
private Singleton() {
}
//直接用sync修饰的话,只能有一个线程获取对象,性能较差
public static Singleton getInstance() {
//只有为空的时候才可以获取锁。
if (INSTANCE == null) {
synchronized (Singleton.class) {
//不加这个的话,也是线程不安全的,可能有两个对象
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
与上面的懒汉模式相比,变量定义上增加了volatile避免指令问题,getInstance()方法中有加了锁,里面又加了一次null判断,防止两个线程同时执行getInstance()且两个线程第一个null都判断通过,但一个线程还没拿锁就被另一个线程先拿了锁之后重复创建对象的情况。
与饿汉模式相比,几乎适用于所有情况,但复杂度上升。
如果需要兼顾大多数的情况,可以使用双重锁懒汉模式。
(4)静态内部类模式
public class Singleton {
private Singleton() {
}
//这里是一个静态内部类
private static class SingletonInnerClass{
private static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInnerClass.INSTANCE;
}
}
与饿汉模式相比,实例定义属性放到了内部类中,Singleton类加载后并不会立即加载内部类,只有第一次调用getInstance()方法后,内部类才会被加载,不存在线程安全问题,比饿汉模式适应了更多的情况,复杂度比双重锁懒汉模式低,和饿汉模式一样,外部无法控制实例初始化的过程,无法传递参数进去。
如果单例类需要在脱离单例对象的情况下使用,可以用静态内部类模式。
类只能同时被1个线程初始化,且在同一个加载器中,同一个类不会第二次初始化。所以饿汉模式和静态内部类模式都没有线程安全问题
(5)枚举单例模式
public enum Singleton {
INSTANCE;
private String field1;
private Integer field2;
public void method1(){
// ...
}
public void method2(){
// ...
}
}
在java中,enum可以像一个正常的class一样定义属性和方法,可以实现接口,但不能继承。
虽然它不复杂,且无法通过反射搞破坏,但它无法适用于更广泛的情况。
如果单例模式可能被破坏,可以使用枚举单例模式。
总结
上面5种方式中,比较推荐饿汉模式,它虽然可能有资源浪费,但这个可能性真的很小,最重要的是它实现起来非常简单。双重校验锁可以提高效率同时保证安全性
Q9:Lamda表达式
注意事项:
集合
Q1:说一说 ArrayList
(ArrayList底层是利用数组实现的,用一个命名为elementDate的数组保存数据,每次添加和删除数据都需要创建一个新数组。是可扩容的非线程安全容器)
非线程安全:在添加与删除操作时,是使用Arrays.copyOf()方法返回一个新的数组对象,如果两个线程,线程a,线程b同时对一个list进行添加或删除操作,则会出现两步操作都在原有数组上进行,导致添加或删除数据不成功。
可扩容:通过Arrays.copyOf()方法实现,如果添加元素后,容量大于原数组长度,则将原数组容量变为原来的1.5倍。
(由ArrayList的特性可知,在查询元素时,其查询速度很快,但在插入与删除元素时,由于其底层的数组结构,速度很慢。)
如何得到线程安全的list:
1.可以使用 Collections.synchronizedList();` 得到一个线程安全的 ArrayList**。
List list = new ArrayList<>();
List synList = Collections.synchronizedList(list);
2.也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
List list = new CopyOnWriteArrayList<>();
写的时候复制,读的时候直接读。
-
内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
-
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
如果既要保证线程安全又要保证效率,那就不能使用Collections.synchronizedList(),换为写时复制CopyOnWriteArrayList容器,就是在多线程的情况之下,每一个线程进行添加的时候都会复制一个新的容器给这个线程,保证一个容器只有一个线程,这样就保证了他的线程安全,还不影响他的效率,但是内存的占用却是大大的增加了
Q2:说一说 LinkedList
(LinkedList 底层是使用双向链表实现的,在每一个node节点中保存数据,是非线程安全的)
非线程安全的原理与ArrayList类似,查询元素速度慢,但插入和删除元素的速度相对更快,且内存利用率更高。
Q3:说一说 散列和散列码
散列:散列也叫散列函数,是一种可以将任意长度的输入转换为固定长度输出的算法,因此不同的输入可能产生相同的输出。
散列码:散列码就是按照散列函数生成的结果。
1,散列和散列码应用于HashMap、HashSet(使用HashMap实现的)、LinkedHashMap或者LinkedHashSet中,目的是提高查询速度。(用数组来保存键,将对象的散列码作为数组的下标,在数组中保存值的list集合,并通过equals方法进行线性查询)
2, 散列码不是独一无二的,但是通过hashcode()和equals()方法可以完全确定对象身份
3,如果想要使用自己定义的类作为Map的键时,必须覆盖重写hashcode()和equals()方法。原因:默认equals方法比较的是地址。而默认的hashcode是基于地址值生成的,作为Map的Key的话,要满足两个条件,一个是要用hashcode来进行比较是否相等,一个是要用hashcode代表他存的值,而不是地址值,所以要都重写。
如何避免哈希冲突?
1.开放定址法开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
2.链地址法将哈希表的每个单元作为链表的头结点,所有哈希地址为i的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头节点的链表的尾部。
3.再哈希法当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不在产生为止。
4.建立公共溢出区将哈希表分为基本表和溢出表两部分,发生冲突的元素都放在溢出表中。
还有String利用31会变得均匀。
Q4:equals()应满足的条件
1) 自反性 对于任意x, x.equals(x)必定返回true。
2) 对称性 对于任意x,y,x.equals(y)与y.equals(x)的返回值相同。
3) 传递性 对于任意x,y,z,如果x.equals(y)与y.equals(z)返回值相同,那么x.equals(z)返回值也相同。
4) 一致性 对于任意的x,y,无论x.equals(y)执行多少次,返回值要么是true,要么为false。
5)对于任意x != null, x.equals(null)返回false。
Q5:说一说 HashMap
(HashMap底层是使用数组+链表实现的,JDK8 改为数组 + 链表/红黑树,节点类型从Entry 变更为 Node。是可扩容非线程安全的)
transient HashMap.Node<K, V>[] table;底层用的是静态内部类Node的数组。
HashMap在初始化的时候,假如制定的长度不是2的n次方,那么会自动的转化为2的n次方。
实现原理:底层是一个存放Entry的数组,Entry是HashMap中的一个静态内部类。当put一个键值对时,会对key值进行哈希运算,生成一个哈希值,这个值对应数组中一个下标的位置。在该位置上,用一个Entry类型的数据结构来保存key,value,next,哈希值。当出现key值不同,但是散列值相同的情况时,会在同一下标的位置维护一个链表,通过前面的next指定下一个节点。当这个链表长度超过8,会转化为红黑树。
因此,在JDK1.8中,如果冲突链上的元素数量大于8,并且哈希桶数组的长度大于64时,会使用红黑树代替链表来解决哈希冲突,此时的节点会被封装成TreeNode而不再是Node。
查找的时候,通过equals方法寻找不同key对应的value。
扩容:扩容的过程,涉及到了四个参数。容量,Entey数组的长度,装载因子,阈值,假如数组的长度大于阈值,也就是容量乘以装载因子,那么就会进行扩容。容量变为原来的2倍,容量默认值为16,默认加载因子为0.75。扩容以后,需要重新计算桶的下标。其中小于前容量的哈希值的对应元素,不需要重新进行计算。大于的直接加上容量值即可。例如初始容量为16,小于16的不需要变化,大于16的加上16即可。
此外,它设置了中间缓存链表,这种实现降低了对共享资源newTab的访问频次,先组织冲突节点,最后再放入newTab的指定位置。避免了JDK1.8之前每遍历一个元素就放入newTab中,从而导致并发扩容下的死链问题。
E:\Desktop\My Learning\刷题代码\hashmap.txt
不允许存储相同key:因为通过hashcode()和equals()方法可以确定唯一的key,key的位置固定,存入相同的key时会产生覆盖,但可以存入相同的value(如(1,2)和(2,2))。
哈希冲突: 当不同元素具有相同的散列码时就会发生哈希冲突,解决办法是使用链表存储元素,同时在定义散列码时,尽量使其分散。
为什么重写equals方法需同时重写hashCode方法?
原始的equals方法比较的是地址值,原始的hashCode方法是用地址值算出来的。只有把hash改成基于当前的值生成,equals改成比较当前的hash值才可以。
非线程安全:因为底层是用数组维护的所以跟list一样,也存在线程安全的问题。在进行扩容操作的时候,当A、B线程同时进来,检测到总数量超过阈值的时候就会同时触发 resize 操作,各自生成新的数组并 rehash 后赋给该 map 底层的数组,结果最终只有最后一个线程生成的新数组被赋给该 map 底层,其他线程的均会丢失。这样当获取一个不存在的 key 时,计算出的 index 正好是环形链表的下标就会出现死循环。
JKD7中头插法,会把元素插入在头部。扩容的时候,首先将容量变为2倍。
2->3,3->2
解决:ConcurrentHashMap
jdk1.7:针对HashTable中锁粒度过粗的问题,在JDK1.8之前,ConcurrentHashMap引入了分段锁机制。整体的存储结构如下图所示,在原有结构的基础上拆分出多个segment,每个segment下再挂载原来的entry(上文中经常提到的哈希桶数组),每次操作只需要锁定元素所在的segment,不需要锁定整个表。因此,锁定的范围更小,并发度也会得到提升。
其中的核心数据 val next 都用了 volatile 修饰,保证了可见性。
jdk1.8中抛弃了分段锁。因为性能还可以优化。
1.而采用了 CAS + synchronized 来保证并发安全性。锁着的是根节点。
CAS读的时候用,写的时候用。
2.其中的核心数据 val next 都用了 volatile 修饰,保证了可见性。
ConcurrentHashMap是否要加锁?
不需要,val next 都用了 volatile 修饰,可以保证多线程情况下的可见性。
ConcurrentHashMap中的value为什么不为空?
因为他工作于多线程环境,如果get方法返回空的话,无法判断值是null,还是没有该key,但是单线程下可以用containsKey(key)判定。
ConcurrentHashMap是强一致性还是弱一致性?
HashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也将这个判断留给用户自己决定是否使用ConcurrentHashMap。get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿
jdk7和jdk8中有什么区别?
● 底层数据结构:JDK7 底层数据结构是使用 Segment 组织的数组 + 链表,JDK8 中取而代之的是数组+链表+红黑树的结构,在链表节点数据大于8 (且数据总量大于等于 64 )时,会将链表转化为红黑树进行存储。
● 查询时间复杂度:从 JDK7 的遍历链表 O(n),JDK8 变成遍历红黑树 O(logN)。
● 保证线程安全机制:JDK7 采用 Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK8 采用 CAS + synchronized 保证线程安全
● 锁的粒度:JDK7 是对需要进行数据操作的 Segment 加锁,JDK8 调整为对每个数组元素的头节点加锁。
Q6:说一说 TreeMap
(TreeMap底层是使用红黑树实现的,增删改查的平均和最差时间复杂度均为O(log(n)),最大特点是 Key 有序。Key 必须实现 Comparable 接口或提供的 Comparator 比较器,所以 Key 不允许为 null。不允许出现相同的key)
红黑树(二叉查找树) :任意节点的值大于其左子树节点的值,小于其右子树节点的值。
插入元素时的比较规则:如果要插入的元素比当前元素小就到左子树去比较,如果比当前元素大,就到右子树去比较,直到当前元素的左或者右子树为空,就插入此元素。如果在比较过程中,出现当前元素等于要插入的元素,那么此元素不插入。(从根节点开始比较,大就去右子树比较,小就去左子树比较,相同则更新,不会出现相同的key)
Q7:Set 有什么特点,有哪些实现?
Set 不允许元素重复且无序,常用实现有 HashSet、LinkedHashSet 和 TreeSet。
HashSet : (通过 HashMap 实现),HashMap 的 Key 即 HashSet 存储的元素,所有 Key 都使用相同的 Value ,一个名为 PRESENT 的 Object 类型常量。使用 Key 保证元素唯一性,但不保证有序性,和线程安全性。
LinkedHashSet : (继承自 HashSet,通过 LinkedHashMap 实现),使用双向链表维护元素插入顺序。
TreeSet : (通过 TreeMap 实现的),添加元素到集合时按照比较规则将其插入合适的位置,保证插入后的集合仍然有序。
Q8:HashMap、Hashtable、ConcurrentHashMap的原理与区别(类似于ArrayList,Collections.syncArrayList,CopyOnWriteArrayList)
Segment数组的,其实和Entry类似。底层采用分段的数组+链表实现,线程安全
通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
Q9 for、foreach和iterator的区别
1.for循环一般用来处理比较简单的有序的,可预知大小的集合或数组
2.foreach可用于遍历任何集合或数组,而且操作简单易懂,他唯一的不好就是需要了解集合内部类型
3.iterator是最强大的,他可以随时修改或者删除集合内部的元素,并且是在不需要知道元素和集合的类 型的情况下进行的(原因可参考第三点:多态差别),当你需要对不同的容器实现同样的遍历方式时,迭代器是最好的选择!
可以看到,foreach语法最终被编译器转为了对Iterator.next()的调用。而作为使用者的我们, jdk并没用向我们暴露这些细节,我们甚至不需要知道Iterator的存在,认识到jdk的强大之处了吧。
Iterator it = arr.iterator()
while(it.hasNext())
{
object o =it.next();
...
}
IO流
Q1:同步/异步/阻塞/非阻塞 IO 的区别?
同步和异步是通信机制,阻塞和非阻塞是调用状态。
同步 IO: 用户发起一次IO操作并采用等待或着轮询的方式查看IO操作是否就绪。
异步 IO:进程触发IO操作后可以离开当前页面去做别的事情。当后台处理完成,操作系统会通知事件和回调机制等通知相应的线程进行后续操作。
阻塞 IO: 在读写数据过程中会发生阻塞现象。当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,并返回结果给用户线程,用户线程才解除block状态。
非阻塞 IO:当用户线程发起read操作后,并不需要等待,而是马上就得到一个结果。如果结果是error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到用户线程,然后返回。非阻塞IO不会交出CPU,而是一直占用CPU。
Q2:什么是 BIO?
(BIO是最传统的IO流(blocking IO),同步阻塞IO模式,是面向流的操作)
BIO是一请求一应答模式,服务器需要为每一个客户端请求创建一个线程,当一个客户端进行读写操作时,不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成,不过可以通过多线程来支持多个客户端的连接。
Q3:什么是 NIO?
(NIO是新型的(NEW IO或NO-blocking IO),同步非阻塞IO模型,它是面向缓冲区的,基于通道的I/O操作方法)
NIO是利用缓存区和通道进行数据的读写的,通过通道和缓存区的交互完成,在读写过程中,单线程中从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是一样的。(不需要等待数据完全读取或写入)
BIO 中的三个核心组件:
1 ,Buffer(缓冲区) :本质是一块可读写数据的内存,用来简化数据读写。
2 ,Channel(通道) :双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要通过 Buffer 来读写数据,也可以和其他 Channel 交互。
3 ,Selectors(选择器) :多路复用器,轮询检查多个 Channel 的状态,即判断 Channel 是否处于可读或可写状态(可以用单线程处理多个通道,提高系统利用率)
Q4:什么是 AIO?
(AIO (Asynchronous I/O)是 JDK7 引入的异步非阻塞 IO)
异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。
Q5:java.io 包下有哪些流?
主要分为字符流和字节流,字符流一般用于文本文件,字节流一般用于图像或其他文件。(字节流以8位2进制为单位,可以处理所有类型的数据;字符流以16位2进制为单位,只能处理字符类型的数据)
字符流包括了字符输入流 Reader 和字符输出流 Writer。
字节流包括了字节输入流 InputStream 和字节输出流 OutputStream。
字符流和字节流都有对应的缓冲流,字节流也可以包装为字符流,缓冲流带有一个 8KB 的缓冲数组,可以提高流的读写效率。除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流 ByteArrayInputStream、文件流 FileInputStream 等。
Q6:序列化和反序列化是什么?
JAVA中的对象在JVM退出时会全部销毁,序列化就是通过将那些实现了Serializable接口的对象转换成一个字节序列并写入磁盘,达到将对象及状态持久化的特点;反序列化就是在重新调用程序时,从磁盘将字节流恢复为对象。(对象的序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化)
常见的三种序列化方式:
1 ,java原生序列化:
实现 Serializabale 标记接口,Java 序列化保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据,兼容性最好,但不支持跨语言,性能一般。
2 ,Hessian序列化:
Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java 对象序列化的二进制流可以被其它语言反序列化。Hessian 协议的特性:① 自描述序列化类型,不依赖外部描述文件,用一个字节表示常用基础类型,极大缩短二进制流。② 语言无关,支持脚本语言。③ 协议简单,比 Java 原生序列化高效。Hessian 会把复杂对象所有属性存储在一个 Map 中序列化,当父类和子类存在同名成员变量时会先序列化子类再序列化父类,因此子类值会被父类覆盖。
3 ,Json序列化:
JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。
(JSONObject json=JSONObject.fromObject(对象);//将java对象转换为json对象。
String str=json.toString;//将json对象转换为字符串。)
注: 对于某些私密的不想被永久保存的字段,可以使用transient关键字,其作用是将变量的生命周期仅限于内存,而不会写入到磁盘里持久化。
JVM
(JVM这部分后续在写文章详解中)
垃圾回收:juejin.cn/post/707527…
java字节码juejin.cn/post/708452…
内存区域划分
Q1:运行时数据区是什么?
虚拟机(JVM)在执行 Java 程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域有各自的用途、创建和销毁时间。(被按照用途不同而划分的内存空间)
线程私有:程序计数器、Java 虚拟机栈、本地方法栈。
线程共享:Java 堆、方法区(方法区中包含着运行时常量池)。
Q2:说一说 程序计数器
程序计数器: 是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器完成。是唯一在虚拟机规范中没有规定内存溢出情况的区域。 (类似于一个指向字节码解释器所读取行的指针,用以帮助字节码指示器逐行读取字节码文件,保存的是程序当前指令的地址)
Q3:说一说 java虚拟机栈
Java 虚拟机栈: 是用来描述 Java 方法的内存模型。每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。(和每个java方法绑定,用于存储方法中的局部变量,在方法中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配)
有两类异常:
① 线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError。
② 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OfMemoryError(HotSpot 不可动态扩展,不存在此问题)。
Q4:说一说 本地方法栈
本地方法栈: 与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法。(和本地方法绑定)
虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如 HotSpot 将虚拟机栈和本地方法栈合二为一。与虚拟机栈一样也会抛出Stack OverflowError异常和OutOfMemoryError异常。
Q5:说一说 java堆
堆: 是虚拟机所管理的内存中最大的一块,被所有线程共享的,在虚拟机启动时创建。堆用来存放对象实例(new出来的对象或数组) ,Java 里几乎所有对象实例都在堆分配内存。如果堆没有内存完成实例分配也无法扩展,抛出 OutOfMemoryError异常。
(堆用来存放对象实例,不要求连续内存和固定容量)
Q6:说一说 方法区
方法区: 用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。如果方法区无法满足新的内存分配需求,将抛出 OutOfMemoryError异常。
(方法区用来存放类型信息,常量,静态变量,同样是线程共享,不要求连续内存和固定容量)
Q7:说一说 运行时常量池
运行时常量池:是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。
运行时常量池相对于 Class 文件常量池的一个重要特征是动态性,Java 不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是 String 的 intern 方法。
(用于存放编译期就已经确定了的数据,如 int a=40;其中40就是字面量,字面量包括基本类型,基本类型包装类,String对象的值。)
三个常量池:class文件常量池,字符串常量池,运行时常量池
1.全局字符串值
全局字符串的值在类加载的时候完成,在堆中生成了字符串池示例,而将引用值存在字符串池中。
在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
2.class文件常量池
当java文件被编译成.class文件的时候生成的。我们都知道,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。一般包括下面三类常量:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
3.运行时常量池
是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。
Q8:直接内存是什么?
直接内存: 不属于运行时数据区,也不是虚拟机规范定义的内存区域(但是是JVM划分出来的一块系统内存区域),这部分内存被频繁使用,而且可能导致内存溢出。
NIO的Buffer提供了一个可以不经过JVM内存直接访问系统物理内存的类——DirectBuffer。 该类继承自ByteBuffer,但和普通的ByteBuffer不同,普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的限制;而DirectBuffer直接分配在物理内存中,并不占用堆空间,其可申请的最大内存受操作系统限制。
他不属于JVM运行时数据区,在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存。然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。本机直接内存的分配不会受到Java 堆大小的限制,受到本机总内存大小限制。配置虚拟机参数时,不要忽略直接内存 防止出现OutOfMemoryError异常
内存溢出
Q1:内存溢出和内存泄漏的区别
内存溢出 (Out Of Memory(OOM)):指程序在申请内存时,没有足够的内存空间供其使用。(可以理解为内存不够用了)
内存的引用,强制
程序计数器不会OOM
内存泄露 (Memory Leak):一个对象分配内存之后,在使用结束时未及时释放,导致一直占用内存,没有及时清理,使实际可用内存减少,就好像内存泄漏了一样。(可以理解为没有及时清理导致可用内存不断减少)
Q2:堆溢出的原因
堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,(例如在 while 死循环中一直 new 创建实例。)
注:Heap Dump(堆转储文件),用于记录JVM中堆内存运行的情况
处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出。
如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,并进行相关优化,如果不是内存泄漏,则检查JVM堆参数,看其与机械内存相比是否还有向上调整的空间。
Q3:栈溢出的原因?
如果线程请求的栈深度大于虚拟机所允许的深度,将产生栈溢出的情况。
OOM总结
什么是OOM
OOM为out of memory的简称,来源于java.lang.OutOfMemoryError,指程序需要的内存空间大于系统分配的内存空间,OOM后果就是程序crash;可以通俗理解:程序申请内存过大,虚拟机无法满足,然后自杀了。
导致OOM问题的原因
为什么会没有内存了呢?原因不外乎有两点:
1)分配的少了:比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少。
2)应用用的太多,并且用完没释放,浪费了。此时就会造成内存泄露或者内存溢出。
常见情况
java.lang.OutOfMemoryError: Java heap space ------>java堆内存溢出,此种情况最常见,一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx等修改。
java.lang.OutOfMemoryError: PermGen space 或 java.lang.OutOfMemoryError:MetaSpace ------>java方法区,(java8 元空间)溢出了,一般出现于大量Class或者jsp页面,或者采用cglib等反射机制的情况,因为上述情况会产生大量的Class信息存储于方法区。此种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m -XX:MaxPermSize=256m的形式修改。另外,过多的常量尤其是字符串也会导致方法区溢出。
java.lang.StackOverflowError ------> 不会抛OOM error,但也是比较常见的Java内存溢出。JAVA虚拟机栈溢出,一般是由于程序中存在死循环或者深度递归调用造成的,栈大小设置太小也会出现此种溢出。可以通过虚拟机参数-Xss来设置栈的大小。
排查手段
一般手段是:先通过内存映像工具对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏还是内存溢出。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。这样就能够找到泄漏的对象是通过怎么样的路径与GC Roots相关联的导致垃圾回收机制无法将其回收。掌握了泄漏对象的类信息和GC Roots引用链的信息,就可以比较准确地定位泄漏代码的位置。
如果不存在泄漏,那么就是内存中的对象确实必须存活着,那么此时就需要通过虚拟机的堆参数( -Xmx和-Xms)来适当调大参数;从代码上检查是否存在某些对象存活时间过长、持有时间过长的情况,尝试减少运行时内存的消耗。
如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。这样就能够找到泄漏的对象是通过怎么样的路径与GC Roots相关联的导致垃圾回收机制无法将其回收。掌握了泄漏对象的类信息和GC Roots引用链的信息,就可以比较准确地定位泄漏代码的位置。
如果不存在泄漏,那么就是内存中的对象确实必须存活着,那么此时就需要通过虚拟机的堆参数( -Xmx和-Xms)来适当调大参数;从代码上检查是否存在某些对象存活时间过长、持有时间过长的情况,尝试减少运行时内存的消耗。
相关工具
MAT 分析 ,调用树,火焰图
可以使用maat定义到实际的代码位置进行分析。
火焰图:可以认为火焰图横轴代表程序运行的时间,上方重叠长条的就是调用栈。火焰图的颜色没有任何意义,主要要看的是某一层调用栈的时间占比。
创建对象
Q1:创建对象的过程是什么?
一个Java对象的创建过程往往包括 类初始化 和 类实例化
① (类加载检测)当 JVM 遇到字节码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
② (分配内存)在类加载检查通过后虚拟机将为新生对象分配内存。(为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有“指针碰撞”和“空闲列表”两种方式。)
③ (初始化零值)内存分配完成后虚拟机将成员变量设为零值,保证对象的实例字段可以不赋初值就使用。
④ (设置对象头)设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
⑤ (执行init方法)执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋给引用变量。
Q2:对象分配内存的方式有哪些?
对象所需内存大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。(分为指针碰撞和空闲列表两种方式)
①指针碰撞: 假设 Java 堆内存规整,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。(在垃圾回收机制有整理功能时使用该方式)
②空闲列表: 如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。(在垃圾回收机制没有整理功能时使用该方式)
选择哪种分配方式由堆是否规整决定,堆是否规整由垃圾收集器是否有空间压缩能力决定。使用 Serial、ParNew 等收集器时,系统采用指针碰撞;使用 CMS 这种基于清除算法的垃圾收集器时,采用空闲列表。
Q3:对象分配内存是否线程安全?
Q4:对象的内存布局了解吗?
(对象在堆内存的存储布局可分为对象头,实例数据和对齐填充)
① 对象头(Header):占 12B,包括对象标记(Mark Word) 和类指针(class pointer)以及数组长度。
对象标记:存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等,这部分占 8B。
类指针:是对象指向它的类型元数据的指针,而这个类的相关信息存在方法区。该指针在32位JVM中的长度是32bit,在64位JVM中长度是64bit。而默认的话,JVM会开启指针压缩,因此占 4B。JVM 通过该指针来确定对象是哪个类的实例。
-XX:+UseCompressedClassPointers 这个参数是+的话,类指针会被从8压缩到4B。
数组长度:只有数组对象有这部分,长度默认4B
(如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据。)
②实例数据(Instance Data):是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。
-XX:+UseCompressedOops (ordinary object pointers)普通对象指针,比如String,在示例数据中,8会被压缩到4 。 这个参数会压缩实例数据。
③对齐填充(Padding):不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。
例子:
Object类中没有实例数据,在开启压缩的时候,对象头8,类型指针4,数据填充4,开启的话,类型指针变成了8,填充变成了0。
对于数组的话,同理。
Q5:对象的访问方式有哪些?
一般来说,一个Java的引用访问涉及到3个内存区域:(JVM栈,堆,方法区。 )
以Person p=new Person()为例,
Person p表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
new Person()作为实例对象数据存储在堆中;
堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;
Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。
①句柄:JVM堆中会专门划分出一块儿内存区域作为句柄池, reference存储对象的句柄地址,句柄池中包含指向对象实例数据(堆的实例池中)的指针和指向对象类型(方法区中)数据的指针。
②直接指针:reference中存储对象在堆中的实际地址,优势是速度快,HotSpot主要使用直接指针对对象进行访问。
垃圾回收
Q1:如何判断对象是否是垃圾?
GC Roots(Garbage Collection)Roots即垃圾回收器的根对象。
1 ,引用计数: 在对象中添加一个引用计数器,如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。原理简单,效率高,但是在 Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。 python用的是记数法
2 ,可达性分析: 主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象(都是可用的对象)。
GC roots包括:
①系统类加载器(bootstrap)加载的类。
②JVM方法区中静态属性引用的对象。
③JVM常量池中引用的对象。
④JVM虚拟机栈中引用的对象。
⑤JVM本地方法栈中引用的对象。
⑥活动着的线程。
Q2:Java 的引用有哪些类型?
强引用: 最常见的引用,(例如 Object obj = new Object() 就属于强引用。)只要对象有强引用指向且 GC Roots 可达,在内存回收时即使濒临内存耗尽也不会被回收。
软引用: 弱于强引用,描述非必需对象。使用SoftRefence来表示软引用。
(例如:SoftReference obj = new SoftReference(new Object()。)在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。一般用于缓存机制。
弱引用: 弱于软引用,描述非必需对象。使用WeakReference来表示弱引用。(例如:WeakReference obj = new WeakReference(new Object()。)弱引用关联的对象只能生存到下次 YGC 前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于 YGC 具有不确定性,因此弱引用何时被回收也不确定。
虚引用: 虚引用是Java中最“弱”的引用,在任何时候都可能被垃圾回收器回收。通过它甚至无法获取被引用的对象,它存在的唯一作用就是当它指向的对象回收时,它本身会被加入到引用队列中,这样我们可以知道它指向的对象何时被销毁。
Q3:有哪些 GC(垃圾回收) 算法?
(JVM中的堆,一般分为三大部分:新生代(用于存放新生的对象)、老年代、永久代)
标记-清除算法
分为标记和清除阶段,首先从每个 GC Roots 出发依次标记有引用关系的对象,最后清除没有标记的对象。
执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发 Full GC。
标记-复制算法
为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。
实现简单、运行高效,解决了内存碎片问题。 代价是可用内存缩小为原来的一半,浪费空间。
HotSpot 把新生代划分为一块较大的 Eden 和两块较小的 Survivor,每次分配内存只使用 Eden 和其中一块 Survivor。垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%。
标记-整理算法
标记-复制算法在对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。
老年代使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法就是根据具体的情况选择具体的垃圾回收算法。一般根据各个年代的特点选择合适的垃圾收集算法。
(就是一种自适应的GC算法,根据具体情况,选择垃圾回收算法)
Q4:你知道哪些垃圾收集器?
Serial
最基础的收集器,使用复制算法、单线程工作,只用一个处理器或一条线程完成垃圾收集,进行垃圾收集时必须暂停其他所有工作线程。
Serial 是虚拟机在客户端模式的默认新生代收集器,简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的,对于处理器核心较少的环境,Serial 由于没有线程交互开销,可获得最高的单线程收集效率。
ParNew
Serial 的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。
ParNew 是虚拟机在服务端模式的默认新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS 配合。自从 JDK 9 开始,ParNew 加 CMS 不再是官方推荐的解决方案,官方希望它被 G1 取代。
Parallel Scavenge
新生代收集器,基于复制算法,是可并行的多线程收集器,与 ParNew 类似。
特点是它的关注点与其他收集器不同,Parallel Scavenge 的目标是达到一个可控制的吞吐量。
Serial Old
Serial 的老年代版本,单线程工作,使用标记-整理算法。
Serial Old 是虚拟机在客户端模式的默认老年代收集器,用于服务端有两种用途:① JDK5 及之前与 Parallel Scavenge 搭配。② 作为CMS 失败预案。
Parellel Old
Parallel Scavenge 的老年代版本,支持多线程,基于标记-整理算法。JDK6 提供,注重吞吐量可考虑 Parallel Scavenge 加 Parallel Old。
CMS
并发回收的算法
工作在老年代,最耗时的地方在gc roots寻找。
设计核心思想:内存越来越大,减少系统停顿时间。
以获取最短回收停顿时间为目标,基于标记-清除算法,过程相对复杂,分为四个步骤:初始标记、并发标记、重新标记、并发清除。
初始标记:200ms,标记和gcroots直接相关的对象,程序停下来
并发标记有浮动垃圾和错标。错标比较严重,错标是运行的过程中,没用的又变有用了
重新标记:修正错标的对象。
并发标记:无停顿,留下的浮动垃圾以后再说。
初始标记和重新标记需要 STW(Stop The World,系统停顿),初始标记仅是标记 GC Roots 能直接关联的对象,速度很快。并发标记从 GC Roots 的直接关联对象开始遍历整个对象图,耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。
缺点:① 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。② 无法处理浮动垃圾,有可能出现并发失败而导致 Full GC。③ 基于标记-清除算法,产生空间碎片。
三色 标记算法:
G1
开创了收集器面向局部收集的设计思路和基于 Region 的内存布局,主要面向服务端,最初设计目标是替换 CMS。
G1 之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。而 G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大。
跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。
Q5:ZGC 了解吗?
JDK11 中加入的具有实验性质的低延迟垃圾收集器,(目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。)
基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。
ZGC 的 Region 具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。
Q6:Minor GC、Major GC、Young GC和Full GC之间的区别
Minor GC,Young GC:对新生代堆内存进行垃圾回收。
Major GC:对老年代堆内存进行垃圾回收。(三者处理的内存区域不同)
Full GC:对全堆内存进行垃圾回收。
Q7:你知道哪些内存分配与回收策略?
1 ,对象优先在 Eden 区分配
大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。
2 ,大对象直接进入老年代
大对象指需要大量连续内存空间的对象,典型是很长的字符串或数量庞大的数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获得足够的连续空间。
3 ,长期存活对象进入老年代
虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头。如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1。对象在 Survivor 中每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。
4 ,动态对象年龄判定
为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。
5 ,空间分配担保
Minor GC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。
如果不满足,虚拟机会查看 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 Minor GC,否则改成一次 Full GC。
(冒险是因为新生代使用复制算法,为了内存利用率只使用一个 Survivor,大量对象在 Minor GC 后仍然存活时,需要老年代进行分配担保,接收 Survivor 无法容纳的对象。)
TXQ8:你知道哪些故障处理工具?
X
X
X
X
X
Q9:JVM垃圾回收调优
一.目标
1.GC的时间足够的小
2.GC的次数足够的少
3.发生Full GC的周期足够的长
前两个目前是相悖的,要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。
二.具体策略
(1) 针对JVM堆的设置,一般可以通过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最大、最小设置为相同的值。
(2) 年轻代和年老代将根据默认的比例(1:2)分配堆内存,可以通过调整二者之间的比率NewRadio来调整二者之间的大小,也可以针对回收代,比如年轻代,通过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。同样,为了防止年轻代的堆收缩,我们通常会把-XX:newSize -XX:MaxNewSize设置为同样大小。
依据:更大的年轻代必然导致更小的年老代,大的年轻代会延长普通GC的周期,但会增加每次GC的时间;小的年老代会导致更频繁的Full GC
更小的年轻代必然导致更大年老代,小的年轻代会导致普通GC很频繁,但每次的GC时间会更短;大的年老代会减少Full GC的频率。
方法:A)本着Full GC尽量少的原则,让年老代尽量缓存常用对象,JVM的默认比例1:2也是这个道理 (B)通过观察应用一段时间,看其他在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际情况加大年轻代,比如可以把比例控制在1:1。但应该给年老代至少预留1/3的增长空间。
(3) 在配置较好的机器上(比如多核、大内存),可以为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集
类加载机制
类的字节码文件(16进制的代码)
Q1:Java 程序是怎样运行的?
java程序的运行过程可以概括为:
1,javac.exe(编辑器)将.java文件编辑为JVM可加载的.class字节码文件。
2,JVM的类加载器(Class Loader)将字节码文件加载入内存。
3,加载完成后,字节码校验器对加载的字节码进行校验。
4,校验通过后交由解释器或JIT(Just In Time Compiler即时编辑器)将字节码文件解释成计算机能够识别的机器指令
Q2:Java 程序的编辑过程\
Javac 是由 Java 编写的程序,编译过程可以分为:
① 词法解析,通过空格分割出单词、操作符、控制符等信息,形成 token 信息流,传递给语法解析器。
② 语法解析,把 token 信息流按照 Java 语法规则组装成语法树。
③ 语义分析,检查关键字使用是否合理、类型是否匹配、作用域是否正确等。④ 字节码生成,将前面各个步骤的信息转换为字节码。
Q3:类加载(加载,链接,初始化)是什么?类的生命周期
java文件编译形成的.class文件不可以直接运行,需要加载到虚拟机中才可以运行。JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制。
与编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。(可扩展性主要是通过多态完成的,而JVM在运行期进行类加载的特性确保了可扩展性,如多态:在执行期间,判断所引用对象的实际类型,根据其实际方法,调用其相应的方法。)
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备和解析三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持 Java 的动态绑定。
Q4:类加载的过程是什么?
①加载:
加载时jvm做了这三件事:
1)通过一个类的全限定名来获取该类的二进制字节流
2)将这个字节流的静态存储结构转化为方法区运行时数据结构
3)在内存堆中生成一个代表该类的java.lang.Class对象,作为该类数据的访问入口
②验证:
验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会威胁到JVM的安全
③准备:
为类的静态变量分配内存,初始化为系统的初始值。对于final static修饰的变量,直接赋值为用户的定义值。(如,int a=7;final int b=8;在准备阶段后,a=0,b=8)
④解析:
将常量池内的符号引用替换为直接引用。
符号引用: 以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。
直接引用: 是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。
⑤初始化:
直到该阶段 JVM 才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的 方法,该方法是 Javac(编译器) 自动生成的。
Q5:有哪些类加载器?
自 JDK1.2 起 Java 一直保持三层类加载器:
1,启动类加载器( Bootstrap Class Loader ) :
在 JVM 启动时创建,负责加载最核心的类,例如 Object、System 等。无法被程序直接引用,通常由操作系统实现。
2,平台类加载器( Platform ****Class Loader ) :
从 JDK9 开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等。
3,应用类加载器(Application Class Loader):
也称为系统类加载器,它负责加载用户类路径(Class path)上所指定的类库,可直接使用这个加载器,也可以通过重写ClassLoader类中的findClass方法自定义类加载器。
Q6:双亲委派模型是什么?
双亲委派模式的工作原理的是:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
(即每个类加载器都有其父类加载器,子类加载器不干活二手交给父类加载器,直到父类加载器无法完成工作,子类加载器才干活,可以确保某个类在各个类加载器环境中都是同一个,保证了程序的稳定性)
最后看一下那个视频,总结一下。
并发 39
并发并行:10个人同时抢十张票,10个人同时抢一张票。
并发编程三大特性:可见性,有序性,原子性
JMM 9
因为cpu运行的速度要远远大于内存的读写速度,因此每一个线程在内存与线程之间建立了一个高速缓冲区,通过操作高速缓冲区来进行与内存之间的交互。
Q1:缓存一致性问题
硬件上:在现代计算机中,因为CPU的运算速度远大于内存的读写速度,为了不让内存速度影响CPU速度,CPU会加入一层缓存(硬件缓存),在运算之前缓存内存的数据,CPU运算的时候操作的是缓存里的数据,运算完成后再同步回内存。
每个CPU都有其自己的缓存,而他们都共享同一个内存,当多个CPU对同一块内存区域的数据进行操作时,会因为速率不一致,导致同步回内存的数据不知道以谁的为准,这就造成了缓存一致性问题。
软件上:在java线程执行过程中,java的实例变量都存储到了主内存(Main Memory)中,对所有线程都是共享的,每个线程都有自己的本地内存(本地内存由缓存和堆栈组成, 缓存中存储主内存中公共变量的拷贝,堆栈中存储线程的局部变量),线程和主内存之间信息的交互,同CPU和内存的交互类似,也是通过本地内存进行的,故而存在缓存一致性问题。
Q2:内存模式的三大特性:原子性,可见性和有序性分别是什么?
volatile和synchronized都以保证实现原子性有序性和可见性。
final可以保证实现可见性。
原子性:代表着当前执行代码是不可分的,也就是当前代码一旦被执行,就不会被中断。基本数据类型的访问都具有原子性,且被volatile修饰的变量和synchronized修饰的方法都具有原子性。
可见性:(可见性是指,当一个线程修改了共享变量时,其他线程能够立即得知修改)JMM通过使线程对共享变量进行修改后同步回写入到主内存,或读取共享变量时必须去主内存中重新加载,而不是直接从主内存中读取来实现可见性。volatile ,synchronized 和 final 都可以保证可见性。
有序性:(有序性用来避免重排序所带来的问题)Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁止指令重排序的语义,而 synchronized 保证一个变量在同一时刻只允许一条线程对其进行 lock 操作,确保持有同一个锁的两个同步块只能串行进入。
(有序性和可见性统称易变性)
Q3:什么是指令重排序?
为了提高性能,编译器和处理器通常会对指令进行重排序(代码执行顺序重写排序),重排序指从源代码到指令序列的重排序,分为三种:
① 编译器优化的重排序: 编译器在不改变单线程程序语义的前提下可以重排语句的执行顺序。
② 指令级并行的重排序: 现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行,如果不存在数据依赖性( 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。 ) ,处理器可以改变语句对应机器指令的执行顺序。
① 内存系统的重排序: 由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
Q4:as-if-serial (串行)是什么?
不管怎么重排序,单线程程序的执行结果不能改变,编译器和处理器必须遵循 as-if-serial 语义。
为了遵循 as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
as-if-serial 把单线程程序保护起来,给程序员一种幻觉:单线程程序是按程序的顺序执行的。(针对单线程而言的,对存在依赖关系的语句,不改变其执行顺序)
Q5:happens-before 是什么?
如果一个操作所执行的结果需要对另一个操作可见,那么两个操作之间必要会存在happens-before(先于) 的关系。具体可分为以下几种情况:
1,在单线程中写在前面的操作先行发生于后面的操作。
2,对于多线程而言,unlock解锁操作先行发生于后面对同一个锁的lock上锁操作。
3,对 volatile 变量的写操作先行发生于后面的读操作。
4,线程的 start 方法先行发生
5,传递性:A happens before B B happens before C =>A happens before C
对所有这些规则的说明:A happens-before B并不意味着A一定要先在B之前发生,而是说,如果A已经发生在了B前面,那么A的操作结果一定要对B可见
Q6:as-if-serial 和 happens-before 有什么区别?
as-if-serial是针对于单线程而言的,保证单线程的执行结果不变, happens-before除了可以适用于单线程,还可以适用于多线程,保证正确同步的多线程程序的执行结果不变。
(这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行并行度。)
Q7:JMM 的作用是什么?
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。(JMM是一种规范),规定一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
JMM规定所有的共享变量(共享变量是指:实例对象、静态字段、数组对象等存储在堆内存中的变量。而对于局部变量这类的,属于线程私有,不会被共享)都存储在主内存中,每个线程都有其自己的本地内存,本地线程里存储主内存中数据的拷贝,主内存和线程间信息的交互都是通过本地内存进行的。
(JMM主要用于解决缓存一致性问题和重排序问题)
Q8:谈一谈 volatile
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
Volatile具有以下特点:
1,保证其修饰变量的可见性:存在的问题是,java中运算符操作并非原子性的,导致volatile修饰的变量在并发情况下仍然不安全。
2,不能保证原子性:因为运算符操作并非原子性的
3,禁止指令重排序:使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,后面的指令不能重排到内存屏障之前。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
详细说明两种特性:当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
sync就没有并发了,voli还有并发
volatile的原理是:cpu不会先从内存拷贝数据,而是直接操作内存中的数据。volatile的底层是通过lock前缀指令、内存屏障来实现的。汇编级别加的,字节码级别没有。
Q9:final 可以保证可见性吗?
final可以保证可见性,被 final 修饰的字段在构造方法中一旦被初始化完成,并且构造方法没有把 this 引用传递出去,在其他线程中就能看见 final 字段值。
Q10:volatile和synchronized的比较
synchronized其实是一种加锁机制,他通过加锁来避免统一代码块只会被一个线程同时执行,从而确保了线程的安全。底层原理是每一个对象有对应一个同步监视器moitor,编译生成的代码,enter和exit中间。
volatile的原理是:cpu不会先从内存拷贝数据,而是直接操作内存中的数据。volatile的底层是通过lock前缀指令、内存屏障来实现的。汇编级别加的,字节码级别没有。
volatile可以实现有序性和可见性,synchronized可以保证实现原子性有序性和可见性。
1.volatile是变量修饰符,而synchronized则作用于一段代码或方法或者类。
2.volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值, 显然synchronized要比volatile消耗更多资源。
3.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
4.volatile保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存中和公共内存中的数据做同步。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排
他(互斥)的机制。
volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的
赋值,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile
又保证了可见性,所以就可以保证线程安全了。
volatile可以看作是一种轻量级的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他的操作,那么就可以用volatile来代替synchronized
锁 17
Q1:谈一谈 synchronized
1. 用处
2. 基本原理
3. 锁的优化
4. 底层原理
一.使用
synchronized是java中的关键字,是一种同步锁 ,其作用是使得被其修饰的代码不会被多个线程同时执行,可以用于以下几种情况:
1,修饰一个代码块:被修饰的代码块被称为同步代码块,其作用范围是{}括起来的部分,作用的对象是调用这个代码块的对象。
2,修饰一个方法:被修饰的方法被称为同步方法,其作用范围是整个方法,作用的对象是调用这个方法的对象。
3,修饰一个静态方法:其作用范围是这个静态方法,作用对象是这个类的所有对象。(因为静态方法是属于类的,和类绑定)
4,修饰一个类:作用对象是这个类的所有对象。synchronized(ClassName.class)
二.基本原理
锁其实是加在锁对象上的,被加了锁的对象称为锁对象。 每个java对象都有一个与之关联的monitor (同步监听器),编译后生成的字节码文件, synchronized修饰的代码块前后,会被加上monitorenter 和 monitorexit。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 Monitor 所有权,即尝试获取对象的锁。(synchronized可以用于保证原子性,可见性和有序性)
三.锁的优化
JDK1.6 之后引入了偏向锁和重量级锁。对象将这些信息存在对象头的mark word中,其中存的有hascode,GC的分代年龄,锁状态。
轻量级锁和重量级锁存的都是指向锁记录的指针。一个是Lock record ,一个是moniter的指针。
四.底层原理
通过调用ObjectMoniter实现的。
Q2:锁优化有哪些策略?
重量级锁需要从用户态向内核态转变,会消耗cpu的资源。
编码过程中的策略: 1,减少锁持有时间;2,减小锁粒度;3,锁分离;4,锁粗化;5,锁消除
JVM 中的策略: 1,偏向锁;2,轻量级锁;3,自旋锁(自适应自旋)
Q3:sync锁的四种状态
(锁存在于java对象头的Mark Work中,锁有四种状态,分别为:无锁,偏向锁,轻量级锁,重量级锁)
锁的状态有五种,无锁,偏向锁,轻量级锁,重量级锁,gc回收,存在java对象头的Mark Work中的后三个比特。可以把这个过程比作是,只有一个位置的洗手间,这个卫生间有一把大锁,锁上它需要用户态和内核态的转变,所需要的资源比较多。另外门上还有一张贴纸,只需要写上自己的名字即可,占用的资源较少。当仅仅有一个人的时候,比如这个人叫作A来的时候,这个时候竞争不激烈,A把自己的名字,也就是线程id贴到了门上,就可以进去了;当不只有一个人时,A,B,C需要去抢夺执行权,抢到的执行,没有抢到的在门口等着,也就是自旋;当自旋的线程较多的时候,用户就会向管理员申请一把大锁,这个时候出现了用户态向内存态的转变。其他人在门外排队。
****这个大锁的具体申请流程,也就是sync的申请过程是: 每个java对象都有一个与之关联的monitor (监听器),编译后生成的字节码文件, synchronized修饰的代码块前后,会被加上monitorenter 和 monitorexit。
线程执行到 monitorenter 指令时,将将会尝试获取对象所对应的 Monitor 所有权,即尝试获取对象的锁。
当无人的时候,也就是初始状态,就无锁的,然后jvm会等4s,看一下多不多,多的话直接轻量级锁。
Q4:什么是偏向锁?
偏向锁其实没有加锁,就是贴一个id上去到对象头的Thread id上,再次获取的时候,假如这两个id一样的话,那么就默认已经获取到了锁。
(偏向锁是主要针对单线程起作用的)偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
锁对象的对象头中有个Thread Id字段,这个字段如果是空的,第一次获取锁的时候,就将自身的Thread Id写入到锁的Thread Id字段内,将锁头内的是否偏向锁的状态位置1.这样下次获取锁的时候,直接检查Thread Id是否和自身线程Id一致, 如果一致,则认为当前线程已经获取了锁,因此不需再次获取锁,略过了轻量级锁和重量级锁的加锁阶段。提高了效率。(偏向锁可以用于略过再次获取锁时的加锁阶段,提高效率)
当锁有竞争关系的时候,会消除偏向锁,使锁进入竞争状态
Q5:什么是轻量级锁
(轻量级锁是一种锁状态)
加锁过程:
1,在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储旧的Mark Word的拷贝,官方称其为Displaced Mark Word。(锁记录解锁的时候会用到)
2,虚拟机将使用CAS操作尝试将对象的Mark Word更新为轻量级锁的标志位和指向锁记录(Lock Record)的指针。
3,如果这个更新动作成功了,那么这个线程就拥有了该对象的锁。(获得轻量级锁)
4,如果这个更新操作失败了,虚拟机首先会检查当前线程是否已经拥有了这个对象的锁,如果已经拥有,那就可以直接进入同步块继续执行。否则就说明这个锁度一项已经被其他线程抢占了。一旦发生这种情况,那么轻量级锁就会膨胀为重量级锁。Mark Word中存储的就会指向重量级锁的指针,后面等待锁的线程也会进入阻塞状态。
解锁过程:
1,通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
2,如果替换成功,整个同步过程就完成了。
3,如果替换失败,说明有其他线程尝试过获取该锁,并导致该锁变成了重量级锁,那就要在释放锁的同时,唤醒被挂起的线程。
Q6:偏向锁、轻量级锁和重量级锁的区别?
偏向锁:优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景。
轻量级锁: 优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗 CPU,适用追求响应时间、同步代码块执行快的场景。
重量级锁:优点是线程竞争不使用自旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执行慢的场景。
Q7:什么是自旋锁
同步过程中对性能最大的影响就是阻塞,挂起和恢复线程的操作都要转入内核状态完成,但是许多应用上共享数据的锁定只会持续很短的时间,为了这段短时间去不断挂起和恢复是不值当的,在多核处理器上,多个线程是可以并行执行的。如果(当后面请求锁的线程没拿到锁的时候,不挂起线程,而是继续占用处理器的执行时间,让当前线程执行一个忙循环(自旋操作),也就是不断在盯着持有锁的线程是否已经释放锁),那么这就是传说中的自旋锁了。
自旋锁虽然减少了线程切换开销,但会占用处理器时间,如果自旋超过了限定的次数仍然没有成功获得锁,就应挂起线程,自旋默认限定次数是 10。
Q8:什么是自适应自旋
(就是自旋时间不再固定,而是由前一次自旋时间和锁拥有者的状态决定)。
如果上次自旋刚刚成功,虚拟机会认为这次自旋也会成功,并允许本次自旋等待更长时间;如果自旋很少成功,虚拟机可能会在下次获取锁时直接省略掉自旋状态,避免浪费处理器资源。
有了自适应自旋,随着程序运行时间的增长,虚拟机对程序锁的状况预测就会越来越精准。
Q9:什么是减少持锁时间
对一个方法加锁,不如只对需要同步的那几行代码加锁,减少线程持有锁的时间。
Q10:什么是锁粗化
如果一串零散的锁操作,都是对同一个对象加锁,不如将这些操作都放在一个锁中,减少多次获取释放锁时线程切换的消耗。
Q11:什么是锁消除
锁消除是指即时编译器对不可能出现同步竞争的数据的锁进行消除,主要判定依据来源于逃逸分析,如果判断一段代码中堆上的所有数据都只被一个线程访问,就可以当作栈上的数据对待,认为它们是线程私有的而无须同步。
Q12:Lock 和 synchronized 有什么区别?
1,synchronized是java的内置关键字,而lock是java的一个接口。
2,synchronized会自动加锁和释放锁,而lock需要使用finally关键字手动释放锁。
3,synchronized是不可中断锁(可能会一直发生等待),lock是可中断锁(例如线程B在等待等待线程A释放锁,但是线程B由于等待时间太久,可以主动中断等待锁。)
4,synchronized是非公平锁,而lock是公平锁,底层是乐观锁,将所有请求线程构成一个队列。
Q13:什么是AQS?
是除了java自带的synchronized关键字之外的锁机制
AQS就是基于CLH队列,用volatile修饰共享变量state,例如ReentrantLock用它来表示线程重入锁的次数。保证多线程的可见性。线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
Q14:ReentranLock和sync
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
兼容问题和自动释放问题。
Q15: ReentranLock原理
ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。在继续之前,咱们先把故事元素转换为程序元素。
Q16:Synchronized方法锁、对象锁、类锁区别
方法锁(synchronized修饰方法时): 方法一旦执行,就会独占该锁,一直到从该方法返回时才将锁释放,此后被阻塞的线程方能得到该锁,从而从新进入可执行状态。线程这种机制确保了同一时刻对于每个类的实例,其全部声明为synchronized的成员函数中之多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突.
对象锁(synchronized修饰方法或代码块): 线程进入synchronized 方法的时候获取该对象的锁,固然若是已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,jvm会自动释放对象锁。这里也体现了用synchronized来加锁的一个好处,即 :方法抛异常的时候,锁仍然能够由jvm来自动释放。
类锁(synchronized修饰静态的方法或者代码块):
因为一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。因此,一旦一个静态的方法被声明为synchronized。此类全部的实例对象在调用此方法,共用同一把锁,咱们称之为类锁。
java类可能会有不少对象,可是只有一个Class(字节码)对象,也就是说类的不一样实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。
因为每一个java对象都有1个互斥锁,而类的静态方法是须要Class对象。因此所谓的类锁,只不过是Class对象的锁而已。
获取类的Class对象的方法有好几种,最简单的是[类名.class]的方式
Q17:说一说java中的各种锁
下面从加锁的原因,锁是为了实现同步,实现同步的三个方法:不变;CAS;sync;
锁的优化自己的理解来说明。
加锁的原因:
多线程访问共享资源的时候,避免不了资源竞争而导致数据错乱的问题,所以我们通常为了解决这一问题,都会在访问共享资源之前加锁。如果选择了错误的锁,那么在一些高并发的场景下,可能会降低系统的性能,这样用户体验就会非常差了。因此要针对不同的场景选择不同的锁。
首先是底层:互斥锁和自旋锁
最底层的两种就是会「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现的,可以认为它们是各种锁的地基。
总之:
1. 互斥锁加锁失败后,线程会释放 CPU,给其他线程;( 互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞 )
对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图:
线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
上下切换的耗时有大佬统计过,大概在几十纳秒到几微秒之间,如果你锁住的代码执行时间比较短,那可能上下文切换的时间都比你锁住的代码执行时间还要长。
所以,如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁
2. 自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
自旋锁是通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
Q18:synchronized与AtomicInteger
synchronized :重量级操作,基于悲观锁,可重入锁。
AtomicInteger:乐观 ,用CAS实现
sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。
因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象
wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
原文链接:blog.csdn.net/xyh269/arti…
线程 13
Q1:线程的生命周期有哪些状态?
新建状态(New) :线程被创建且未启动,此时还未调用 start 方法。
就绪状态(Runnable): 线程执行了start方法,但是还没有开始执行,等待CPU分配资源。
运行状态(Running): 就绪的线程被调度并且获得CPU资源,线程进入运行状态。
阻塞状态(Blocking ): 在运行状态,线程可能会因为sleep()或wait()等方法进入阻塞状态,需要等待其他线程去唤醒,唤醒后的线程进入就绪状态,需要再次等待CPU分配资源。
死亡状态(Dead ) :线程执行完毕,或因为异常退出run()方法,线程的生命周期结束。
(新建->就绪->运行->阻塞->死亡)
Q2:线程的创建方式有哪些?
1,通过继承Thread类来创建线程,并重写run()方法。
2,通过实现Runable接口来创建线程,并重写run()方法。
3,通过实现Callable接口来创建线程,并重写call()方法。
4,借助线程池,使用Executor。
比较: Thread的子类只能继承一个父类,不可以继承其他类;Runable的实现类还可以实现其他接口;Callable可以获取线程执行结果的返回值,也可以抛出异常;前三种方法频繁创建关闭线程会造成大量的资源消耗,使用线程池,可以节约资源。
Q3:线程池有什么优点
适合处理任务量比较大,但是单个任务的处理时间比较短。
1,复用已经创建的线程,降低开销,节约资源。还可以控制最大并发数。(节约资源)
2,提高响应速度,当任务到达时,不需要等待,就可以立即执行。(快)
3,提高线程的可管理性,可以统一调度和监控和调优。(方便管理)
4,实现任务线程队列缓冲策略和拒绝机制。(功能强大)
Q4:线程池处理任务的流程
1,任务提交到线程池,先判断线程池中的核心线程数是否已满,如果未满,则创建核心线程处理任务,如果已满则执行下一步。
2,判断任务队列是否已满,如果未满则将任务加入到任务队列排队,如果已满则进行下一步。
3,判断线程数是否达到最大线程数,如果未达到,则创建非核心线程处理任务,否则将任务交给饱和策略来处理。默认是Abord Policy,表示无法处理新任务,并抛出Rejected Execution Exception异常。
Q5:创建线程池的方法有哪些?
1、New Single Thread Executor:创建一个单线程的线程池,只有一个线程工作,保证所有任务的执行顺序按照任务的提交顺序执行;
2、New Fixed Thread Pool:创建固定大小的线程池,每次提交一个任务就从线程池中拿一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程;
3、new Cached Thread Pool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
4.new Scheduled Thread Pool:创建一个支持定时以及周期性执行任务的线程池。
Q6:谈一谈ThreadLocal
作用:用于实现线程内的数据共享,访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
set()方法:
Q7:进程与线程
类比一下吧。比如现在牛客网这个程序,它运行起来以后,就是一个进程,他是资源分配的最小单位,而每一个模块就是线程,他是cpu调度的最小单位。
Q8: 死锁
待更新
MySQL数据库
逻辑架构
Q1:MySQL的逻辑架构了解吗?
(Mysql的逻辑结构分为三层:服务层->核心层->存储引擎)
① 第一层:服务层,为客户端提供服务,用于进行连接,授权认证和安全等。
② 第二层:核心层,MySql的核心服务都在这一层。包括查询解析,分析、优化、缓存,提供内置函数、存储过程、触发器、视图。
③ 第三层:存储引擎,用于数据的存储和提取,响应上层服务器的请求。
Q2:谈一谈 MySQL 的读写锁
读写锁是为了解决多用户并发读或写数据库过程中存在的问题而存在的,读锁也叫作共享锁,是对所有用户共享的,互相不阻塞,多个用户可以同时对数据库中的同一条数据进行读取,但是不能写入;写锁也叫作排它锁,当一个用户正在对数据库中的数据进行写入操作时,会阻塞其他用户对当前数据库此数据的读取或写入,以确保在给定时间内只有一个用户能对数据库中的数据进行写入,并防止其他用户读取正在写入的不完整资源。
(写锁比读锁有更高的优先级,一个写锁请求可能会被插入到读锁队列的前面,但是读锁不能插入到写锁前面。共享锁允许读取操作,不允许写入,排他锁读取和写入都不允许)
Q3:MySQL 的锁策略有什么?
因为锁机制可以执行并发操作,但是锁会带来额外的开销,锁机制就是在锁的开销和数据的安全性之间寻找平衡的一种策略。(一般分为表锁和行锁)****
表锁:
表锁是mysql中最基本的锁策略,也是开销最小的锁策略,表锁会锁定整张表,一个用户在对表进行写操作前会先获取锁,这会阻塞其他用户对表的读写操作,当写操作完成后释放锁,其他用户才可以对表进行读写操作。
行锁:
行锁对表中的一行或几行进行加锁,可以最大程度的支持并发,同时带来的开销也非常大,InnoDB 和 XtraDB 以及一些其他存储引擎实现了行锁。行锁只在存储引擎层实现,而服务器层没有实现。
Q4:数据库死锁如何解决?
数据库的死锁是指多个事务同时请求对方所占有资源时造成的循环等待现象。
产生原因:
1,多个顺序对不同资源的访问顺序出现了交替:事务one访问表a(锁住了表a)后访问表b,事务two访问表b(锁住了表b)后访问表a,两个事务都在等待对方被锁住的那张表,就会出现循环等待。
2,多个事务同时锁定同一资源:事务one先查询表a再修改表a,事务two修改表a,事务one查询时使用的是共享锁,在修改时要上升到排它锁,事务two由于要使用排他锁,必须要等待事务one释放掉共享锁,而事务one由于事务two独占锁的存在,无法上升,也就造成了互相等待。
解决方法:
InnoDB 会自动检测,并使一个事务回滚,另一个事务继续。
设置超时等待参数 innodb_local_wait_timeout。
避免方法:
不同业务并发访问多个表时,约定以相同的顺序访问。
在事务中,如果要更新记录,使用排它锁。
Q6:事务是什么?
事务是一组原子性的****SQL 查询,或者说是一个独立的工作单元。整个独立单元作为一个不可分割的整体,要么都执行,要么都不执行(原子性)。如果事务中的某条sql语句执行失败,那么整个事务都会发生回滚,也就是所有受影响的事务都会恢复到事务发生之前的状态;如果事务中的所有sql语句都执行成功,则事务被顺利执行。
Q5:关系型数据库的三大范式
(1) 第一范式(F1):要求数据库表的每一列都是不可分割的原子数据项。(确保每一列保持原子性)(如:用户表中,家庭信息写成如:3口人,北京,就不符合原子性,应为3口人和北京可以再分割成两列)
(2) 第二范式(F2):要求数据库表的所有非主键属性要和主键相关而不能和主键的一部分相关(主要针对联合主键)(如:商品表的联合主键是商品编号和订单号,那么如果表里有商品名,商品信息的列,就只和商品编号有关而与订单号无关,违反了第二范式,应该将商品名,商品信息与商品编号单独列一张表)
(3) 第三范式(F3):要求数据库表中的任何非主键属性不依赖于其他非主键属性(确保数据库里的所有属性都和主键直接相关而不能间接相关)(如:在一个订单表中,不能将客户信息写入表中,而是应该单独写一张客户表,通过外键与订单表中的销售员相连)
(注:每一个范式都是以前一个范式为基础的)
Q7:事务有什么特性?
(事务有四大特征ACID)原子性,隔离性,一致性和持久性
1,原子性: 原子性是指一个事务中的所有操作都是一体的,要么都做,要么都不做
2,一致性: 事务的操作必须满足业务规范约束,比如转账业务,转账前后两个账户的总金额不变
3,隔离性: 是针对事务并发导致的数据库数据不一致而言的,一般来说一个事务所做的修改在最终提交以前,对其他事务是不可见的
4,持续性: 事务执行后对数据库的修改是永久的
Q8:事务的隔离级别?Mysql的默认隔离级别
事务的隔离级别:
① 读取未提交: 一个事务可以读取到另一个未提交事务的执行结果,会产生脏读(脏读:又称为无效数据读出,事务a读取到事务b对数据库的修改,但事务b如果回滚了,事务a读取的数据就无效了)****
② 读取已提交: 一个事务只能看见另一个事务已提交的数据,会产生不可重复读,大多数数据库采用的隔离级别(不可重复读: 不可重复读是指在同一个事务内,两次相同的查询返回了不同的结果,事务a读取两次数据,当事务a读取完一次数据后,事务b对数据进行了修改,导致事务a第二次读取到的结果和第一次不一致)
③ 可重复读: 同一个事务多次读取相同记录时,会看到相同的数据,会产生幻读(幻读: 事务a查询一张表的行数,查询后事务b向表中加入一行,事务a再读取的时候就像产生幻觉一样)****
注:幻读是指数据条数,不可重复读是指同一条数据内容
④ 可串行化: 最高的隔离级别,强制所有事务都串行执行****
(Mysql的默认隔离级别为可重复读。)
Q9:谈一谈乐观锁和悲观锁
乐观锁和悲观锁是为了应对多个事务并发操作数据库过程中可能带来的原子性,隔离性,一致性破坏而产生的一种思想。
乐观锁:
乐观锁的思想是相信事务之间产生冲突的概率比较小,因此直接做下去,直至提交阶段才去锁定。(通过在提交阶段进行冲突检测来避免出现并发问题,可以避免死锁)
实现方式: 一般在数据库的表上加上一个版本号****version , 数据被修改时,version值会+1,当事务要修改表中数据时,读取数据时会将version值一并读取,写入更新时,比对当前version值和数据库中的version值,如果一样则更新,否则重新操作直至更新成功。
悲观锁:
悲观锁的思想是多个事务对数据库的操作极大可能会互相影响,因此在事务操作数据之前,先对数据进行加锁操作,当事务执行之后再释放锁。(特点就是会造成高额的锁开销,也可能造成死锁)
实现方式: 读锁(共享锁)和写锁(排他锁)
Q10:数据库的连接过程
① 导入驱动jar包:mysql-connector
② 注册驱动:使用代码class.forName("com.mysql.jdbc.Driver")来加载驱动,在mysql8.0版本之后变成了com.mysql.jc.jdbc.Driver
③ 获取数据库连接对象:使用DriverManager(驱动管理对象)的getConnection方法获得Connection数据库连接对象
其中url用于指定连接路径,格式为:
Jdbc://mysql://ip地址(域名):端口号/数据库名称
④ 创建Statement类对象用于执行sql语句
⑤ 创建ResultSet对象用于存放结果集
⑥ 断开连接释放资源
Q11:数据库分库分表
当数据量过大,或请求过多。我们可能必须采取分库分表的方法解决问题。但是相对于单库单表,分库存在很多问题:
id
性质:每个物理表的id可以保证自增,但是无法保证全局唯一。
问题场景: 我们经常需要根据id进行设备等,但是默认id无法保证全局唯一。
我们有两种解决方法:
- 使用sequence在插入时保证id的唯一性,替换掉原来默认的自增id
- 默认id保留,新增一个biz_id 再插入时使用sequence保证唯一性。
针对方法一问题场景:
MySQL分页查询的时候,一般会使用:SELECT * FROM world.city LIMIT 100000, 20; 但是这个扫描了100020行只返回了20行。通用的解决办法是记录上次的偏移量,使用id过滤:SELECT * FROM world.city where Id > 100000 LIMIT 20; 。这句话在单库单表没有问题,因为id保证了自增,但是我们替换掉物理表默认id后无法保证其自增性,所以这种高效的查询方法失效。
针对方法二的其他优势:
在进行数据库系统迁移的时候,原来的线上系统可能在内存或其它地方缓存了id并进行使用,如果硬切会导致id不匹配,线上业务会出现问题,所以我们最好使用bizId保存原来的id。做好系统的兼容性。
分表规则
问题场景:在分表时,我们可以分物理库的任意整数倍张表,但是多张表的分表规则要一致。否则在进行涉及多个表的事务管理的时候,会导致无法落到同一张表上,影响事务。
Q12:读写分离时,如何保证一致性
数据库读写分离主要解决高并发时,提高系统的吞吐量。因为大部分场景下都是读多写少。下图是数据库读写分离模型。
在这里插入图片描述
每次请求打到这个系统后:
读请求,直接读从库
写请求,先写入主库,然后主库将数据同步到其他从库
在高并发或者网络状况不理想时,写完数据后,主库还没来得及将数据同步到从库,其他读请求去读从库,发现从库中的数据仍然是旧数据。这就是读写分离数据库数据不一致的根本原因。
下面给出两种方案去解决这个问题:
缓存标记法
在这里插入图片描述
上图流程:
1)A发起写请求,更新了主库,再在缓存中设置一个标记,代表此数据已经更新,标记格式( 用户ID+业务ID+其他业务维度)
2)设置此标记,要加上过期时间,可以为预估的主库和从库同步延迟的时间
3)B发起读请求的时候,先判断此请求的业务在缓存中有没有更新标记
4)如果存在标记,走主库;如果没有走从库。
这个方案就有效了解决了数据不一致的问题。
但这个方案会有个严重的问题,也就是每次的读请求都要到缓存中去判断是否存在缓存标记,如果是单机部署用的是jvm缓存,对性能还好;但如果是集群部署缓存肯定用redis,每次读都要和redis进行交互,这样肯定会影响系统吞吐量。
那么解决这个问题可以用下面这个方案
本地缓存标记
在这里插入图片描述
上图流程:
1)用户A发起写请求,更新了主库,并在客户端设置标记,过期时间(预估的主库和从库同步延迟的时间),可以使用cookie实现
2)用户A再发起读请求时,带上这个cookie
3)服务器处理请求时,获取请求传过来的数据,看有没有这个标记
4)有这个业务标记,走主库;没有走从库。
这个方案就保证了用户A的读请求肯定是数据一致的,而且没有性能问题,因为标记是本地客户端传过去的。
但是无法保证其他用户读数据是一致的,但是实际场景很少需要保持其他用户也保持强一致。延迟个几秒也没问题。
存储引擎
聚簇索引和非聚簇索引,B+和B-的区别不一样。
叶子结点挂不挂数据,非叶子节点有无数据
Q1:MVCC 是什么?
MVCC是多版本并发控制,类似于CopyOnwrite。它是行级锁的一个变种,是乐观锁的实现,他在很多情况下可以避免加锁操作,节约系统资源。
在InnoDB 中通过在行中增加两个隐藏列来实现MVCC,增加的是事务的版本号和回滚指针。另外还会维护一个readveiw,去记录还未提交的事务。另外还有undo log中的版本链。
在实现读已提交时,当一个事务查询的时候,会去检查Readveiw,如果小于事务编号的最小值,证明要查询的已经提交了,这个时候,查询版本链最早的数据即可。
如果小于最大值大于最小值,说明这个事务修改了数据,但是未提交,这个时候如果有的话会读到自己修改的记录。如果大于最大值,那么是在readview生成之后才发生的,这样不可读。
读已提交和可重复读基本原理都如上,区别在于,读已提交readveiw会变化,而可重复读,只会维护一个。
1、 如果trx_id<min_id.表示这个版本是已提交事务生成的,所以对当前活动的事务来说是可访问的。
2、 如果trx_id>max_id,表示这个版本是ReadView生成之后才发生的,不能被访问。
3、 如果min_id<trx_id<max_id,
a. 若trx_id在数组中,表示这个版本是还未提交的事务生成的,所以版本不能被访问。
b. 若trx_id不在数组中,则事务已提交,可以被访问。
(MVCC 只能在提交读和可重复读两个级别工作,因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行,而可串行化则会对所有读取的行都加锁。)
Q2:什么是数据库的存储引擎
是数据库底层的软件组织,数据库管理系统利用存储引擎进行数据的增删改查,不同的存储引擎提供不同的功能,MySql的核心插件就是存储引擎,并且MySql支持多种存储引擎,如InnoDB,MyISAM,Memory。
Q3:MySQL的三大日志(bin log/redo log/undo log)
Bin log (二进制日志):
日志内容:是一种逻辑日志,可以简单理解为记录的是****sql 语句(不包括查询语句) ,通过追加的方式进行写入
使用场景:1)主从复制;2)数据恢复
文件大小:通过max_binlog_size设置每个log文件的大小,当到达指定最大值时,会生成新的文件来保存日志
刷盘时机:mysql5.7.7之后默认是每次事务提交之后刷到磁盘中
Redo log (重做日志):
日志内容 : 是一种物理日志,记录的是事务对数据页的哪些修改(例如转账业务,事务的作用是甲方向乙方转账100元,那么redo log记录的就是甲方的账单金额减少100,乙方的账单金额增加100)
使用场景:用于保证事务的一致性,当事务已经提交,InnoDB已经把数据加载进了内存但是还没来得及刷入磁盘,如果突然停电导致电脑关机,刷盘过程将会停止,如果没有redo log,转账过程就出现了一致性问题。
刷盘时机:为了防止上述场景中出现的问题,redo log分为两部分:内存中的log buffer日志缓存区和磁盘上的redo log file日志文件,是一种先写日志再写磁盘的过程,当事务执行时先将记录写入缓存区, 在缓存区容量不足或者事务提交完成后一次性将多条记录写入磁盘的redo log file中
Undo log (回滚日志):
日志内容 : 是一种逻辑日志,记录的是sql的反向语句(如:事务执行一个insert则对应一个delete的undo log;执行一个update语句则对应一个反相update的undo log)
使用场景:用于保证事务的原子性,当事务进行回滚时,利用undo log实现使数据恢复到事务执行的前一个版本****
Q4:谈一谈 InnoDB
InnoDB是MySQL的5.1版本之后的默认搜索引擎
索引类型:聚簇索引
底层结构:b+树
磁盘文件:.frm文件(用于存储创建表的语句).ibd文件(数据+索引文件)
如果是以主键作为索引,则叶子节点中存储的是主键行的所有元素;如果以非主键作为索引(辅助索引/二级索引),则叶子节点中存储的是主键列,再通过主键去主索引树中查询全部数据。(所以主键应尽可能小,否则其他索引树都会很大)
主要特点:
1) 支持auto_increment主键自增长
2) 支持事务:默认的隔离级别为可重复读,通过MVCC来实现
3) 支持外键约束
4) 支持行级锁,采用MVCC支持高并发
Q5:谈一谈 MyISAM
MyISAM是MySQL的5.1版本之前的默认搜索引擎
索引类型:非聚簇索引
底层结构:b+树
磁盘文件:.frm文件(用于存储创建表的语句)
.MYD文件(用于存储表中的数据)
.MYI文件(用于存储表中的索引)
在myi文件中保存的索引树,其叶子节点中不存放真实的数据,存放的是键所在行对应数据的地址,通过指向myd文件,进行数据查找
主要特点:
1) 不支持事务,行锁和外键
2) 插入和查询速率快
Q6:谈一谈 Memory
Memory是一种不怎么被使用的存储引擎,与前两种存储引擎最大的不同就是,其将数据存储在内存中,在磁盘中只有一个.frm文件来存储创建表的语句,默认使用速度较快的哈希索引
(注:哈希索引相对于b树索引的查询速度更快,但由于哈希索引的存储是无序的,所以哈希索引无法支持范围查找)
Q7:聚簇索引和非聚簇索引
聚簇索引和非聚簇索引不是一种索引类型,而是一种数据的存储方式****
聚簇索引:
聚簇索引就是按照每张表的主键构造一棵****b+ 树,同时叶子节点中存放的是整张表的行记录数据(与主键对应的所在行),聚簇索引的叶子结点也被称为数据页。(还有一个)
(InnoDB使用聚簇索引,如果没有主键,就会选择唯一的非空索引代替,如果没有这样的索引,InnoDB会隐式的定义一个主键作为聚簇索引)
非聚簇索引:
非聚簇索引也是通过b+树存储的,但是叶子节点中存放的是指向数据的地址而不是真实的数据
(MyISAM使用非聚簇索引,在磁盘中有两个文件来存储,一个文件存储非聚簇索引,另一个文件存储索引指向地址的真实数据)
区别:
聚簇索引的叶子结点是数据节点;非聚簇索引的叶子节点为索引节点
聚簇索引只能有一个,非聚簇索引可以有很多个
Q8:查询执行流程是什么?
① 客户端发送一条查询给服务器。
② 服务器先检查查询缓存,如果命中了缓存则立刻返回存储在缓存中的结果,否则进入下一阶段。
③ 服务器端进行 SQL 解析、预处理,再由优化器生成对应的执行计划。
① MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询。
② 将结果返回给客户端。
Q9:索引优化
- 独立的列
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
例如下面的查询不能使用 actor_id 列的索引:
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
#2. 多列索引
在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。
SELECT film_id, actor_ id FROM sakila.film_actor
WHERE actor_id = 1 AND film_id = 1;
#3. 索引列的顺序
让选择性最强的索引列放在前面。
索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,每个记录的区分度越高,查询效率也越高。
例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment;
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16049
#4. 前缀索引
对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
前缀长度的选取需要根据索引选择性来确定。
#5. 覆盖索引
索引包含所有需要查询的字段的值。
具有以下优点:
索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。
一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引
数据类型
Q1:varchar和char的区别
Varchar :
存储可变字符串,比char更节省空间,varchar需要1或2个额外字节记录字符串长度,不会删除末尾空格
适用场景:字符串列的长度比平均长度大很多,列的更新少,使用了UTF-8这种复杂字符集
Char :
是定长的,根据定义的字符串长度分配空间,会删除末尾空格
适用场景:适合存储很短的字符串,或者所有字符串的值都接近同一个长度
(char在存储空间上更有效率,例如存储只有Y/N的值只需要一个字节,但varchar需要两个字节)
Q2:Datatime和Timestamp的区别
Datatime : 能保存大范围的值(1001~9999),精度为秒,把日期和时间封装到一个整数中,与时区无关,使用8字节的存储空间。
Timestamp : 保存范围比datatime要小很多(1970~2038),依赖于时区,使用4字节的存储空间。
Q3:数据类型优化策略
1) 尽可能小:
尽量使用可以正确存储数据的最小数据类型,更小的数据类型占用的磁盘,内存和CPU缓存更少,执行速率也就更快。
2) 尽可能简单:
简单数据类型的操作通常需要更少的CPU周期,例如整形比字符串操作代价更低(因为字符集和校对规则使得字符相比于整形更为复杂),例如:应该使用数据库的内置类型datetime而不是字符串来存储日期和时间;应使用整形来存储ip地址。
3) 尽量避免NULL:
通常情况下,最好指定列为NOT NULL,因为如果查询时碰到NULL的列对数据库来说更难优化
数据库缓存
Q1:什么是缓存
(缓存是用来协调数据传输速率差异的一种工具)
广义的理解:
凡是存在于两种传输速率相差很大的硬/软件之间,用来协调两者传输速率差异的结构,都可以称作是缓存
数据库缓存的理解:
缓存就是将原本要存储在磁盘中的数据,在内存中开辟出一块儿缓存区,存储到内存中的缓存区中,这样当再次访问数据时,会直接在内存的缓存区中查找,有则直接获取,没有再从磁盘上读取(缓存区的执行速率比磁盘的执行速率要快很多)
数据库读写操作的处理过程:处理写请求时,先将数据写入数据库(磁盘),然后再写入缓存;处理读请求时,先尝试从缓存获取,如果失败了在从数据库查询,并将查询结果写入缓存。
Q2:缓存更新策略
缓存更新策略一般分为四种:
Cache Aside(缓冲区扔一边):
① 查询数据时,先查询Cache缓存区,如果没有,去数据库中读取,在数据库中读取到之后,写入缓存区
② 更新数据时,先更新数据库中的数据,更新完成后,通过发送指令让缓冲区中的老数据失效(并没有将新数据立即写入缓冲区,可以避免脏读)
Read/Write Through(在读写过程中更新):
① 在读写数据或者更新数据的时候都直接访问缓存区
② 缓存区同步将数据更新到数据库(这个模式其实就是将缓存区作为主要的存储,应用的所有读写请求都直接和缓存打交道,而不管磁盘中的数据库,数据库由缓存来维护和更新)
Write Behind(写后更新数据库):
① 在读写数据或者更新数据的时候都直接访问缓存区
② 缓存区异步将数据更新到数据库(这个模式可以看做是上一种模式的变种,在更新或者写后再维护数据库,效率高,但数据的一致性差)
Q3:缓存穿透是什么?
(查询不存在的数据,每次都会查询到数据库,看起来就像是绕过(穿透)了缓存,每次都去查询数据库)
当查询的数据不存在时(因为不存在,所以缓存中一定不会有),查询缓存区未命中,查询磁盘中的数据库也未命中(出于容错考虑,不将空结果写回缓存),返回空结果。最终也会造成数据库CPU压力过大
(产生原因:自身代码出现问题;遭到一些网络恶意攻击)
Q4:缓存击穿是什么?
(缓存中没有数据但是数据库中有数据(一般是由于缓存时间到期))
针对的是并发用户特别多的情况,如许多用户同时查询同一条数据,这时这缓存区中的这条数据失效了,则用户们都去查询数据库,造成数据库压力过大。
Q5:缓存雪崩是什么?
(大面积的缓存失效,导致数据库访问压力过大)
针对的是多条缓存数据失效,例如在设置缓存时采用了相同的过期时间,造成在同一时刻大面积的缓存失效,那么原本访问这些缓存的请求,都去查询数据库,而对数据库CPU造成巨大的压力,严重的话会导致宕机(死机)
Q6:缓存穿透,缓存击穿,缓存雪崩的区别
1) 缓存穿透是所要查询的数据,在数据库和缓存中都不存在;缓存击穿和雪崩是所要查询的数据只在缓存区中不存在,但在数据库中可以获取
2) 缓存击穿可以理解为缓存雪崩的一个特例,缓存击穿主要针对高并发场景,多用户查询同一条缓存区中失效的数据;缓存雪崩还可以针对大面积数据同时失效的场景
Q7:缓存命中率是什么?
命中: 指的是可以直接通过缓存获取需要的数据
不命中: 无法直接通过缓存获取需要的数据,可能是数据根本不存在,或者缓存已经过期
(通常来讲,命中率越高,代表使用缓存的收益越高,应用性能越好)
可以通过缓存预热: (在系统上线后,提前将相关的缓存数据加载到缓存系统);增加存储容量;调整缓存粒度: (通常粒度越小,命中率越高,比如一个粒度为行的缓存数据,只有当这条数据变化时,才更新或移除缓存;但是如果是粒度为表的缓存数据,表中的任意一行数据变换,都要更新或移除缓存)等手段提高缓存命中率
优化
算法与数据结构
数据结构
完全二叉树:假如将树的节点按照广度优先的顺序编号,那么完全二叉树的编号与满二叉树的编号相同。
二叉搜索树—>二叉平衡树AVL树(平衡,左右的深度小于1)—>红黑树(自平衡的二叉查找树)
Q1:什么是(BST)二叉查找树
二叉查找树,又被称为二叉排序树/二叉搜索树。
定义:一颗树,若其左子树上所有节点的值均小于根节点,右子树上所有节点的值均大于根节点(左小右大),且其左右子树也为二叉查找树。
特点:二叉查找树的中序遍历会得到一个递增的数组。
应用: 二叉查找树的的查找与添加操作都比较简单,通过二分搜索的思想来实现;删除操作分为三种情况:
1,删除的是叶子节点(没有左右孩子)--直接删除即可(0个孩子)
2,删除的节点只有左孩子或右孩子--将被删除节点的子节点连接到被删除节点的父节点即可。(1个孩子)
3,删除的节点左右孩子都存在--找到其左子树中最大的节点或者右子树中最小的节点代替被删除节点。(2个孩子)
(二叉搜索树存在的问题:可能会出现斜树的情况,即所有节点都排成了一条线,会将原来查找的时间复杂度从O(log n)增大为O(n))
Q2:什么是(AVL)二叉平衡树?
AVL树即平衡二叉树,其实就是一颗平衡的二叉查找树
定义: 一颗树,其左右子树都是平衡二叉树。且其左右子树的深度之差的绝对值不大于1。
平衡因子:平衡二叉树的平衡因子(BF)定义为某个节点其左子树的深度减去其右子树的深度,平衡二叉树上所有节点的平衡因子只能为-1,0,1。
为了维护一颗插入新元素的平衡二叉树,需要用到左旋和右旋操作:
右旋:根节点的左孩子(x)向有旋转到根节点的位置(y),将根节点(y)作为左孩子(x)的右节点,将左孩子原来的右节点作为根节点(y)的左节点。
左旋:根节点的右孩子(y)向左旋转到根节点(x)的位置,根节点作为右孩子(y)的左节点,右孩子原来的左节点作为根节点(x)的右节点。
插入过程:
1,先按照二叉搜索树的方式插入新节点
2,更新树中每个节点的平衡因子
3,从新插入的节点开始,向上回溯找到第一个不平衡的节点
4,当找到第一个不平衡的节点后可能会出现四种情况,分别如下所示:
(1) 不平衡节点的子节点和孙子节点都位于左侧 (LL) ,直接将z右旋;
(2) 不平衡节点的子节点位于左侧,孙子节点位于右侧 (LR), 先对子节点y进行左旋,再对根节点z进行右旋;****
(3) 不平衡节点的子节点和孙子节点都位于右侧 (RR) ,直接将z左旋;
(4) 不平衡节点的子节点位于右侧,孙子节点位于左侧 (RL) ,先将儿子节点右旋,再将根节点左旋
( 删除操作和插入操作类似,但是要一直回溯到根节点)
Q3:什么是红黑树?
定义: 红黑树是一棵自平衡的二叉查找树,他具有如下性质:
1,每个节点或者是红色或者是黑色的
2,根节点是黑色的
3,每个叶子结点是黑色的
4,如果一个节点是红色的,那么他的子节点必须是黑色的
5,从一个节点到他任意后代null节点的每条路径都具有相同数量的黑色节点。
黑高:
从任意节点到其叶子节点的任意一条路径上黑色节点的数目。
插入操作:
插入操作主要通过旋转和重新着色两种方式
1,先进行标准的BST插入过程,将插入的节点设置红色
2,如果插入节点为根节点,将插入节点变为黑色即可
3,如果插入节点不是根节点且其父节点为红色,分以下两种情况考虑:
(1)如果插入节点的叔叔节点也为红色
将插入节点的父节点和叔叔节点变为黑色,将其爷爷节点变为红色,并将其爷爷节点作为最新的插入节点进行处理,重复2,3两步
(2)如果其叔叔节点为黑色, 则与AVL过程类似,通过旋转,并重新着色,着色规则为,最终将爷爷节点变为红色,父节点变为黑色,来完成红黑树。
以ll为例子。
删除操作:
Q4:AVL树和红黑树的区别
查询效率方面: AVL相比于红黑树,维护更加严格的高度平衡,在查询时比红黑树稳定性和效率稍高,但二者的查询效率大致相同都是O(log2 n)。
插入删除效率方面: AVL为了维护高度平衡,在每次插入删除过程中都需要进行大量的旋转操作,而且删除操作中,旋转操作要一直回溯到根节点;而红黑树以牺牲高度平衡为代价,通过重新着色减少旋转次数,将插入删除操作都控制在最多三次旋转内完成。所以红黑树等待插入删除效率更高。
Q5:b树(b-树)和b+树
为什么要使用b树:
当数据量过大时候,主存中存储不下,要将数据分块儿存储到磁盘中,与主存的访问时间相比,磁盘的I/O操作更加耗时,而b树的目的就是降低磁盘访问时间,大多说二叉平衡树的访问时间复杂度为O(log h),其中h为树的高度;而b树可以通过控制节点所包含的键的数目(也可以叫做数据库的索引,本质上是磁盘上的一个位置信息) 来控制树的高度(一般远小于h)
(注1: 每个节点(结点)所包含的key个数有上界和下界。用一个被称为B树的最小度数的固定整数t>=2来表示这些界。
注2: 阶树是指每个节点所包含的子节点的最大值)
b 树:
(1)除根节点外每个节点至少有t-1个key,所有节点至多包含2t-1个key
(2)每个节点中的key都按从小到大的顺序排列,key左孩子节点中的key都比他小,右孩子节点中的key都比他大。
(每个节点存放多个索引,索引的key是索引元素,value是索引元素所在的那一行记录的磁盘地址)
b+ 树:
(1)有k个子节点的节点,一定有k个key
(2)非叶子节点只包含key,起索引作用,不包含value
(3)非叶子节点之间通过指针相连,非叶子节点既有key又有value
区别:
(1)b树的所有节点都存放key和value,b+树的非叶子节点只存放key,不存放value
(2)b+树因为非叶子节点因为不存放value,因此内存页中可以存储更多的key,数据存放的更加紧密,有更好的空间利用率
(3)b+树因为叶子节点都通过指针相连,可以更方便遍历和数据库的范围搜索
(4)b树中如果访问的元素距离根节点较近,则对其的访问将更加迅速
Q6:大顶堆和小顶堆
实现一个堆:E:\Desktop\My Learning\刷题代码\二叉堆.txt
堆: 堆通常可以被看做是一个完全二叉树的数组
大顶堆: 堆中的每一个节点的值都大于等于其左右孩子节点的值
小顶堆: 堆中的每一个节点的值都小于等于其左右孩子节点的值
以大顶堆的图为例,则每个节点在数组中的索引规则如下,如果根节点的索引为****index ,则其左孩子的索引为2*index+1 ,右孩子的索引为2*index+2. 这个根结点的根节点的索引为index-1/2 。
堆化操作:(以构建一个大顶堆为例3,7,16,10,21,23 )
其实现代码如下:
Q7:跳表
排序算法
Q1:排序有哪些分类?
常见的排序算法有:
(1)快速排序;(2)冒泡排序;(3)希尔排序;(4)归并排序;(5)桶排序;(6)选择排序;(7)插入排序;(8)堆排序
Q2:插入排序的原理?
动图演示: 菜鸟程序员的动图 ** 排序动图 ** 插入排序 .gif****
从第二个数开始跟前一个数进行比较,如果比前一个数小则交换,直到比前一个大,若比前一个大则保持不变,时间复杂度为****O (n^2 ), 最优时间复杂度为O (n ), 空间复杂度为O (1 ), (向前比较并交换)
代码演示:
Q3:希尔排序的原理?
图形演示:
希尔排序是插入排序 的改进版本,将数组按照下标的一定增量进行分组,对每组进行插入排序,每次排序后减小增量,重复上述过程,直至增量减小至1,最后进行一次插入排序进行微调。如上图所示,对一个长度为10的数组,第一次选取增量为length/2=5(增量为5表示将数组分为5组),第二次取增量为5/2=2;最后一次取增量为1。时间复杂度优于O (n^2 ), 空间复杂度为O (1 )
代码演示:
Q4:选择排序的原理?
动图演示: 菜鸟程序员的动图 ** 排序动图 ** 选择排序 .gif****
从第一个数开始,循环一圈找到最小的数并交换位置,交换后再从下一个数开始重复,直至结尾。时间复杂度: O (n^2 ), 空间复杂度:O (1 ) ,(找最小数)****
代码演示:
Q5:堆排序的原理?
堆排序的思想就是将待排序序列构成一个大顶堆,每次讲堆顶元素和序列最后一个元素交换,保证最大的数在序列的最后位,再将前n-1个元素重新构成大顶堆,重复操作,最终得到一个有序序列。时间复杂度为O (n log n) ,空间复杂度为O (1) ,具体代码见数据结构的Q6大顶堆和小顶堆。
Q6:冒泡排序的原理?
动图演示: 菜鸟程序员的动图\排序动图\冒泡排序.gif
从第一个数开始,相邻元素两两对比,小的数放前面。(每循环一次,最后一个数都会被确定下来,为每轮的最大数),平均/最坏时间复杂度为: O (n^2) , 空间复杂度为: O (1) 。 (两两比较并交换)****
代码演示:
Q7:快速排序的原理?
动图演示: *菜鸟程序员的动图* *排序动图* 快速排序.gif****
快速排序的基本思想是选择一个元素作为基准点(可以选择数组的头,尾,中间或任意元素),比这个元素小的数放到他的左边,比这个元素大的数放到他的右边。然后对左右两边的子数组继续进行递归调用。平均/最好时间复杂度为O (n log n) ,最坏时间复杂度为O (n^2) ,空间复杂度为O (log n)****
代码演示:(以头元素为基准点为例子),(1) 设置两个指针low和high分别指向待排序列的开始和结尾,记录下基准值base val(待排序列的第一个记录), (2) 然后先从high所指的位置向前搜索直到找到一个小于base val的记录并互相交换, (3) 接着从low所指向的位置向后搜索直到找到一个大于base val的记录并互相交换, (4) 重复这两个步骤直到low=high为止。至此完成了一次快排。
快排的空间复杂度:考虑递归深度的话。最好logn,最差n,平均n。****
Q8:归并排序的原理?
动图演示: *菜鸟程序员的动图* *排序动图* 归并排序.gif****
归并排序就是先使用分治的思想将序列分为两部分,再将两部分分别递归排序(再分为两小部分,直到不能再分),对分开的两部分进行合并,逐层合并回原来的序列。
在合并的过程中可以引用一个辅助空间,并设置三个指针,其中两个指针分别指向两部分的头元素,第三个指针指向辅助空间的头元素,将两指针对应的较小元素放入到辅助空间中,重复该步骤到某一序列到达末尾,然后将另一序列剩余元素合并到辅助空间末尾。时间复杂度为O (n log n) ,空间复杂度为O (log n)
代码演示:
Q9:排序算法怎么选择?
当数据规模较小时:可以考虑插入排序和选择排序,当数据有序时,时间复杂度会减少到O(n)。
当数据规模中等时:可以考虑希尔排序
当数据规模较大时:可以考虑快排,归并排序和堆排序
计算机网 26
网络分层 2
Q1:OSI七层模型
OSI七层模型自底致上分别为:物理层**->** 数据链路层-> 网络层-> 传输层-> 会话层-> 表示层-> 应用层(武术网传会表应)
应用层: 最靠近用户的一层,为应用程序提供服务
表示层: 将下层传来的数据格式化或者进行数据加密,保证一个系统应用层发送的数据能被另一个应用层识别
会话层: 建立管理和维护会话,由请求和响应组成(cookie和session会话层技术)
传输层: 建立主机之间端到端的连接,为上层服务提供可靠透明的数据传输服务(TCP/UDP协议)
网络层: 进行地址解析和路由选择(IP DNS协议)****
数据链路层: 提供介质访问和链路管理(MAC地址)****
物理层: 实际信号的传输是在本层实现的,如电缆,双绞线,网线都是物理层的常用传输介质。
Q2:TCP/IP五层模型
TCP/IP的五层模型包括:物理层**->** 数据链路层-> 网络层-> 传输层-> 应用层,和IOS七层模型对应如下:
(注: 数据的传输过程都是自顶向下逐层封装,解析过程是自底向上逐层拆封)
应用层(HTTP HTTPS DNS)9
Q1:HTTP协议
HTTP是超文本传输协议,由客户端和服务器程序实现,客户端和服务器通过交换HTTP 报文进行通信,HTTP 定义了报文的格式和报文的交换方式,当用户请求web页面时,用户向服务器发送HTTP请求,服务器响应用户请求,并向客户端返回包含资源对象的HTTP响应。
HTTP是一种无状态协议,服务器不保存任何有关客户的信息,当客户端两次访问同一个web资源时,服务器会把每次请求都当成一次全新的请求。
Q2:HTTP状态码
2xx (成功):
1) 200 OK:表示服务器端发送的请求在客户端被正确处理
2) 204 NO Content:表示请求成功,但响应报文不包含数据,不会发生页面跳转
3) 206 Partial Content:进行范围请求
3xx (重定向):
重定向:假如现在收藏夹里有一个地址,但是这个时候原地址网页的目录结构变化了,转移到了新的地址,网站的扩展名变化了。不重定向的话,就会返回404 。 这就是重定向。
1) 301 moved permanently:永久性重定向,表示资源已经被分配了新的URL
2) 302 found:临时重定向,表示资源被临时分配了新的URL
3) 303 see other:表示资源存在着另一个URL,应该使用GET方法获取资源
4) 304 not modified:表示客户端请求的资源,在客户端已经有缓存并且可以继续使用
5) 307 temporary redirect:和302相同,临时重定向
4xx (客户端错误):
1) 400 bad request:请求的报文存在语法错误
2) 401 unauthorized:表示发送的请求需要有通过HTTP认证的认证信息
3) 403 forbidden:表示请求资源的访问被服务器拒绝
4) 404 not found:表示服务器上没有找到请求的资源
5xx (服务器错误):
1) 500 internal server error:表示服务器端执行请求时发生错误
2) 502 bad gateway:表示服务器执行请求时,从上游服务器收到了无效的响应
3) 503 service unavailable:表示服务器暂时处于超负载或正在停机维护,无法处理请求
4) 504 gateway timeout:表示服务器在执行请求时,未能及时从上游服务器收到响应
Q3:HTTP请求
包括请求行,头部信息,空行,请求数据。其中请求行包括:方法,url,协议和版本号
Q4:HTTPS协议
让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。
使用非对称密钥加密方式,传输对称密钥加密方式所需要的 Secret Key,从而保证安全性;
获取到 Secret Key 后,再使用对称密钥加密方式进行通信,从而保证效率。(下图中的 Session Key 就是 Secret Key)
HTTPS协议相当于HTTP协议的改进版本,在HTTP协议的基础上加入了安全机制,在应用层和传输层之间加入了SSL/TSL(SSL安全套接字层,TLS传输层安全协议,是SSL的新版本),将原本的HTTP与TCP直接通信变为HTTP先与SSL进行通信,SSL再与HTTP进行通信;采用对称加密和非对称加密的方式保证信息不被监听或篡改。(保证安全性,SSL,对称加密和非对称加密相结合)
注:与https相比http直接使用明文在浏览器和服务器之间传输信息,安全性较差,http的默认端口号是80,而https的默认端口号是443。
Q5:对称加密和非对称加密
对称加密: 就是加密和解密过程都使用同一个密钥
非对称加密: 密钥是成对出现的,分为公钥和私钥,公钥的加密需要用私钥来解密;私钥的加密需要公钥来解密(a将自己的公钥发布到网上,b通过公钥对数据进行加密并回传给a,这时只有拥有私钥的a能解密数据)
比较: 对称密钥传输速度快但是安全性较低,非对称密钥安全性相对较高,但是传输速度慢,所以在实际应用中一般采用对称加密和非对称加密相结合的方式来使用。****
(注: 非对称加密也存在安全性问题,因为公钥是发布到网上的,客户端不知道他收到的用公钥加密的数据是否来自于目标服务器 )****
Q6:HTTPS的加密流程
在TCP的三次握手完成后
1) 客户端使用https的url访问服务器,建立SSL连接
2) 服务器接收到SSL连接后,将申请到的CA认证证书和自己的公钥发给客户端,通过****CA 证书客户端可以知道收到的公钥是否为目的服务器发送的
3) 客户端收到公钥后对其验证,验证其是否有效
4) 客户端验证有效后获得服务器端公钥,客户端生成对称密钥,用服务器公钥将自己的密钥加密发给服务器端
5) 服务器端用自己的私钥进行解密,得到无法被其他人看见的客户端密钥
6) 之后客户端和服务器用这个密钥进行通信
(通过CA证书验证避免收到的公钥属于错误服务器;将对称密钥用公钥进行加密,只有服务器可以获得密钥,服务器和客户端使用密钥进行安全通信)
Q7:HTTP2.03
Q7:如何依靠CA证书来避免访问到伪造的服务端
(采用HTTPS协议的客户端必须有一套自己的数字证书,可以自己制作,也可以向组织申请)
(单向认证过程,即客户端保证收到的是正确服务器端发来的信息,而服务器端不用管客户端是不是正确的客户端)
1) 服务器将公钥和服务器个人信息通过hash算法形成信息摘要
2) 服务器采用CA提供的私钥对信息摘要进行加密生成数字签名(防止数字签名被伪造)
3) 最后将原始的个人信息,公钥,数字签名组合起来生成数字证书
4) 客户端拿到数字证书后用公钥对数字签名进行解密得到信息摘要
5) 将个人信息与服务器公钥进行hash得到另一份信息摘要
6) 比较两份信息摘要,如果相同,则说明是目标服务器
(服务器端将公钥,个人信息,以及公钥和个人信息hash运算的信息摘要,客户端将得到的个人信息与公钥进行hash运算得到另一份信息摘要,认证过程是一个比对两个信息摘要的过程)
Q8:DNS协议
(主机名/ 域名 -> IP 地址)
DNS域名解析系统,用于将主机名或域名映射为相应的IP地址,有着“翻译官”的作用(DNS采用递归查询的方式查找对应域名的IP地址)
域名:如www.baidu.com
1) 客户端提出域名解析请求,并将请求发送给本地的域名服务器
2) 当本地的域名服务器收到请求后,先查找本地缓存,如果有该项记录,则本地域名服务器就直接将查询结果(域名对应的IP)返回
3) 如果本地缓存中没有该记录,则本地域名服务器就直接将请求发给根域名服务器,然后根域名服务器再返回给本地域名服务器一个所查询域(根的子域)的主域名服务器的地址
4) 本地服务器再向上一步返回的域名服务器发送查询请求,过程同2,如果没有则返回相关的下级域名服务器的地址
5) 重复第4步,直到找到正确的记录
6) 本地域名服务器将查询的结果保存到本地缓存,同时将结果返回给客户
(先查询本地服务器的缓存->没有的话访问根域名服务器->得到其子域名服务器的地址->开始层层查询)
Q9:DNS劫持是什么
DNS劫持就是通过某些特殊手段,获得某域名解析控制权,将对该域名的访问由原****IP 地址转移到修改后IP 地址, 造成访问不到原网站或者访问到的是虚假网站。
Q10:如何避免DNS劫持
1) 为域名注册商和注册用邮箱设置复杂的密码且经常更换
2) 定期检查域名账户信息,域名注册商信息,查看事件管理器,清理web网站中存在的可疑文件
3) 将域名更新设置为锁定状态
Q11 : HTTPS
传输层(TCP UDP)8
Q1:TCP如何保证可靠性
1) 应用数据被分割成 TCP 认为最适合发送的数据块。
2) TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
3) 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
4) TCP 的接收端会丢弃重复的数据。
5) 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
6) 拥塞控制: 当网络拥塞时,减少数据的发送。
7) ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
8) 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
Q2:流量控制和拥塞控制是如何实现的
流量控制:
流量控制是针对端到端而言的,是为了解决接收端的接收速率赶不上发送端的发送速率而导致丢包,解决方法是采用滑动窗口:接收端在发送应答ACK时会把自己的即时窗口大小填入到TCP报文头部(即时窗口大小和接收端即时缓存区剩余容量相关联),发送端根据即时窗口大小控制自己的发送数据速率,当即时窗口大小为0时,发送端停止发送数据,并定期向接收端询问窗口大小的,发现窗口大小的恢复后继续开始发送。(根据即时窗口大小控制发送速率)
拥塞控制:
拥塞控制是为了防止过多的数据注入网络,分为慢开始,拥塞避免,快重传,快恢复****
慢开始: 初始化窗口大小为1,以指数形式增长
拥塞避免: 当窗口大小达到阀值时,以1的大小增大窗口,当发生超时重传时再次进入慢开始,并将阀值设为原来的一半
快重传: 当连续出现三次重复****ACK时,将窗口值设为当前的一半
快恢复: 当快重传发生后,跳过慢开始阶段,直接进入拥塞避免阶段
Q3:TCP的三次握手过程
(注: 标志位的含义ACK:确认序号有效;SYN:发起连接;FIN:释放连接)
1) 客户端向服务器端发送一个带有序列号seq=client-s的SYN报文,并将SY N标志位置1(seq,SYN=1)
2) 服务器端收到数据报后,为TCP连接分配缓存和变量,并将ACK标志位和SYN标志位置1,返回一个带有ack=client-s+1和seq=server-s的SYNACK报文段(ack ,seq ,ACK=1 ,SYN=1)
3) 客户端收到服务器端的数据报后,也要分配缓存和变量,将ACK位置1,向服务器端发送一个带有ack=server-s+1的确认号的报文段(ACK=1 ,ack)
Q4:为什么TCP是三次握手而不是两次或者四次
TCP协议的目的是保证数据的可靠传输,而TCP是全双工双向通信的,通过序列号和确认号实现( seq ,ack ), 三次握手是告诉对方自己的序列号,并通过对方的确认号,保证双方都收到了对方的数据,连接建立完成 , 如果是两次握手,则只有一方收到了确认号,而另一方收不到确认号无法保证可靠传输;如果是四次握手,则会造成不必要的浪费
Q5:TCP的四次挥手
1) 客户端将FIN位置1,并发送报文段给服务器,自己进入FIN-WAIT-1状态(FIN)
2) 服务器端收到FIN报文后,将ACK位置1并发送给客户端,自己进入CLOSE-WAIT状态,客户端收到报文后进入FIN-WAIT-2状态(ACK)
3) 服务器发送FIN位置1的报文给客户端,自己进入LASK-ACK状态(FIN)
4) 客户端收到报文段后,向服务器发送ACK=1的报文段,自己进入TIME-WAIT状态,服务器收到报文段的后进入CLOSE状态(ACK)
5) 客户端经过两个MSL(最长报文存活时间后)进入CLOSE状态
(客户端请求断开连接,服务器端发送确认断开,服务器端请求断开连接,客户端发送确认断开)
通俗描述3次握手就是
A对B说:我的序号是x,我要向你请求连接;(第一次握手,发送SYN包,然后进入SYN-SEND状态)
B听到之后对A说:我的序号是y,期待你下一句序号是x+1的话(意思就是收到了序号为x的话,即ack=x+1),同意建立连接。(第二次握手,发送ACK-SYN包,然后进入SYN-RCVD状态)
A听到B说同意建立连接之后,对A说:与确认你同意与我连接(ack=y+1,ACK=1,seq=x+1)。(第三次握手,A已进入ESTABLISHED状态)
B听到A的确认之后,也进入ESTABLISHED状态。
描述四次挥手就是:
1.A与B交谈结束之后,A要结束此次会话,对B说:我要关闭连接了(seq=u,FIN=1)。(第一次挥手,A进入FIN-WAIT-1)
2.B收到A的消息后说:确认,你要关闭连接了。(seq=v,ack=u+1,ACK=1)(第二次挥手,B进入CLOSE-WAIT)
3.A收到B的确认后,等了一段时间,因为B可能还有话要对他说。(此时A进入FIN-WAIT-2)
4.B说完了他要说的话(只是可能还有话说)之后,对A说,我要关闭连接了。(seq=w, ack=u+1,FIN=1,ACK=1)(第三次挥手)
5.A收到B要结束连接的消息后说:已收到你要关闭连接的消息。(seq=u+1,ack=w+1,ACK=1)(第四次挥手,然后A进入CLOSED)
6.B收到A的确认后,也进入CLOSED。
Q6:TCP为什么要四次挥手
因为TCP是全双工的,当一方收到FIN时,仅表示另一方申请断开连接不再发送数据,但是可以接收数据,一方也未必已经把全部数据发给另一方,所以在挥手阶段,将ACK和FIN分两次发送,可以保证数据的完整性
Q7:TCP为什么最后一次挥手要等待两个MSL
因为客户端发送的最后一个****ACK 包可能会丢失, 从而导致服务器端收不到对FIN的确认报文,如果不等待两个MSL而在一个MSL后就关闭客户端,服务器会一直处于LAST-ACK阶段。在正常TCP机制中,如果服务器在一段时间内没有收到确认报文,会重传FIN给客户端,客户端重启计时器,直到两个MSL后,如果没有再次收到FIN,则表示服务器端成功接收到了ACK,客户端关闭,这样可以保证服务器端和客户端都能正常关闭。
Q8:TCP和UDP的区别
1) TCP是传输控制协议,UDP是用户数据报协议,两者都是传输层协议
2) TCP向上提供面向连接的可靠传输服务,UDP是无连接的不可靠服务。
3) TCP传输速率慢,适用于少量重要完整数据,UDP传输速率快,适用于传输大量数据
4) TCP相较UDP安全性更低(TCP需要三次握手,握手过程中可能发生欺骗过程)
5) TCP只适用于单播,UDP适用于单播或者广播。
网络层(ARP ICMP IP)3
Q1:ARP协议的过程
(IP -> MAC )
ARP地址解析协议,是一个通过****IP 地址寻找目的主机MAC 地址的协议
当主机a 想要访问主机b 时:
1) 主机a先查看自己的本地缓存,查找主机b的IP地址对应的MAC地址
2) 如果主机a的缓存中没有相应的映射,他会将一个包含自己****IP 地址和MAC 地址以及主机b 的IP 地址的ARP请求分组发送到本局域网中
3) 局域网中的所有主机检查自己的IP地址和请求的IP地址是否一致,如果不一致则丢弃ARP分组
4) 当主机b接收到ARP分组时,会将主机a的IP地址和MAC地址保存到本地缓存,并向主机a以单播的形式发送一个响应分组,该响应分组中包含主机b的IP 地址和MAC 地址
5) 主机a收到响应分组后,将IP地址和MAC地址存入本地缓存
(当主机a得到了主机b的MAC地址,就可以开始通信了,本地缓存是有生命周期的,当生命周期失效后,会再次重复以上过程)
Q2:讲讲ARP欺骗
(通过伪造IP地址来实现)
由于主机之间的数据传输是通过MAC地址确定传输目标的,在建立连接时候会首先检查自己的本地缓存,且主机不会检验收到的响应分组是否是目的主机的响应,于是欺骗就发生了:
单向欺骗: 主机a向主机b发送连接请求,主机c将自己的IP地址伪造成主机b的IP地址,收到请求后,将自己的伪造IP地址和MAC地址发给主机a,主机a的缓存中会更新主机c虚假的IP地址和MAC地址,当主机a再次向主机b发送数据时,检查本地缓存找到的是主机c的MAC地址,会直接向主机c发送数据 (只欺骗一端)
双向欺骗: 主机a向主机b发送请求,主机c伪造自己的IP地址为主机b的IP地址,主机a更改缓存表为主机c的MAC地址;主机c再伪造自己的IP地址为主机a的IP地址,主机b更改缓存表为主机c的MAC地址。这样,在a与b通信的过程中,其实都是在与c通信。 (两端都欺骗)****
Q3:如何避免ARP欺骗
1) 采用静态双向绑定的方法:
在主机上将网关的MAC地址和IP地址进行绑定,在网关上将所有主机的MAC地址和IP地址进行绑定,并将缓存表设置为静态(即不自动更新缓存表)
2) 采用ARP防诈骗软件(如:ARP防火墙)和防火墙
网络通信 4
Q1:点击URL会发生什么
1) 在浏览器键入一个URL地址(键入)
2) 浏览器通过DNS域名系统查找访问资源所在服务器的IP地址(这里可能会应用到ARP协议)(DNS -> IP)
3) 确定好IP地址和端口号后,浏览器与目的主机进行TCP三次握手建立连接(建立TCP连接)
4) 在建立连接后向目的主机发送一个HTTP请求(发送HTTP请求)
5) 服务器响应HTTP请求,并将封装好的HTML超文本格式的数据返还给浏览器(返还HTML数据)
6) 浏览器解析HTML数据,并请求HTML数据中的资源,如:js,css,图片等(解析数据获取资源)
7) 浏览器对页面进行渲染并呈现给用户(渲染呈现)
Q2:转发和重定向的区别
转发:
转发的过程是客户端向服务器端的组件1发送request请求,组件1将request请求和response响应发送给组件2,组件2对请求做出响应后响应给客户端,是客户端和服务器之间一次请求—响应的过程。
重定向:
重定向的过程是客户端向服务器的组件发送请求,该组件向客户端返回一个响应,该响应信息不包含具体信息,只在响应头中包含了需要重定向的地址信息,客户端收到响应后,根据响应头中的地址再次进行请求响应过程,是客户端和服务器之间两次请求—响应的过程。
区别:
转发在服务器端完成的;重定向是在客户端完成的
转发的速度快;重定向速度慢
转发的是同一次请求;重定向是两次不同请求
转发不会执行转发后的代码;重定向会执行重定向之后的代码
转发地址栏没有变化;重定向地址栏有变化
转发必须是在同一台服务器下完成;重定向可以在不同的服务器下完成
Q3:get和post的区别
GET和POST的本质都是TCP连接,是用户和服务器端数据交互的方式
GET通过将参数包含在URL中来实现传参,POST通过response body来传递数据
GET将数据头header和数据data一起发送
POST将数据头header和数据data分两次发送(并不是所有浏览器都分两次发送,火狐浏览器一次发送)
(注:GET和POST是HTTP协议规定的两种数据传输方式)
Q4:Cookie和Session的区别
会话:打开一个浏览器直到关闭浏览器的过程(类似于拨通电话到挂断电话的过程),期间浏览器和服务器的多次请求响应都属于一次会话过程(类似于通话双方你一句我一句的聊天过程)。
因为HTTP协议是无状态协议,无法在一次会话过程后保存用户的信息,也就是说,同一个浏览器,再次访问之前访问过的服务器时,服务器还会将此次访问当做一次新的会话来处理。会话技术Cookie和Session应运而生。
Cookie:
Cookie将用户信息存储在客户端,但是Cookie是由服务器端创建的,服务器端创建Cookie,以key_value的方式设置Cookie值,并将Cookie加入到response中返回给客户端,在之后的请求响应过程中客户端request请求中会携带Cookie键值对。(Cookie相当于饭店的积分卡,由饭店(服务器)发给用户(客户端),用户自己保存)Cookie的默认生命周期跟浏览器绑定,浏览器关闭后Cookie失效。
Session :
Session将用户信息存储在服务器端,当客户端发送request请求时,浏览器首先检查request中是否已经包含了session标识(SESSIONID),如果已经包含了一个SESSIONID,说明服务器端已经为客户端创建过session,服务器根据ID检索出对应的session,如果不存在,则为客户端创建一个session并生成与其相关联的SESSIONID用cookie存储SESSIONID并返回给客户端。
区别:
Cookie保存在客户端,Session保存在服务器端
Cookie是在客户端的所以安全性较差,Session安全性相对较好
操作系统
用户态和内核态总结:
用户态和内核态是计算机在内存上按照逻辑划分的两块区域,他们的权限不同,内核态权限大,可以用来操作硬件,而用户需要操作硬件的话,是调用内核空间的接口实现的。
具体切换的情况有三种:系统调用,中断,异常。系统调用是用户态主动申请一些服务,例如申请使用某个文件。异常是程序发生了异常,然后调用内核态处理。中断是外围设备发出申请,会发出中断指令。
Q1:内核态和用户态的区别
程序的两种状态,切换的三种情况:系统调用,中断,异常。
申请锁的时候,会申请锁,可以认为内核态是与硬件进行交互的软件
系统调用:
1. 进程
2. 文件
3. 设备
4. 信息
5. 通信
读写文件,申请内存(new 对象)
1) 内核态和用户态是操作系统的两种运行级别,内核态处在最高的运行级别,用户态处在最低的运行级别。(运行级别)
2) CPU处于内核态时可以执行特权指令和非特权指令,处于用户态时只能执行非特权指令(特权指令:如读写操作,指令是CPU能识别,执行的最基本的命令)(特权指令和非特权指令)
3) 从用户态进入到内核态本质都是通过中断实现的,从内核态进入到用户态是通过执行一条特权指令来实现的,将程序状态字(PSW)的标志位设为“用户态”(两种状态之间的切换方式)
4) 处于内核态的程序可以访问系统的所有资源,但是对可靠性和安全性的要求较高,维护和管理都比较复杂,处于用户态的程序访问资源受限,但对可靠性和安全性的要求相对较低,且维护简单。(资源访问权限,安全性和可靠性)
Q2:内核态和用户态的转换
有三种切换方式分别为:
1) 系统调用: 是用户态主动要求切换到内核态的一种方式,用户态申请使用内核态的服务来完成工作。
2) 异常: 当异常发生时,系统自动将当前处于用户态的程序切换到内核态处理异常的程序中去。
3) 外围设备的中断: 当外围设备(如打印机)完成用户请求的操作时,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序。
Q3:用户态到内核态的具体切换步骤
运行在用户态的应用程序,将用于系统调用的参数传入到某个通用寄存器中-->执行陷入指令(中断)-->CPU切换到核心态执行系统调用相应服务程序-->返回用户程序。
注意:陷入指令是唯一一个只能在用户态下执行而不能在核心态下执行的指令。因为陷入指令是用于让CPU从用户态进入到核心态
Q4:线程和进程的区别
1.总的来说
首先,进程是包括线程的,一个大的进程可能分为好多小的线程。进程是系统进行资源分配和调度的基本单位,而线程是线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
2.相同点
它们都能提高程序的并发度,提高程序运行效率和响应时间。
3.不同点
多进程中每个进程有自己的地址空间,线程大部分地址空间都是共享的,其他的区别也就是基于这一点区别产生的。比如说:
-
速度。线程产生的速度快,通讯快,切换快,因为他们处于同一地址空间。(同样,进程之间的通信中的共享内存会效率比较高)
-
线程的资源利用率好。
-
线程使用公共变量或者内存的时候需要同步机制,但进程不用。
通信方式的差异:
因为共享内存的原因,线程之间不需要通信,就是我改动了值,因为内存是共享的,所以你立刻就知道了。但是要做好同步,就是我改过的值,你再去读,会读到这个改动。
而进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用。
Q5:什么是协程
协程是比线程更轻量级的存在,正如一个进程拥有多个线程一样,一个线程也可以拥有多个协程。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程完全由程序控制,也就是说会一直在用户态执行,不会像线程切换那样消耗资源。(注:在java的原生语法中并没有实现协程)
Q6:线程,进程的通信方式
首先线程之间的通信的目的是线程之间的同步,利用的主要是锁机制。线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
进程的同步是结果,进程的通信是方法手段。
进程的通信方式:共享内存,信号量,管道,消息队列,套接字
1) 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
2) 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。
具体执行过程:使用信号量对共享资源进行同步访问。信号量相当于程序计数器,本身也是临界资源,当进程要访问临界资源时,会检测信号量,如果信号量为正数,则进程进入临界资源,并且信号量-1;如果信号量为非正数,则进程挂起,直至有其他进程退出临界区,信号量+1。
3) 管道:一种半双工的通信方式,管道分为普通管道和命名管道,普通管道位于内存,命名管道位于文件系统,普通管道只能应用于父子进程之间的通信,命名管道可以应用于所有进程之间的通信。
4) 消息队列
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5) 套接字:用于不同机器间的通信。
Q7:进程切换的时机
进程切换方式主要分为两种:
1) 进程主动放弃CPU:
1.进程运行结束主动放弃
2.进程抛出异常
2) 进程被动放弃CPU:
1.进程在执行时遇到更紧急的任务
2.进程在执行时遇见优先级更高的进程
3.分给进程的时间片被用完了
在以上两种情况下可以进行进程的切换,但在如下的情况不能进行进程的切换:
1) 在处理中断时不能进行进程切换(中断操作较为复杂,很难同时进行进程的切换)
2) 在操作系统内核程序的临界区时不能进行进程切换(访问普通临界区时可以进行切换和调度)
3) 当进程运行原子操作的过程中不能进行切换(原子操作不能被打断)
Q8:进程的死锁
死锁是指多个进程同时等待对方资源,在得到对方资源之前不会释放自己资源,进而陷入了循环等待的状况。
产生死锁有四个必要的条件:
① 资源互斥:同一个资源不能被共享使用
② 继续请求:已经占有资源的进程还能继续请求其他资源
③ 资源不可剥夺:正在被占有的资源,在释放之前不能被强制性的分配给其他进程
④ 环路等待:系统中有两个或两个以上的进程形成了一个环路,都在等待其他进程所占有的资源
Q9:怎么理解操作系统里的内存碎片
内存碎片一般分为内部碎片和外部碎片****
内部碎片: 是指,CPU为每个进程分配固定大小的内存区域,当进程不能完全使用内存时,就会产生内部碎片。
外部碎片: 是指,内存中剩余的某些连续内存区间因为太小而无法分配给任何进程,这些内存区域被称作为外部碎片。
Q10:讲一讲页式存储
页式存储就是:
将内存上的物理地址分为若干大小相同的块;
将磁盘上的虚拟内存分为若干大小相同的页(页由页号和页内地址组成,页号也就是页表开始的地址,页内地址也可以叫做页偏移量)。
并且页的大小和块儿的大小相等(为了方便虚拟内存到物理内存的映射)
操作系统为每个进程创建一张页表,页表一般存放在内存中,页表右页号和块号组成:
通过这种方式,实现了在分配内存时,将几块儿不连续的内存分配给一个进程,减少了内存碎片的问题。
Q11:对虚拟内存的理解
虚拟内存是用于解决当进程数过多,实际物理内存空间不够用问题的
(可以理解为在磁盘上划分出一块儿区域作为内存区使用)
当进程创建时,系统会在磁盘上为每个进程分配一个大小为****4GB 的虚拟内存,虚拟内存会被每个进程看作是一段连续的地址空间,虚拟内存与物理内存和磁盘相映射,通过页表找到对应的资源。
当进程访问虚拟内存的时候会先查看页表,如果发现对应的数据不在物理内存上,就会产生缺页异常,操作系统阻塞该进程,并将磁盘里对应的页换入内存,并使该进程进入就绪状态,如果内存中的页已满,就会造成页覆盖。
Q12:虚拟地址如何映射成物理地址
物理地址=虚拟地址对应的块号*块大小+页偏移量
具体解释参考虚拟内存
Q13:堆和栈的区别
栈中存储的是方法中的局部变量,跟线程中的方法绑定,当方法中的局部变量生命周期结束后就会被释放。
堆中存储的是对象实例,对象实例不会因为其中某些属性的生命周期结束而释放,定期会有垃圾回收机制对堆中的对象实例进行清理。
栈内存是线程私有的,而堆内存是线程共享的,栈内存的更新速度远快于堆内存。
框架 27
Spring IoC 10
Q1:IoC 是什么?
(注:两种Ioc容器BeanFactory和ApplicationContext。)
BeanFactory:是基础类型的Ioc容器,采用延迟加载策略,当客户端调用某个被管理的对象时,才对其进行初始化和依赖注入操作。
ApplicationContext:是BeanFactory的加强版,采用即时加载策略,当容器启动后对所有被管理的对象进行初始化和依赖注入。
Ioc(Inversion of Control),即控制反转,不是一种技术,而是一种设计思想,将对象的创建过程交给容器来完成,为了直观的表述Ioc,也可以把Ioc称为:依赖注入。
控制反转:传统方法中,在一个对象中通过new来主动创建依赖对象,可以称之为正转,在spring框架中,通过Ioc容器创建依赖对象,原对象只是被动的接受依赖对象,称之为反转。
依赖注入:应用程序依赖于Ioc容器,Ioc容器为应用程序注入其所依赖(需要)的对象。
(Ioc管理的对象被称为bean对象。使用Ioc的优势:用来降低程序之间的耦合关系。)
Q2:IoC 容器初始化过程?
Ioc 容器的初始化过程大体可分为:定位->载入(载入和解析)->注册,三步。
)Resource``定位:
(指对BeanDefinition的资源定位过程。通俗地讲,就是找到定义Java Bean信息的XML文件,并将其封装成Resource对象。)
ApplicationContext`` 的所有实现类都实现了ResourceLoader接口, 通过调用getResource()方法返回Resource对象,即.xml文件名。
` 2. )BeanDefinition的载入和解析:```
(把用户定义好的Java Bean表示为IoC容器内部的数据结构,这个容器内部的数据结构就是BeanDefinition。)
通过调用父类 AbstractApplicationContext 的 refresh() 方法启动整个 IoC 容器对 Bean 定义的载入过程,该方法规定,在创建容器之前如果已有容器则销毁原容器,创建新容器,保证refresh后使用的是新创建的容器;
通过调用loadBeanDefinition(Resource res) 方法,将xml文件中的Bean配置信息转换为文档对象;
通过调用registerBeanDefinition(Document doc,Resource res) 方法,将载入的Document对象解析为BeanDefinition类型(BeanDefinition相当于在Ioc容器中被创建和使用的独有数据结构)
3. ``)BeanDefinition``的注册``:
(将解析得到的BeanDefinition向Ioc容器的beanDefinitionMap中映射)
Key``是字符串,Value是BeanDefinition,注册过程中需要使用 synchronized 保证线程安全。
Q3:依赖注入的实现方法有哪些?
依赖注入可以通过Annotation 注解进行注入,也可以通过配置bean.xml文件进行注入
1)配置注入又分为三种方式:构造函数注入;setter注入;接口注入
1.构造函数注入:Ioc容器检查被注入对象的构造方法,取得其所需要的依赖对象列表,进而为其注入相应的对象。在bean.xml配置文件的被注入对象标签下配置标签,该标签中指明对应构造函数参数的依赖对象,有四个属性:(1)index索引,即对应构造函数的第几个参数;(2)type依赖对象的类型;(3)ref指引用的依赖对象的id;(4)value如果注入的不是对象而是基本数据类型时用value。
2 ,setter注入:在被注入对象中,为依赖对象对应的属性添加setter方法,在bean.xml配置文件的被注入对象标签下配置标签,在标签中的name属性值会被自动添加set前缀,构成一个方法名,然后去对应的被注入对象中查找该setter方法,通过反射调用,实现注入。(注:如果通过set方法注入属性,那么spring会通过默认的空参构造方法来实例化对象,所以被注入对象类中一定要有空参构造方法)
3 ,接口注入:必须实现某个接口,接口提供方法来为其注入依赖对(很少使用)
2)通过Annotation注解进行注入:
1 , 首先要在主配置文件中开启注解扫描,<context:component-scan base-package=" ">,base-package用于指定需要扫描的包路径。
2 , 在需要注解的bean对象上加上注解声明:
@Repository:这个注解主要是声明 dao 的类组件;
@Service:这个注解主要是声明 service 服务类;
@Controller:主要是声明控制类
(例:@Repository(“MyDao”)等价于在bean.xml中配置
)
3 ,进行依赖注入:
@Resource:默认是以 by Name 方式注入(即根据bean的id注入),by Name 找不到的话,再用 by Type 去匹配;
@Autowired :默认是以 by Type 注入,-如果有多个实现类,他再用 by Name 的方式去匹配;
@Qualifier:可以指定实现的方法名称,与@Autowired 结合使用;
@Value :用于注入基本数据类型和 String 类型。
Q4:Bean 的生命周期?
Bean的生命周期分为:实例化->属性赋值->初始化->销毁这四个阶段。
1)实例化( Instantiation): 调用doCreateBean方法中createBeanInstance() 方法对Bean进行实例化处理;
2)属性赋值 (Populate) :调用doCreateBean方法中populateBean() 方法对Bean进行属性赋值;
3)初始化(InitializeBean) :调用doCreateBean方法中initializeBean() 方法对Bean进行初始化处理;
4)销毁(Destroy) :调用DisposableBean接口中的distroy() 方法对Bean对象进行销毁。
(在单例模式中,Bean的生命周期和容器的生命周期绑定,容器创建时bean创建,容器存在时bean存在,容器销毁时bean销毁;
在多例模式中,bean在被调用(getBean)时被创建,只要在使用中就一直存在,不再被引用的时候销毁。)
Q5:依赖注入的过程?
lazy-init属性决定了依赖注入的注入时机,当该属性为true时,依赖注入发生在第一次向容器获取Bean时候(第一次调用getBean()方法);当该属性为false时,依赖注入发生在容器初始化的过程中。
具体过程如下图所示:
Q6:Bean 的作用范围?
Bean的作用范围由xml文件中标签中的scope属性来控制,有五种作用范围,分别为:
① singleton:单例模式,是默认作用域,不管收到多少 Bean 请求每个容器中只有一个唯一的 Bean 实例。
② prototype:多例模式,和 singleton 相反,每次 Bean 请求都会创建一个新的实例。
③ request:每次 HTTP 请求都会创建一个新的 Bean 并把它放到 request 域中,在请求完成后 Bean 会失效并被垃圾收集器回收。
④ session:和 request 类似,确保每个 session 中有一个 Bean 实例,session 过期后 bean 会随之失效。
⑤ global session: 作用范围是一次全局会话,比如多台服务器之间需要共用同一个bean的时候就需要此属性。
Q7:如何通过 XML 方式创建 Bean?
通过xml来创建Bean有三种方式,分别为:通过默认构造函数创建;通过实例工厂方法创建;通过静态工厂方法创建。
①通过默认构造函数来创建: 通过在xml中配置标签,并指明id和class属性,期中class即为想要创建bean的类地址,默认调用想要被创建为bean的类的默认构造函数。
②通过实例工厂方法闯将: 创建工厂类,将工厂类创建为bean对象(使用上一种方法),再配置一个标签,其中factory-bean属性为上面创建的工厂bean的id,factory-method属性指明在工厂类中创建其他bean的方法(一般在工厂类中使用bean创建)
例:(因为实例工厂中的方法不是static的,在主函数中调用方法要先实例化工厂对象,所以要先创建工厂的bean对象)
③通过静态工厂方法创建: 创建工厂类,无需将该工厂类设置为bean,在工厂类中创建静态方法,静态方法用来创建其他bean,在标签中指明class和factory-method方法即可。
例:(因为静态方法是和类绑定的,所以在调用的时候直接使用类名.方法名即可,所以不需要再在xml配置文件中创建静态工厂的bean)
Q8:如何通过注解创建 Bean?
1 , 首先要在主配置文件中开启注解扫描,<context:component-scan base-package=" ">,base-package用于指定需要扫描的包路径。
2 , 在需要注解的bean对象上加上注解声明:
@Repository:这个注解主要是声明 dao 的类组件(持久层);
@Service:这个注解主要是声明 service 服务类(业务层);
@Controller:主要是声明控制类(表现层)
(例:@Repository(“MyDao”)等价于在bean.xml中配置
)
Q9:如何通过注解配置文件?
1 , @Configuration : 相当于主配置文件中的标签,用于指定当前类是一个Spring的配置类, 当容器创建时,会从该类上加载注解。(写在类前)。
2 , @Bean : 写在配置类下的方法上(返回某个实例对象的方法),等价于配置文件中的标签,用于注册bean。(一般情况下,configuration和bean在配置类中同时使用)
3 , @ComponentScan : 相当于SpringMvc.xml中的开启注解扫描标签
可以和@Configuration配合使用,其中basePackages属性用于指定所要扫描的包。使用@ComponentScan注解,需要使用@Component,@Repository等注解为想要声明为bean的类进行配置,之后在配置类中不再需要使用返回某个实例对象的方法,也不再需要@Bean注解。
4 , @PropertySource : 用于加载 .properties 文件中的配置。value 属性用于指定文件位置,如果是在类路径下需要加上 classpath。可以和@Value注解配合使用@Value(“${properties中的key}”)。
5 , @Import : 用于引入其他配置类。
Q10:BeanFactory、FactoryBean 和 ApplicationContext 的区别?
BeanFactory :是一个 Bean 工厂,使用简单工厂模式,是 Spring IoC 容器顶级接口,可以理解为含有 Bean 集合的工厂类,作用是管理 Bean,包括实例化、定位、配置对象及建立这些对象间的依赖。BeanFactory 实例化后并不会自动实例化 Bean,只有当 Bean 被使用时才实例化与装配依赖关系,属于延迟加载,适合多例模式。(是一个工厂)
FactoryBean :是一个工厂 Bean,使用了工厂方法模式,作用是生产其他 Bean 实例,可以通过实现该接口,提供一个工厂方法来自定义实例化 Bean 的逻辑。FactoryBean 接口由 BeanFactory 中配置的对象实现,这些对象本身就是用于创建对象的工厂,如果一个 Bean 实现了这个接口,那么它就是创建对象的工厂 Bean,而不是 Bean 实例本身。(是一个bean)
ApplicationContext: 是 BeanFactory 的子接口,扩展了 BeanFactory 的功能,提供了支持国际化的文本消息,统一的资源文件读取方式,事件传播以及应用层的特别配置等。容器会在初始化时对配置的 Bean 进行预实例化,Bean 的依赖注入在容器初始化时就已经完成,属于立即加载,适合单例模式,一般推荐使用。
(beanfactory和applicationcontext是ioc的两种容器,区别是后者是前者的强化,后者采用即时间加载,前者采用延迟加载;)
Spring AOP 4
Q1:AOP 是什么?
AOP(Aspect Oriented Program):即面向切面式编程, 是相对于OOP(Object Oriented Program)面向对象式编程而言的,简单地说就是将代码中重复的部分抽取出来,在需要执行的时候使用动态代理技术,在不修改源码的基础上对方法进行增强。实现了程序之间的解耦。
(将重复代码抽取出来作为一个切面,在需要的时候插回到代码中)
Q2:AOP 的相关术语有什么?
① Aspect (切面): 其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通Java类(存放共有功能的模块),之所以能被AOP容器识别成切面,是在配置中指定的。****
② Advice (通知) :是切面的具体实现。以目标方法为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。
③ Joinpoint (连接点) : 是一个虚拟的概念,就是程序在运行过程中可以插入切面的地方,所有的方法都可以看作是连接点。
④ PointCut (切入点) :指通知切入的连接点,(切入点一定是连接点,但连接点不一定是切入点),不同的通知通常需要插入在不同的切入点,这种匹配由切入点正则表达式完成。
⑤ Target (目标对象): 指插入通知的对象。
⑥ Proxy (代理对象): 就是把通知插入到的目标对象后,被动态创建的对象,具有原有对象的功能和通知新增的公共功能。(是对目标对象的增强)代理解决的问题当两个类需要通信时,引入第三方代理类,将两个类的关系解耦。
⑦ Weaving (织入) :指把通知应用于目标对象来创建代理对象的过程。
Q3:AOP中的代理技术
AOP代理可以分为动态代理和静态代理两部分:
静态代理:代理类在编译期就已经存在,因此也称为编译期增强。(编译期永久),静态代理会为每个业务都创建一个代理类,由代理类来创建代理对象。
动态代理:在运行时借助于 JDK 动态代理、CGLIB(code generate libary)字节码生成技术等在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。(运行期临时)
1) JDK 动态代理:
要实现的JDK动态代理,前提条件是目标类需要实现至少一个接口,动态代理过程依靠InvocationHandler接口和Proxy类来实现,具体流程如下:
(1)创建一个类实现InvocationHandler接口,并重写其中的invoke方法:
invokeResult一行执行的是目标对象的原有方法。
(2)通过Proxy.newProxyInstance(动态加载被代理类,被代理类的实现接口,使用handler)创建代理类。
(3)被代理对象享有目标对象的所有方法,并在调用原方法时,虚拟机会自动调用invoke方法,实现对原有方法的增强。
(JDK动态代理需要具备四个条件:目标类,目标接口,拦截器,代理类。之所以代理类拥有目标类的所有方法,是因为代理类实现了目标类的接口)
2) CGLIB 动态代理:
JDK动态代理通过代理类实现目标类的接口来实现,而在目标类没有实现接口的情况下,可以采用CGLIB动态代理,他是通过为目标类创建子类,并在子类中采用方法拦截父类所有方法的调用,实现动态代理。 (具体流程待补充)
Q4:AOP中的通知类型与实现方式
通知类型:
@Aspect : 声明被注解的切面为一个切面bean
@Before:(前置通知)在连接点方法执行之前执行
@After:(后置通知)在连接点方法执行之后执行(无论是否正常执行完毕)
@Around:(环绕通知)在连接点方法执行前后执行
@AfterThrowing:(异常通知)在连接点方法抛出异常后执行
@AfterReturning:(返回后通知)在连接点方法正常执行结束后执行,如果抛出异常则不执行。
实现方式:
(1) 基于xml配置文件实现:
① 在springmvc.xml文件中将通知类用bean配置起来,也可以通过注解配置通知类,并开启注解支持
② 声明AOP配置,使用 aop:config 标签
③ 配置切面,使用aop:aspect标签
④ 配置通知,以前置通知为例,写在aspect标签内部,表明通知类中的哪个方法是前置通知
⑤ 配置切入点表达式,使用 aop:pointcut 标签,用于指明通知类中的增强方法在目标对象的哪个方法中切入
写于aspect标签外部,作用范围为所有切面;写于aspect标签内部,作用范围为当前切面。
(2) 基于注解实现:
① 配置自动扫描,并开启AOP注解支持
② 将切面类声明为一个通知类,并注入到ioc容器中
③ 配置切入点表达式(可以声明一个函数,也可以在通知中配置)
④ 配置通知
Q5:切入点表达式的写法
使用execution关键字,格式:execution(表达式)
表达式: 访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
其中访问修饰符可以省略,返回值可以使用通配符*表示任意返回值
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个 *.
包名可以使用 . . 表示当前包及其子包
类名和方法名都可以使用 * 来实现通配
全通配写法: * * . . * . *(. .)
Spring MVC 3
Q1:Spring MVC 的处理流程?
客户端的所有请求都会被转发给前端控制器==>》 前端控制器请求处理器映射器查找处理器(被@Controller修饰的bean和被@RequestMapping修饰的方法和类)==>》 处理器映射器向前端控制器返回处理器(HandlerMapping会把请求映射为HandlerExecutionChain对象,包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象)==>》 前端控制器根据Handler找到对应的处理器适配器, 处理器适配器执行Handler中的方法,将请求参数绑定到方法的形参上,执行方法处理请求并得到模型和视图==>》 处理器适配器向前端控制器返回模型与视图,前端控制器请求视图解析器对模型与视图进行解析得到视图(View),然后对视图渲染,将数据填充到视图中并返回给客户端。
Q2:MVC中的核心组件
① DispatcherServlet(前端控制器):用户请求首先到达前端控制器,前端控制器调用其他组件处理用户请求,在web.xml文件下配置
② Handler(处理器):平时也叫做controller控制器,用于完成具体的业务。
③ HandlerMapping(处理器映射器):完成请求url到处理器的映射,前端控制器根据处理器映射器将不同的请求映射到不同的处理器中。
④ HandlerAdapter(处理器适配器):Handler执行业务方法前需要进行一系列操作,包括表单数据验证、数据类型转换、将表单数据封装到JavaBean等,这些操作都由 HandlerAdapter 完成。DispatcherServlet 通过 HandlerAdapter 来执行不同的 Handler。
(可以在springmvc.xml配置文件中加入如上的这个标签,会自动配置处理器映射器和处理器适配器)
⑤ ModelAndView(模型和视图):装载模型数据和视图信息,作为 Handler 处理结果返回给 DispatcherServlet。
⑥ ViewResolver(视图解析器):DispatcherServlet 通过它将逻辑视图解析为物理视图,最终将渲染的结果响应给客户端。(其中prefix为前缀,suffix为后缀)
(可以通过配置视图解析器,配置请求路径的前后缀,达到减少请求路径中代码的目的)
Q3:Spring MVC 的相关注解?
略
注:此面经董子贤同学原创,本人找实习与秋招期间补充总结所得。在此特别感谢。