1、Java基础
1.1 Java概述
1.1.1 Java和C++的区别
- 都是面向对象的语言,都支持封装、继承和多态
- Java不提供指针来直接访问内存,程序内存更加安全
- Java的类是单继承的,C++支持多重继承;虽然Java的类不可以多继承,但是 接口可以多继承。
- Java有自动内存管理机制,不需要程序员手动释放无用内存
1.2 基础语法
1.2.1 Java有哪些数据类型
1.2.2 访问修饰符的区别
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
1.2.3 final 有什么用?
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的
- 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
- 被final修饰的常量,在编译阶段会存入常量池中
1.2.4 final finally finalize区别
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
- finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
1.2.5 static存在的主要意义
static的主要意义是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
1.2.6 static的独特之处
- 被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
- 在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
- static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
- 被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
1.2.7 static应用场景
- 修饰成员变量
- 修饰成员方法
- 静态代码块
- 修饰类【只能修饰内部类也就是静态内部类】
- 静态导包
import static java.lang.Math.*;
public class Test{
public static void main(String[] args){
//System.out.println(Math.sin(20));传统做法
System.out.println(sin(20));
}
}
1.2.8 static注意事项
- 静态只能访问静态。
- 非静态既可以访问非静态的,也可以访问静态的。
1.2.9 String
1、String 类是 final 类,不可以被继承。
2、String 类的常用方法都有那些?
- indexOf():返回指定字符的索引。
- charAt():返回指定索引处的字符。
- replace():字符串替换。
- trim():去除字符串两端空白。
- split():分割字符串,返回一个分割后的字符串数组。
- getBytes():返回字符串的 byte 类型数组。
- length():返回字符串长度。
- toLowerCase():将字符串转成小写字母。
- toUpperCase():将字符串转成大写字符。
- substring():截取字符串。
- equals():字符串比较。
3、在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
1.2.10 Java 序列化中如果有些字段不想进行序列化 怎么办
使用 transient 关键字修饰。
transient 关键字的作用是:
- 阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient 修饰的变量值不会被持久化和恢复。
- transient 只能修饰变量,不能修饰类和方法。
1.2.11 红黑树
在原有的排序二叉树上增加了几个性质:
- 性质 1:每个节点非黑即红。
- 性质 2:根节点永远是黑色的。
- 性质 3:所有的叶节点都是空节点(即 null),并且是黑色的。
- 性质 4:每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
- 性质 5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
对于给定的黑色高度为 N 的红黑树,从根到叶子节点的最短路径长度为 N-1,最长路径长度为 2 * (N-1)。
1.2.12 Java注解概念
Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation 对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
1.3 面向对象
1.3.1 面向对象和面向过程的区别
面向过程:
- 是分析解决问题的步骤,然后用函数把这些步骤一步一步地实现,然后在使用的时候一一调用则可。性能较高,单片机、嵌入式开发等一般采用面向过程开发
- 缺点:没有面向对象易维护、易复用、易扩展
面向对象:
-
把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。
- 多态的三大手段:重写重载、抽象类、接口
-
性能上来说,比面向过程要低。
1.4 类与接口
1.4.1 抽象类和接口
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
相同点
- 接口和抽象类都不能实例化
- 都位于继承的顶端,用于被其他实现或继承
- 都包含抽象方法,其子类都必须覆写这些抽象方法
不同点
参数 | 抽象类 | 接口 |
---|---|---|
声明 | abstract | interface |
实现 | extends,如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | implements它需要提供接口中所有声明的方法的实现 |
构造器 | 可以 | 不可以 |
访问修饰符 | 任意 | 默认public,还可以是abstract |
多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final的 |
1.4.2 普通类和抽象类有哪些区别?
- 普通类不能包含抽象方法,抽象类可以包含抽象方法。
- 抽象类不能直接实例化,普通类可以直接实例化。
1.4.3 抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承, 这样彼此就会产生矛盾,所以 final 不能修饰抽象类
1.4.4 创建一个对象用什么关键字?对象实例与对象引用有何不同?
new关键字
对象实例在堆内存中,对象引用存放在栈内存中
一个对象引用可以指向0个或1个对象,一个对象可以有n个引用指向它
1.5 内部类
1.5.1 内部类的分类有哪些
成员内部类、局部内部类、匿名内部类和静态内部类
// 成员内部类
public class Outer {
private static int radius = 1;
private int count =2;
class Inner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
System.out.println("visit outer variable:" + count);
}
}
}
// 成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有;成员内部类依赖于外部类的实例
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
// 局部内部类
public class Outer {
private int out_a = 1;
private static int STATIC_b = 2;
public void testFunctionClass(){
int inner_c =3;
class Inner {
private void fun(){
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d =3;
class Inner {
private void fun(){
// System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}
// 匿名内部类
// 静态内部类
public class Outer {
private static int radius = 1;
static class StaticInner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
}
}
}
//静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit
1.6 重写与重载
1.6.1 构造器是否可被重写
构造器不能被继承,因此不能被重写,但可以被重载。
1.6.2 重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态 性,而后者实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同、参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回 类型进行区分
重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类 方法访问修饰符为private则子类中就不是重写。
1.7 对象相等判断
1.7.1 == 和 equals 的区别是什么
==
- 对于基本数据类型,判断内容是否相等
- 对于引用数据类型,判断内存地址是否相等,即判断是否是同一对象
equals:比较两个对象内容是否相等,equals是Object类的方法,如果类没有覆盖方法,则等价于==
1.7.2 hashCode()
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出 对应的“值”。这其中就利用到了散列码!
1.7.3 HashSet 如何检查重复
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals 的次数,相应就大大提高了执行速度。
1.7.4 为什么重写equals时必须重写hashCode方法?
- 提高效率:因为在比较一个对象的时候,会先进行hashCode比较,如果两个值的hashCode不同,则没必要进行equals的比较了,这样大大减少了equals比较的速度
- 为了保证同一个对象:如果重写了equals但是没有重写hashCode,就可能出现两个对象equals相等,hashCode却不一样的情况
1.7.5 对象的相等与指向他们的引用相等,两者有什么不同?
对象相等:内容相等 引用相等:内存地址相等
1.8 Java包
1.8.1 JDK中常用的包
- java.lang:这个是系统的基础类;
- java.io:这里面是所有输入输出有关的类,比如文件操作等;
- java.nio:为了完善 io 包中的功能,提高 io 包中性能而写的一个新包;
- java.net:这里面是与网络有关的类;
- java.util:这个是系统辅助类,特别是集合类;
- java.sql:这个是数据库操作的类。
1.9 IO流
1.9.1 java中IO 流分为几种?
- 按流向划分:输入流、输出流
- 按操作单元划分:字节流、字符流
- 按流的角色分:节点流、处理流
InputStream/Reader:所有输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
1.9.2 字符流和字节流的区别
- 字节流在操作时不会用到缓冲区(内存),是直接对文件本身进行操作的。而字符流在操作时使用了缓冲区,通过缓冲区再操作文件
- 如果一个程序频繁对一个资源进行IO操作,效率会非常低。此时,通过缓冲区,先把需要操作的数据暂时放入内存中,以后直接从内存中读取数据,则可以避免多次的IO操作,提高效率
- 在硬盘上的所有文件都是以字节形式存在的(图片,声音,视频),而字符值在内存中才会形成
1.9.3 BIO,NIO,AIO 有什么区别?
简答
- BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
- NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过Channel(通道)通讯,实现了多路复用。
- AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO,异步 IO 的操作基于事件和回调机制。
详细回答
- BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O, 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio包,提供了 Channel , Selector,Buffer。它支持面向缓冲的、基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- AIO (Asynchronous I/O): AIO 也就是 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。但是目前来说 AIO 的应用还不是很广泛。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
1.9.4 NIO
Non-blocking I/O,是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,能解决高并发与大量连接。
1.9.5 Files的常用方法都有哪些?
- Files. exists():检测文件路径是否存在。
- Files. createFile():创建文件。
- Files. createDirectory():创建文件夹。
- Files. delete():删除一个文件或目录。
- Files. copy():复制文件。
- Files. move():移动文件。
- Files. size():查看文件个数。
- Files. read():读取文件。
- Files. write():写入文件。
1.10 反射
1.10.1 什么是反射机制?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能称为java语言的反射机制。
-
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
-
缺点: 增加了安全问题,比如可以无视泛型的参数检查,性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
-
静态编译:在编译时确定类型,绑定对象
-
动态编译:运行时确定类型,绑定对象
1.10.2 反射机制的应用场景有哪些?
平时开发项目的代码中很少用到反射,但是我们使用的框架中很常见,比如
- 我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
- Spring框架也用到很多反射机制, 经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean的过程:
- 将程序内所有 XML 或 Properties 配置文件加载入内存中;
- Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
- 使用反射机制,根据这个字符串获得某个类的Class实例
- 动态配置实例的属性
1.10.3 Java获取反射的三种方法
- 通过new对象实现反射机制
- 通过路径实现反射机制
- 通过类名实现反射机制
public class test {
public static void main(String[] args) throws ClassNotFoundException {
User user = new User();
// new对象
Class<? extends User> c1 = user.getClass();
System.out.println(c1);
// 路径
Class<?> c2 = Class.forName("com.atguigu.User");
System.out.println(c2);
// 类名
Class<User> c3 = User.class;
System.out.println(c3.getName());
}
}
1.10.4 反射中,Class.forName和classloader的区别
两者都可以用来对类进行加载
- Class.forName()除了将类的.class文件加载到jvm,还会对类进行解释,执行类中的static块,Class.forName(name, initialize, loader)带参函数也可控制是否加载static块
- classLoader只会将class文件加载到jvm,只有在调用newInstance才会去执行static块
1.10.5 反射的API
- Class 类:反射的核心类,可以获取类的属性,方法等信息。
- Field 类:表示类的成员变量,可以用来获取和设置类之中的属性值。
- Method 类: 类的方法,它可以用来获取类中的方法信息或者执行方法。
- Constructor 类:表示类的构造方法。
1.11 集合
1.11.1 常用的集合类有哪些?
-
Collection接口的子接口包括:Set接口和List接口
- Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
- List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等
- Queue接口的实现类主要有:ArrayDeque、ArrayBlockingQueue、PriorityQueue
-
Map接口的实现类主要有:HashMap、TreeMap、Hashtable、 ConcurrentHashMap以及Properties等
1.11.2 Java集合的快速失败机制 “fail-fast”?
是java集合的一种错误检测机制,如果在多线程下试图改变集合的结构,就可能会产生fail-fast机制
例如:存在两个线程,线程1通过iterator遍历集合A的元素,这时线程2改变了集合A的结构(是改变结构,不是修改值),这时候程序就会抛出ConcurrentModificationException异常,从而产生fail-fast机制
原因:迭代器在遍历访问集合中的内容时,会维护一个modCount的变量。集合如果在被遍历期间内容发生变化,modCount值就会改变,每当迭代器去遍历下一个值的时候,就会检查modCount是否为expectedModCount,是就返回遍历,否则就抛出异常
解决方法
- 在遍历过程中,所有涉及到改变modCount值的地方都加上synchronized
- 使用线程安全的集合
1.11.3 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?
遍历方式有以下几种:
- for循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到后一个元素后停止。
- 迭代器遍历。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口
- foreach循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
最佳实践:Java Collections框架中提供了一个RandomAccess接口,用来标记List是否支持RandomAccess,如果支持,例如ArrayList,按位置读取元素的时间复杂度为O(1),建议使用for循环遍历,如果不支持,例如LinkedList,建议使用迭代器或者foreach循环遍历
1.11.4 如何实现数组和 List 之间的转换?
数组 -->List:使用 Arrays. asList(array) 进行转换。
List -->数组:使用 List 自带的 toArray() 方法。
1.11.5 BlockingQueue是什么?
阻塞队列,主要用于实现生产者–消费者模型
队列类型
- 无限队列:几乎可以无线增长
- 有限队列:队列容量固定、
方法 | 说明 |
---|---|
add() | 插入成功则返回 true,否则抛出 IllegalStateException 异常 |
put() | 将指定的元素插入队列,如果队列满了,那么会阻塞直到有空间插入 |
poll() | 如果插入成功则返回 true,否则返回 false |
offer(E e, long timeout, TimeUnit unit) | 尝试将元素插入队列,如果队列已满,那么会阻塞直到有空间插入 |
1.11.6 说一下 HashMap 的实现原理?
为什么HashMap中链表长度超过8会转换成红黑树
红黑树平均查找长度为log(n),链表为n/2,
log(8)=3,8/2=4,所以长度为8时转换为树查找速度才会提升
小于6才会退化为链表
中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
get、put方法
put
-
首次扩容:判断数组是否为空,空则进行首次扩容
-
通过key计算出hash值,找到当前对象在数组中存储的下表
-
如果当前位置没有元素,则直接插入,如果有,则用equals一一比较key是否相等,有一样的key的话,覆盖掉之前的value,没有的话插入到链表
-
如果数组中元素个数达超过了Load Facotr,则进行再次扩容
get
- 利用key计算出hash值,找到当前对象在数组中存储的下表
- 通过equals找到对应的键值对
HashMap死循环:只会发生在JDK1.7,因为使用的是头插法
JDK1.7 1.8区别
-
在JDK1.7的时候hashmap实现是用数组+链表,在JDK1.8之后,加入了红黑树,当链表的长度达到了8,如果继续用链表,则会导致查询的效率降低,所以会将链表转为红黑树,提高查询的效率
-
JDK1.7还使用的是头插法,JDK1.8改为了尾插法,原因是,在多线程下,如果使用头插法,可能会导致循环链表,
HashMap的扩容机制
- 数组的初始容量为16,每次以2的次方扩容。一是可以使用足够大的数组提高性能,二是用位运算替代取模运算,提高计算速度
- 是否需要扩容通过负载因子判断,如果数组大小超过了负载因子则进行扩容,默认是0.75
- 还有当链表长度达到8时,会从链表转换为红黑树,小于六时也会从红黑树转为链表
为什么链表长度到8才转红黑树,为什么不直接使用红黑树
红黑树需要左旋右旋,在元素较小的时候,还是链表性能更好
而且链表的平均查找长度是n/2,红黑树的平均查找长度是log(n),log(8)=3 < 8/2=4 这才有必要转换,
HashMap是怎么解决哈希冲突的?
什么是哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做哈希碰撞
解决哈希冲突
static final int hash(Object key) {
int h;
// 判断key是否为null, 如果为null,则直接返回0;
// 如果不为null,则返回(h = key.hashCode()) ^ (h >>> 16)的执行结果
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
问题:明明通过第一步h = key.hashCode()就可以作为hash返回,为什么还需要后面的计算 ----- 为了减少hash冲突
元素在数组中存放的位置是由下面这行代码决定的:
i = (n - 1) & hash
当数组长度n较小时,n-1的二进制数高16位全部位0,这个时候如果直接和h值进行&
(按位与)操作,那么只能利用到h值的低16位数据,这个时候会大大增加hash冲突发生的可能性,因为不同的h值转化为2进制后低16位是有可能相同的
当我们使用 h ^ (h >>> 16) 操作时,会将h的高16位数据和低16位数据进行异或操作,最终得出的hash值的高16位保留了h值的高16位数据,而hash值的低16数据则是h值的高低16位数据共同作用的结果。所以即使h1和h2的低16位相同,最终计算出的hash值低16位也大概率是不同的,降低了hash冲突发生的概率。
为什么是(n-1)?:同样是为了降低hash冲突发生的概率
首先hashMap扩容是以2的次方扩容的,既然n为2的整数次幂,那么n一定是一个偶数。那么n转化为2进制后最低位一定为0,与hash进行按位与操作后最低位仍一定为0,这就导致i值只能为偶数,这样就浪费了数组中索引为奇数的空间,同时也增加了hash冲突发生的概率。
所以我们要执行n-1,得到一个奇数,这样n-1转化为二进制后低位一定为1,与hash进行按位与操作后最低位即可能位0也可能位1,这就是使得i值即可能为偶数,也可能为奇数,充分利用了数组的空间,降低hash冲突发生的概率。
HashMap 的长度为什么是2的幂次方
尽量减少碰撞,让数据分布均匀,此时就要靠将数据存入到链表中的算法,就是个取余操作,hash%length
,但是用位运算代替取余可以提升效率,所以使用的是**hash&(length-1)
**,使这两个公式相等的前提就是length是2的n次方
所以,使用2的幂次方,总的来说还是为了减少hash冲突,而且使用按位与&运算可以提高效率
1.11.7 HashMap和HashTable的区别
HashMap | HashTable | |
---|---|---|
线程安全 | 安全 | 不安全 |
效率 | 高 | 低 |
初始大小 | 16 | 11 |
扩容大小 | 2的幂次方 | 2n+1 |
对null的支持 | key可以有一个null,value可以有无数个 | 不支持 |
1.11.8 ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?
JDK1.7
底层数据结构:Segments数组+HashEntry数组+链表,采用分段锁保证安全性
分段锁:容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。
- 一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。
- Segment继承自ReentrantLock锁。 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。
get()操作:
HashEntry中的value属性和next指针是用volatile修饰的,保证了可见性,所以每次获取的都是最新值,get过程不需要加锁。
-
1.将key传入get方法中,先根据key的hashcode的值找到对应的segment段。
-
2.再根据segment中的get方法再次hash,找到HashEntry数组中的位置。
-
3.最后在链表中根据hash值和equals方法进行查找。
ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。
put()操作:
-
1.将key传入put方法中,先根据key的hashcode的值找到对应的segment段
-
2.再根据segment中的put方法,加锁lock()。
-
3.再次hash确定存放的hashEntry数组中的位置
-
4.在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。
JDK1.8
底层数据结构Synchronized + CAS + Node +红黑树 Node的val和next都使用了volatil保证可见性 查找,替换,赋值操作都使用CAS
为什么在有Synchronized的情况下还要使用CAS
因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能。
get()操作:
get操作全程无锁。get操作可以无锁是由于Node元素的val和指针next是用volatile修饰的。
在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见的。
-
1.计算hash值,定位到Node数组中的位置
-
2.如果该位置为null,则直接返回null
-
3.如果该位置不为null,再判断该节点是红黑树节点还是链表节点
-
如果是红黑树节点,使用红黑树的查找方式来进行查找
-
如果是链表节点,遍历链表进行查找
-
put()操作:
-
1.先判断Node数组有没有初始化,如果没有,先初始化initTable();
-
2.根据key进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入
-
3.如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁
-
4.如果是红黑树,就按照红黑树的结构进行插入。如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。
1.11.9 comparable 和 comparator的区别?
- comparable接口实际上是出自java.lang包,它有一个 compareTo(Object obj)方法用来排序,用于自定义一个集合的排序
- comparator接口实际上是出自 java.util 包,它有一个compare(Object obj1, Object obj2)方法用来排序,用于自定义一个集合实现两种排序方式,比如age和name,age相同按name排
1.11.10 TreeMap 和 TreeSet 在排序时如何比较元素? Collections 工具类中的 sort()方法如何比较元素?
TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。
TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。
Collections 工具类的 sort 方法有两种重载的形式,
- 第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;
- 第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。
1.12 Java异常
1.12.1 异常概述
所有的异常都有一个共同的祖先Throwable类,Throwable有两个子类
- Error:程序无法处理的错误,这些错误一般是代码运行时JVM出现的问题,例如Virtual MachineError、OutOfMemoryError
- Exception:程序本身可以处理的异常,Exception 又有两个分支,一个是运行时异常 RuntimeException , 一个是 CheckedException
- RuntimeException:NullPointerException 、 ClassCastException,是程序员代码的错误
- CheckedException:一般是外部错误,这种异常都发生在编译阶段,Java 编译器会强制程序去捕获此类异常,即会出现要求你把这段可能出现异常的程序进行 try catch,该类异常一般包括几个方面:
- 试图在文件尾部读取数据
- 试图打开一个错误格式的 URL
- 试图根据给定的字符串查找 class 对象,而这个字符串表示的类并不存在
1.12.2 异常分类
-
不进行具体处理,抛给调用者,throw、throws、系统自动抛异常
-
try-catch-final
throw和 throws 的区别:
- throws 用在函数上,后面跟的是异常类,可以跟多个;而 throw 用在函数内,后面跟的是异常对象。
- throws 用来声明异常,让调用者只知道该功能可能出现的问题,可以给出预先的处理方式;throw抛出具体的问题对象,执行到throw,功能就已经结束了,跳转到调用者,并将具体的问题对象抛给调用者。
- throws 表示出现异常的一种可能性,并不一定会发生这些异常;throw 则是抛出了异常,执行throw 则一定抛出了某种异常对象。
- 两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异常,真正的处理异常由函数的上层调用处理。
1.13 Object类的方法
- getClass:获取运行时类型
- hashCode:获取hash值
- equals:判断两个对象是否相等,可以重写用于实现不同的比较
- clone:浅拷贝
- toString:返回字符串
- wait
- notify
- notifyAll
- finalize:对象在被GC前一定会调用finalize方法,
2、JVM
2.1 说一下 JVM 的主要组成部分及其作用?
- 1.类加载器(classLoader):将字节码文件加载到运行时数据区
- 2.运行时数据区(Runtime data area):JVM内存
- 3.执行引擎(Execution engine):将JVM指令集翻译为操作系统指令集
- 4.本地库接口(Native Interface):是与其他编程语言交互的接口
JVM作用:首先通过编译器把 Java 代码转换成字节码,类加载器(ClassLoader) 再把字节码加载到内存中,将其放在运行时数据区(Runtime data area)的方 法区内,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(ExecutionEngine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。
2.1.1 什么是类加载器,类加载器有哪些?
实现通过类的全限定名获取该类的二进制字节流的代码块叫做类加载器。
- 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被 java程序直接引用。
- 扩展类加载器(extensions class loader): 它用来加载 Java 的扩展库。 Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH )来加载 Java类。一般来说,Java 应用的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现。
2.1.2 类装载的执行过程
- **加载:**通过类的全限定名获取类的二进制字节码,然后生成一个代表这个类的java.class.lang对象作为方法区这个类的各种数据的访问接口
- **验证:**验证class文件的正确性
- **文件格式验证:**验证class文件是否符合字节码文件规范,以及能否被当前版本的JVM处理
- **元数据验证:**对字节码描述的信息进行语义分析,保证其符合Java语言规范
- **字节码验证:**通过数据流、控制流分析,确保语义是合法的、符合逻辑的
- **符号引用验证:**验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段
- **准备:**为类中定义的静态变量分配空间
- **解析:**将符号引用替换为直接引用
- **初始化:**为静态变量和静态代码块执行初始化工作
2.1.3 双亲委派机制
双亲委派机制的好处
避免类的重复加载
每次进行类加载时,都尽可能由顶层的加载器进行加载,保证父类加载器已经加载过的类,不会被子类再加载一次,同一个类都由同一个类加载器进行加载,避免了类的重复加载。
防止系统类被恶意修改
通过双亲委派模型机制,能保证系统类由Bootstrap ClassLoader进行加载,用户即使定义了与系统类相同的类,也不会进行加载,保证了安全性。
当一个类加载器接收到类加载请求的时候,会先请求其父类加载,直到请求到最顶层的类加载器,然后开始进行加载,如果不能加载,则会交给子类尝试加载
破坏双亲委派机制
**1、自定义加载类,重写loadclass()方法:**因为双亲委派的机制都是通过这个方法实现的,这个方法可以指定类通过什么类加载器来进行加载,所有如果改写他的加载规则,相当于打破双亲委派机制
**2、使用线程上下文类:**这是由这个模型本身的缺陷导致的,如果一个基础启动类又需要调用用户代码,那就需要引入线程上下类加载器
**3、用户对程序动态性的追求导致的:**这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
2.2 JVM 运行时数据区
JVM在执行程序的过程中会把它所管理的内存区域划分为5个不同的数据区域,其中可以分为线程共享和线程私有的
线程共享
- 程序计数器:指向当前字节码文件运行的位置,因为在执行过程中,CPU会不停的在多条线程中切换,当切换回去的时候需要知道从哪儿继续执行
- 虚拟机栈:为java方法服务的,当调用一个java方法的时候,栈内就会创建一个栈帧,栈帧中包含:局部变量表、操作栈、动态链接、方法出口,每个java方法的调用以及结束,都对应着一个栈帧的入栈和出栈
- 本地方法栈:和虚拟机栈一样,只不过是为本地方法服务
线程私有
- Java堆:几乎所有java对象都在这里分配内存
- 方法区:用于存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据
- 直接内存:
2.3 堆和栈的区别
- **物理地址:**堆的地址是不连续的,栈是连续的
- **内存分配:**堆的内存是在运行时分配的,大小不固定,栈需要连续的空间,所以在编译期间就确定了,大小固定,一般来说,堆的远远大于栈
- **存放的内容:**堆存放的是对象实例和数组,栈存放的是局部变量表、操作栈、动态链接、方法出口等
- **程序可见度:**堆是共享的,栈是线程私有的
2.4 对象的创建
2.4.1 创建对象方法
- new关键字
- newInstance方法(有Class类和Constructor)
- clone
- 反序列化
2.4.2 创建对象的流程
- Step1:当JVM收到一条new指令时,会先去常量池检查是否已经加载该类,如果没有,先加载这个类
- Step2:然后给这个类分配内存,如果内存是规整的,则使用指针碰撞,否则使用空闲列表
- Step3:还需要考虑并发问题,可以使用CAS同步处理,也可以使用TLAB(ThreadLocal Allocation Buffer,本地线程分配缓冲)
- Step4:内存空间初始化操作,然后做一些必要的对象设置(元信息、哈希码)
2.4.3 为对象分配内存
类加载完成后,接着会在Java堆中划分一块内存分配给对象。内存分配根据Java 堆是否规整,有两种方式:
- **指针碰撞:**如果内存是规整的,即所有用过的内存放一边,空闲的内存放另一边。分配内存时位于中间的指针指示器向空闲的一端移动与对象大小相等的距离
- **空闲列表:**如果内存不是规整的,则需要JVM维护一个列表记录哪些内存是可用的,在分配的时候可以从列表中查询到足够大的内存分配给对象,并更新列表
2.4.4 处理并发安全问题
- 对分配内存空间进行同步处理,使用CAS+失败重试的方式保证更新操作的线程安全。但是这样每次分配时都需要进行同步控制,效率比较低
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配。只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁。通过-XX:+/-UserTLAB参数来设定虚拟机是否使用TLAB。
2.5 对象的访问定位
Java程序需要通过 JVM 栈上的引用访问堆中的具体对象。目前主流的访问方式有句柄和直接指针两种方式。
指针: 指向对象,代表一个对象在内存中的起始地址。
**句柄:**可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。
2.6 内存泄露、内存溢出
内存泄漏:不再使用的对象一直占据着内存
内存溢出:系统内存不够,会报错OOM
内存泄漏的解决方法
2.7 Java的四种引用
- **强引用:**OOM的时候也不会被回收
- 软引用:内存不足时会被回收
- 弱引用:GC操作时,即使内存够,也会被回收
- 虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在,不会对对象生存时间构成影响,也无法通过虚引用来取得一个对象的实例,虚引用存在可以来判断对象是否被回收
2.8 GC
2.8.1 怎么判断对象是否可以被回收?
- **引用计数器法:**为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它不能解决循环引用的问题;
- **可达性分析算法:**从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。 当一个对象到 GCRoots 没有任何引用链相连时,则证明此对象是可以被回收的。
在java语言中,什么对象可作为GCRoot的对象?
- JVM栈中引用的对象
- 本地方法栈中JNI(native方法)引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
2.8.2 垃圾回收算法
-
-
标记–清除:首先标记出需要回收的对象,然后清除掉标记的对象
优点:实现简单,对象不需要移动
缺点:1. 如果需要清除的对象多,就需要大量的标记、清除动作 2.会产生内存空间碎片问题
-
-
-
标记–复制:将内存划分为两个大小相等的区域,一次只使用其中的一块内存,当一块内存用完了,就将存活的对象复制到另一块内存上,然后将目前正在使用的那块内存清理掉
优点:实现简单,不用考虑内存碎片问题
缺点:可用内存为原来的一半,对象存活率高时会进行频繁的复制
-
-
-
标记–整理:过程和标记–清理差不多,但是最后存活的对象都向内存空间的一端移动
**优点:**解决了标记–清理的内存碎片问题
**缺点:**仍然需要局部对象移动
-
-
-
分代收集算法:当前商业虚拟机都采取的分代垃圾回收算法。根据对象的存活周期,将内存划分为年轻代、老年代、永久代,年轻代又划分为endn和两个幸存区(默认占比8:1:1),GC发生在年轻代和老年代
-
2.8.3 垃圾回收器
- 新生区
- Serial:新生代单线程收集器,简单高效
- ParNew:新生代并行收集器,实际上是Serial的多线程版本
- Parallel Scavenge:新生代并行收集器,追求高吞吐量,高效利用CPU,适合对交互要求不高的场景
- 老年区
- Serial Old:老年代单线程收集器
- Parallel Old:老年代并行收集器,吞吐量优先,Parallel Scavenge的老年版
- CMS:老年代并行收集器,以获取最短回收停顿时间为目的,具有高并发、低停顿的特点,基于标记–清理算法实现
- 堆
- G1:Java堆并行收集器,基于标记–整理算法实现,G1的回收范围是整个Java堆(新生代、老年代)
2.8.4 CMS垃圾回收器
Concurrent Mark-Sweep,运作过程分为四个步骤
- **初始标记:**仅标记GC Root能直接关联的对象,速度很快
- **并发标记:**从GC Root能直接关联的对象出发,标记整个对象图,时间较长,但是不会STW,可以与用户线程一起并发运行
- **重新标记:**因为在并发标记期间,有些对象的标记记录可能被改变了,所以需要重新标记(会STW,不然可能对象的标记记录又变了)
- **并发清除:**清理标记阶段已经死亡的对象,由于不需要移动存活对象,所以这个阶段也可以和用户线程并发执行
缺点
- CMS收集器对处理资源非常敏感,在并发阶段虽然不会导致STW,但是因为占用了一部分线程(或者说CPU的计算能力)而导致应用程序变慢,降低吞吐量
- CMS基于标记–清理算法,所以可能产生大量的内存碎片,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
- CMS收集器无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”而导致Full GC的产生
- 浮动垃圾:在并发清除阶段,因为没有STW,所以可能产生了一些新的对象
使用:-XX:+UseConcMarkSweepGC
2.8.5 分代回收器
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
- 把 Eden + From Survivor 存活的对象放入 To Survivor 区
- 清空 Eden 和 From Survivor 分区
- From Survivor 和 To Survivor 分区交换
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达15(默认配置是15)时,升级为老生代。大对象也会直接进入老生代。
老年代当空间占用到达某个值之后就会触发Full GC,一般使用标记–整理算法。
2.8.6 一次完整的GC流程
新创建的对象一般会被分配在新生代中,当某一时刻,我们创建一个对象将Eden区挤满了,就会产生Minor GC
在正式MinGC之前,JVM先检查新生代中的对象,是比老年区的剩余空间大还是小
- 老年区的剩余空间 > 新生代中的对象,直接Minor GC,就算GC完后幸存区不够放,那老年区也可以放
- 老年区的剩余空间 < 新生代中的对象,查看是否启动了 “老年区空间分配担保规则”(查看-XX:-HandlePromotionFailure 参数是否设置了):如果老年代中剩余空间大小是否 > Minor GC后剩余对象的平均大小
- 大于,则进行Minor GC
- 小于,进行Full GC
- Minor GC后也会有三种情况
- Minor GC后的对象足够放到幸存区,GC结束
- Minor GC后的对象不够放到幸存区,但是可以放入老年区,GC结束
- Minor GC后的对象不够放到幸存区,而且老年区也放不下,执行Full GC
- 以上都是GC成功的例子,还有GC失败的
- 在上一次Full GC后,老年区仍然放不下对象,报OOM
- 未开启 “老年区空间分配担保规则”,且一次Full GC的时候仍然放不下,报OOM
- 开启 “老年区空间分配担保规则”,但是担保不通过,且一次Full GC的时候也放不下,报OOM
2.9 JDK8为什么用元空间代替永久代
区别:
-
- **存储位置不同:**永久代在物理上是堆的一部分,和新生代、老年代的位置是连续的,元空间属于本地内存
-
- **存储内容不同:**永久代用来存储类的元数据信息、静态变量和常量池等,元空间存储元数据,静态变量和常量池在堆上
替换的原因:
-
- 字符串存在永久代中,容易出现性能问题和内存溢出
-
- 类和方法的信息比较难确定其大小,因此对于永久代的大小难以指定,太小容易造成永久代溢出,太大容易造成老年代溢出
2.10 ThreadLocal
2.11 JVM调优
调优步骤
1、性能监控:以非强行的方式收集或者查看程序性能数据的活动
GC频繁、cpu load过高、OOM、死锁、内存泄漏、程序响应时间过长
2、性能分析:以强行的方式收集运行性能活动数据,会影响应用的吞吐量和响应性,通常在系统测试或者开发环境下进行
打印GC日志、运用命令行工具(jstack、jmap、jinfo等)、dump出堆文件 使用内存分析工具分析文件
3、性能调优:改善应用响应性或者吞吐量而更改参数、源代码、属性配置的活动
适当增加内存 根据业务背景选择垃圾回收器、增加机器 分散节点压力、合理设置线程池线程数量、使用中间件
3、多线程
synchronized 中的 4 个优化
https://blog.csdn.net/hollis_chuang/article/details/120030258
synchronized和Lock的区别
- synchronized是关键字,Lock是接口
- synchronized是隐式的加锁,Lock是显式的加锁
- synchronized可以作用于方法和代码块上,Lock只能作用于代码块
- synchronized底层采用的是objectMonitor,lock采用的AQS
- synchronized是阻塞式加锁,lock是非阻塞式加锁,支持等待可中断、超时机制
- synchronized在进行加锁解锁时,只有一个同步队列和一个等待队列,lock有一个同步队列,可以有多个等待队列
- synchronized只支持非公平锁,lock支持非公平锁和公平锁
- synchronized使用wait和notify进行等待和唤醒,lock使用condition接口进行等待和唤醒(await和signal)
- lock支持个性化定制, 使用了模板方法模式,可以自行实现lock方法
3.1 简述线程,程序、进程的基本概念。以及他们之间的关系
-
线程与进程相似,是更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
-
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如 CPU ,内存空间,文件,输入输出设备的使用权等等。
-
程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
3.2 线程的基本状态
从操作层面上讲,线程有5种状态:新建、就绪、运行、阻塞、终止
- **新建:**在程序中创建一个线程,
new Thread()
,这个时候就是新建状态,线程以及分配了内存空间和其他资源,但是还没有开始执行 - **就绪:**使用
start()
方法启动线程,进入就绪状态,但是还需要等待分配CPU - **运行:**当就绪状态的线程被调用并且获得处理器资源的时候,线程就进入了运行状态,自动调用
run()
方法 - **阻塞:**正在运行的线程因为一些特殊原因,会被暂时中止执行并让出CPU,例如人为挂起或者执行耗时较长的输入输出动作
- **死亡:**线程调用
stop(),destroy()
或者正常执行结束,线程也就死亡了
从Java层面上讲,线程有6种状态:NEW(新建)状态、RUNNABLE(可执行)状态、BLOCKED(阻塞)状态、WAITING(等待)状态、TIMED_WAITING(限时等待)状态、TERMINATED(终止)状态
3.3 线程池
1. 了解Java的线程池吗?里面有一个ForkJoinPool,有没有用过?说一下
ForkJoinPool概念
ForkJoinPool是JDK1.7以后加入的,体现的是分治的思想,就是将任务拆分求解后再合并,ForkJoinPool在分治的基础上加入了多线程,就是将任务的拆分和合并交给不同的线程完成,提高了运算效率
ForkJoinPool默认会创建与cpu核心数大小相同的线程池
ForkJoinPool使用到的算法(ForkJoinPool与其他普通线程池的区别)
- 分治算法
- work-stealing(工作窃取)算法:线程池内所有线程优先执行自己队列中的任务,执行完成后区其他线程的队列中获取任务
3.3.1 线程池的创建
-
Executors创建线程池
-
newFixedThreadPool:创建一个固定大小的线程池
ExecutorService service= Executors.newFixedThreadPool(5); for (int i=0;i<5;i++){ //给线程池添加任务 threadPool.submit(new Runnable() { @Override public void run() { System.out.println("线程名"+Thread.currentThread().getName()+"在执行任务1"); } }); }
-
newCachedThreadPool:带缓存的线程池,适用于短时间有大量任务的场景,但有可能会占用更多的资源;线程数量随任务量而定
-
newSingleThreadExecuto:创建单个线程的线程池
- 创建单个线程的线程池?为啥不直接创个线程? 优点: 复用线程:不必频繁创建销毁线程 也提供了任务队列和拒绝策略
-
newSingleThreadScheduledExecutor:创建执行定时任务的单个线程的线程池
service.schedule(new Runnable(){...})
-
newScheduledThreadPool:创建执行定时任务的线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(5);
-
newWorkStealingPool:根据当前设备的配置自动生成线程池
-
-
ThreadPoolExecutor——手动创建线程池
- 七大参数
public ThreadPoolExecutor(
int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
int maximumPoolSize, // 线程数的上限
long keepAliveTime, // 非核心线程闲置时超时时长,超过这个时间,多余的线程会被回收。
TimeUnit unit, // 时长单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler) // 拒绝策略
3.3.2 线程池处理流程
- 判断核心线程是否都在工作,如果不是,则创建一个新的线程来执行任务
- 判断阻塞队列是否已满,如果没有,则进入阻塞队列等待
- 判断是否已达到最大线程数,如果没有,则创建一条线程执行任务,如果是则采用拒绝策略
3.3.3 拒绝策略
- **AbortPolicy:**默认拒绝策略,拒绝任务并抛出任务
- **CallerRunsPolicy:**使用调用线程直接运行任务,谁提交的谁执行
- **DiscardPolicy:**直接拒绝任务,不抛出错误
- **DiscardOldestPolicy:**丢弃阻塞队列的最老的任务,并将新的任务加入
3.3.4 创建线程池的方式
ThreadPoolExecutor、ThreadScheduledExecutor、ForkJoinPool
3.3.5 线程池大小分配
- **CPU密集型:**CPU几核,maximumPoolSize就多大 Runtime.getRuntime().availableProcessors()
- **IO密集型:**CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用CPU,maximumPoolSize > 程序中大型任务数 一般为两倍
- **混合型:**将任务分为CPU密集型和IO密集型,然后用不同的线程池去处理
3.4 对Volatile的理解
3.4.1 特性
Volatile是Java虚拟机提供的轻量级同步机制
1、保证可见性:一个线程对某个变量的修改其他变量是可见的
2、不保证原子性:线程A在执行时,不能被打扰,也不能被分割。要么同时成功,要么同时失败
如果不加lock和synchronized,怎么保证原子性? : 用原子类AtomicXXXX,例如AtomicInteger、AtomicBoolean
3、禁止指令重排:程序会按照代码的顺序执行
指令重排:计算机并不是按照自己所写的程序那样去执行的 编译器优化重拍 —> 指令并行也可能会重排 —> 内存系统也会重排 ----> 执行
3.4.2 使用场景
**1. 状态标志:**用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
**2.一次性安全发布:**在缺乏同步的情况下,可能会遇到某个对象引用的更新值 与 该对象状态的旧值同时存在
3.独立观察:
-
定期 “发布” 观察结果供程序内部使用。【例】假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
-
收集程序的统计信息。【例】
public class UserManager {
public volatile String lastUser; //发布的信息
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
**4.“volatile bean” 模式:**很多框架为易变数据的持有者(例如 HttpSession )提供了容器,但是放入这些容器中的对象必须是线程安全的。
5.开销较低的“读-写锁”策略:如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
3.5 ThreadLocal
3.5.1 概念
ThreadLocal的作用是提供线程内的局部变量
总结:
1.线程并发:在多线程并发的场景下使用
2.传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3.线程隔离:每个线程的变量都是独立的,不会相互影响
3.5.2 基本方法
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?
ThreadLocal类中有一个static声明的Map,用于存储每一个线程的变量副本,Key为线程对象,Value为对应线程的变量副本
3.5.3 ThreadLocal造成OOM
如果使用线程池的时候,意味着当前线程不一定会退出,这样的话,如果将一些比较大的对象设置到ThreadLocal中,就可能造成OOM
因为在处理一次业务的时候将一个大对象存放到ThradLocalMap中,处理另一个业务的时候又将一个大对象存放到ThradLocalMap中,但是这个线程是由线程池创建的,不会被销毁,以前存放的大对象可能不会被再次引用,但线程没有关闭,资源就不会释放,从而导致OOM
3.6 ReetrantLock
3.6.1 隐式锁和显式锁
关键字synchronized属于隐式锁,即锁的持有与释放都是隐式的,我们无需干预。
显式锁,即锁的持有和释放都必须由我们手动编写。
ReetrantLock也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,需要注意的加锁多少次,就必须解锁多少次,这样才可以成功释放锁。
ReetrantLock默认是非公平锁,但是可以在构造函数中传入参数,改成公平锁模 public ReentrantLock(boolean fair)
ReetrantLock是基于AQS并发框架实现的。
4、MySQL
4.1 MySQL基础
4.1.1 数据库三大范式是什么
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。
4.2 MySQL数据类型
分类 | 名称说明 |
---|---|
整数类型 | |
tinyInt | 很小的整数(8位二进制) |
smallint | 小的整数(16位二进制) |
mediumint | 中等大小的整数(24位二进制) |
int(integer) | 普通大小的整数(32位二进制) |
小数类型 | |
float | 单精度浮点数 |
double | 双精度浮点数 |
decimal(m,d) | 压缩严格的定点数 |
日期类型 | |
year | YYYY1901~2155 |
time | HH:MM:SS-838:59:59~838:59:59 |
date | YYYYMM-DD1000-01-01~9999-12-3 |
datetime | YYYYMM-DDHH:MM:SS1000-01-0100:00:00~9999-12-3123:59:59 |
timestamp | YYYYMM-DDHH:MM:SS1970010100:00:01UTC~2038-01-1903:14:07UTC |
文本、二进制类型 | |
CHAR(M) | M为0~65535之间的整数 |
VARCHAR(M) | M为0~65535之间的整数 |
TINYBLOB | 允许长度0~255字节 |
BLOB | 允许长度0~65535字节 |
MEDIUMBLOB | 允许长度0~167772150字节 |
LONGBLOB | 允许长度0~4294967295字节 |
TINYTEXT | 允许长度0~255字节 |
TEXT | 允许长度0~65535字节 |
MEDIUMTEXT | 允许长度0~167772150字节 |
LONGTEXT | 允许长度0~4294967295字节 |
VARBINARY(M) | 允许长度0~M个字节的变长字节字符串 |
BINARY(M) | 允许长度0~M个字节的定长字节字符串 |
4.3 引擎
4.3.1 MyISAM与InnoDB区别
- InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语句组成一个事务
- InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败
- InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大
MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的
-
InnoDB表必须有唯一索引(如主键)(用户没有指定的话会自己找/生产一个隐藏列Row_id来充当默认主键),而MyISAM可以没有
-
InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快
-
Innodb不支持全文索引,而MyISAM支持全文索引,在涉及全文索引领域的查询效率上MyISAM速度更快高;PS:5.7以后的InnoDB支持全文索引了
-
InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁
-
MyISAM表格可以被压缩后进行查询操作
-
Innodb存储文件有frm、ibd,而Myisam是frm、MYD、MYI
Innodb:frm是表定义文件,ibd是数据文件
Myisam:frm是表定义文件,myd是数据文件,myi是索引文件
4.3.2 如何选择MyISAM与InnoDB
- 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM
- 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读也有写,请使用InnoDB
- 考虑系统奔溃后,MyISAM恢复起来更困难,能否接受
- MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差
4.4 索引
1.索引在哪些情况下会失效
(1)对索引列进行运算操作,索引会失效
(2)不匹配数据类型,会造成索引失效
(3)前导模糊查询索引会失效
(4)在WHERE中使用OR时,有一个列没有索引,那么其它列的索引将不起作用
(5)数据分布影响,如果MySQL评估使用索引比全表更慢,则不使用索引
(6)where语句中使用了IS NULL或者IS NOT NULL,可能会造成索引失效,看数据量情况(看版本号,有些的is not null一定不走索引)
(7)使用了反向操作或者link操作,该索引将不起作用(存疑)
4.4.1 概念
索引是一个特殊的文件,包含对数据表里所有记录的引用指针
数据库索引是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中的数据,通常使用B树或者B+树实现
通俗的讲,索引就像一本书的目录
4.4.2 使用索引的场景
适合使用索引的场景
- **主键:**主键一般为id等具有唯一标识的字段,需要频繁查找、连接
- 频繁作为查询条件的字段:
- 查询中作为与其他表关联的字段(外键)
- **查询中常作为查询排序条件的字段:**注意:order by字段出现在where条件中才能使用索引,否则索引失效
- **查询中统计、分组字段:**注意:group by和union也要出现在where条件中才能使用索引,否则索引失效
不适合使用索引的场景
- **频繁更新的字段、表:**更新时不仅要更新数据,还要更新索引,带来很多额外的开销
- 很少作为查询条件的字段
- **表的记录不多:**一般数据量达到300w~500w时考虑使用索引
- **数据重复且分布平均:**比如年龄、性别
4.4.3 索引有哪几种类型
- **主键索引:**数据列不允许重复、不为NULL,一个表只能有一个主键
- **唯一索引:**数据列不允许重复、可以为NULL,一个表允许多个列创建唯一索引
- **普通索引:**基本的索引类型,没有唯一性的限制,允许为NULL值
- **组合索引:**多列值组成一个索引,用于组合搜索,效率大于索引合并
- **全文索引:**是目前搜索引擎使用的一种关键技术,对文本的内容进行分词、搜索
4.4.4 创建索引的原则
- **左前缀匹配原则:**组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配
比如a=1 and b=2 and c>3 and d=4如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整
还有一个最左前缀原则(基于组合索引)
最左优先,创建多列索引时,根据业务需求,where子句中使用最频繁的一列放最左边
-
较频繁作为查询条件的字段才去创建索引
-
更新频繁字段不适合创建索引
-
若是不能有效区分数据的列不适合做索引列(如性别,男女未知,多也就三种,区分度实在太低)
-
尽量的扩展索引,不要新建索引。
比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可
-
定义有外键的数据列一定要建立索引
-
对于那些查询中很少涉及的列,重复值比较多的列不要建立索引
-
对于定义为text、image和bit的数据类型的列不要建立索引
4.4.5 创建索引的三种办法
- 使用CREATE INDEX创建
例如对ip_address这一列创建一个长度为16的索引:
CREATE INDEX index_ip_addr ON t_user_action_log (ip_address(16));
- 使用ALTER语句创建
ALTER TABLE t_user_action_log ADD INDEX ip_address_idx (ip_address(16));
- 建表的时候创建索引
CREATE TABLE tableName(
id INT NOT NULL,
columnName columnType,
INDEX [indexName] (columnName(length))
);
4.4.6 删除索引
根据索引名删除普通索引、唯一索引、全文索引:altertable表名dropKEY索引名
如果主键自增长,需要取消自增长在删除
alter table user_index drop KEY name;
alter table user_index drop KEY id_card;
alter table user_index drop KEY information;
4.4.7 创建索引时需要注意什么?
- **非空字段:**应该指定列为NOT NULL
- **取值离散大的字段:**如果存在大量取值相同的字段,在查询时,结果集的数据行可能占了表中数据很大的比例,这时增加索引并不能明显加快检索速度
- **索引字段越小越好:**因为数据库的数据存储是以页为单位的,一页存储的数据越多,一次I/O读取到的数据也就越多,效率也就越高
4.4.8 百万级别或以上的数据如何删除
- 先删除索引
- 删除无用的数据
- 删除完成后重新创建索引
4.4.9 前缀索引
**语法:**index(field(10)) 使用字段的前10个字符建立索引,默认是使用字段的全部内容建立索引
**前提:**前缀的标识度较高(比如密码,前10位往往都不相同)
4.4.10 Hash索引和B+树索引有什么区别或者说优劣呢?
hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询得到实际数据
B+树索引底层是多路平衡查找树,每次查询从根结点出发,查找到叶子节点就可以得到键值,然后根据查询判断是否需要回表查询数据
不同:
- 一般情况下,hash进行等值查询更快,但是无法进行范围查询,因为hash索引经过hash函数建立索引后,索引的顺序与原顺序无法保持一致,不能支持范围查询,但是B+树天然就支持(左节点小于父节点,右节点大于父节点)
- hash索引不支持使用索引进行排序(原因如上)
- hash索引不支持模糊查询以及多列索引的最左前缀匹配,
- hash索引任何适合都避免不了回表查询,而B+树在符合某些条件(聚簇索引、覆盖索引等)的时候可以只通过索引完成查询
- hash索引在等值查询上较快,但不稳定。因为如果某个键值大量重复,就会产生hash冲突,此时效率会较低;B+树都是从根节点到叶子节点,且树的高度较低
4.4.11 为什么使用B+树而不使用B树
-
B树的非叶子节点和叶子节点都保存了索引和数据,B+树只有叶子节点保存了数据,所以B+树的遍历效率和增删效率更高
-
B树只支持随机检索,B+树支持随机检索和顺序检索,还支持范围查询
-
一个节点由一页存放的,B+树内部节点不存储数据,只存储索引,所以B+树的空间利用率更高,可以减少I/O次数,磁盘读写代价更低
-
B+树查询效率更稳定,B树搜索可能在非叶子节点结束,越靠近根节点查找时间越短,其性能等价于在关键字全集中做一次二分查找;而在B+树中,顺序查找比较明显,随机索引时,任何一个关键字都必须从根节点走到叶子节点,所有关键字的查找路径长度相同,导致每个关键字查找效率差不多
4.4.12 什么是聚簇索引?何时使用聚簇索引与非聚簇索引
**聚簇索引:**将数据与索引放到了一块,找到索引也就找到了数据
**非聚簇索引:**将数据与索引分开存储,索引的叶子节点指向数据的对应行
何时使用聚集索引或非聚集索引
动作描述 | 使用聚集索引 | 使用非聚集索引 |
---|---|---|
外键列、主键列 | 应 | 应 |
列经常被分组排序(order by) | 应 | 应 |
返回某范围内的数据 | 应 | 不应 |
小数目的不同值 | 应 | 不应 |
大数目的不同值 | 不应 | 应 |
频繁更新的列 | 不应 | 应 |
频繁修改索引列 | 不应 | 应 |
一个或极少不同值 | 不应 | 不应 |
非聚簇索引一定会回表查询吗
不一定,如果所要求的查询字段全部命中了索引,就不需要再回表查询
4.4.13 覆盖索引和回表查询
4.5 事务
4.5.1 事务四大特性
- **原子性:**事务是最小的执行单位,不允许分割
- **一致性:**执行事务前后,数据保持一致(比如转账,A少了100,那B就多100)
- **隔离性:**并发访问数据库时,一个用户的事务不被其他用户干扰,各并发事务之间数据库是独立的
- **持久性:**一个事务提交后,它对数据库的改变是永久的
4.5.2 事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | ✔ | ✔ | ✔ |
READ-COMMITTED | ❌ | ✔ | ✔ |
REPEATABLE-READ(MySQL默认) | ❌ | ❌ | ✔ |
SERIALIZABLE | ❌ | ❌ | ❌ |
4.5.3 并发事务带来的问题(脏读、不可重复读、幻读)
-
**脏读:**一个事务读到了另一个事务暂未commit的数据
- A、B事务并行,B事务修改a=3,暂未commit,A事务读到了a=3
-
**不可重复读:**一个事务读到了另一个事务已提交的数据
- A、B事务并行,B事务修改a=3,并且commit,A事务在commit前读到了a=3
-
**幻读:**一个事务在同样的查询条件下,两次查询的数据行数不同
- A、B事务并行,B事务insert了一条数据并且commit,A事务在commit前读到这条新增的数据
4.6 锁
4.6.1 隔离级别与锁的关系
- 在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会和被修改数据的排他锁冲突
- 在Read Committed级别下,读取数据需要加共享锁,在语句执行完后释放共享锁
- 在Repeatable Read级别下,读取数据需要加共享锁,在事务提交后释放共享锁
- 在SERIALIZABLE级别下,会锁定整个范围的键,并一直持有锁,直到事务提交
4.6.2 锁的分类
按锁的粒度来分
- **行级锁:**锁定粒度最细,只对当前操作的行加锁,分为共享锁和排他锁
- 开销大、加锁慢、会产生死锁; 锁定粒度最小,发生锁冲突的概率最低,并发度最高
- **表级锁:**锁定粒度最高,对当前操作的整张表加锁,分为共享读锁(共享锁)和表独占写锁(排他锁)
- 开销小、加锁快,不会产生死锁; 锁定粒度最大,发生所冲突的概率最高,并发度最低
- **页级锁:**锁定粒度居中,一次锁定相邻的一组记录
- 开销居中、加锁时间居中,会出现死锁; 锁定粒度居中,并发度居中
按锁的类别分
**共享锁:**读锁,当用户读取数据时,对数据加上共享锁,共享锁可以同时加上多个
**排他锁:**写锁,当用户写入数据时,对数据加上排他锁,排他锁只可以加一个,他和其他的排他锁、共享锁都互斥
4.6.3 InnoDB的行锁
怎么实现的?
基于索引完成行锁
如果不是通过索引键,那么将完成表锁
举例:
1、id为主键,name为普通字段
2、使用主键id进行索引,其余行仍可以操作
如果使用name进行索引,则将完成表锁,其余行不可以操作
InnoDB行锁的三种算法
- Record lock:单个行记录上的锁
- Gap lock:间隙锁,锁定一个范围,不包括记录本身
- Next-key lock:record+gap 锁定一个范围,包含记录本身
4.6.4 数据库的乐观锁和悲观锁是什么?怎么实现的?
数据库管理系统中的并发控制的任务是确保在多个事务操作同一数据时不破坏事务和数据库的一致性,乐观锁和悲观锁是并发控制的主要技术手段
- 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询玩数据的时候就把事务锁起来,直到提交事务
- 实现方法:使用数据库中的锁机制
- 乐观锁:假定不会发生并发冲突,只在提交操作时检查是否违反数据的完整性,在修改数据的时候把事务锁起来
- 实现方法:版本号机制或者CAS算法
4.7 视图
4.7.1 什么是视图,为什么要使用视图
**概念:**视图是一种虚拟表,物理上是不存在的,其内容与真实的表一样,行和列数据来自于定义视图的查询所引用的基本表
作用:
1)简单:使用视图的用户完全不需要关心后面对应的表的结构、关联条件和筛选条件,对用户来说已经是过滤好的复合条件的结果集。
2)安全:使用视图的用户只能访问他们被允许查询的结果集
3)数据独立:一旦视图的结构确定了,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响。
4.7.2 视图的优缺点
优点:
- 查询简单:简化用户的操作
- 数据安全性:能够对机密的数据提供安全保护
- 逻辑数据独立性:视图对重构数据库提供了一定程度的逻辑独立性
缺点:
4.8 优化
1. 慢日志查询
记录了所有执行时间超过了指定参数long_query_time(默认10秒)的所有SQL语句的日志
MySQL默认没有开启,需要在MySQL配置文件(/etc/my.cnf)中配置:
查看慢日志查询开启状态
配置文件中开启慢查询,修改long_query_time
慢日志查询文件
2. show profiles
通过hava_profiling参数查看当前MySQL是否支持profile操作
默认是关闭的,需要开启
查看每条SQL执行的时间
查看cpu的耗时情况
3. explain执行计划
4.9、MVCC
MVCC基于隐藏字段、Read View、undo log实现
下·
5、设计模式
5.1 设计模式的目的
- 代码复用性
- 可读性(便于其他程序员阅读和理解)
- 可扩展性((当需要增加新功能时,非常方便)
- 可靠性(当增加新功能后,对原来的功能没有影响)
- 是程序呈现高内聚、低耦合的特性
5.2 设计模式七大原则
**1、开闭原则:**对扩展开放,对修改关闭(程序在需要拓展的时候,不能去修改原有的代码,实现一个热插拔的效果)
**2、里氏替换原则:**引用基类的地方必须能透明的使用子类对象
- 原来:B类继承A类,并重写fun()方法
- 现在:把fun()方法提出来到一个基类,A、B去继承这个基类
**3、依赖倒转原则:**针对于接口编程,依赖于抽象,不依赖于具体
-
原来:Person类中有多个sned()的方法,Email、微信、QQ
-
现在:有一个IReceive接口 其中有send()方法,Email、微信、QQ实现接口并实现send()方法,Person直接调用接口
**4、接口隔离原则:**客户端不应该依赖它不需要的接口
----》改进
**5、迪米特法则:**如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用
**6、合成复用原则:**合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
**7、单一职责原则:**类的职责要单一,不能将太多的职责放在一个类中
5.3 设计模式分类
5.4 创建型模式
5.4.1 单例模式
饿汉式(静态常量)(重点!!!)
class Singleton {
private Singleton() {}
private static final Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
饿汉式(静态代码块)
懒汉式(线程不安全)
懒汉式(线程安全,同步方法)
懒汉式(线程安全,同步代码块)(实际上没有用,a进去初始化以后,b不知道已经初始化了,b仍然会进去初始化)
懒汉式(双重检查)(重点!!!)
class Singleton {
private Singleton() {}
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类
class Singleton {
private Singleton() {}
private static class SingletonInstance {
private static final Singleton instance = new Singleton();
}
public static synchronized Singleton getInstance() {
return SingletonInstance.instance;
}
}
枚举
enum Singleton {
INSTANCE;
public void sayHello(){
System.out.println("hello");
}
}
JDK中单例模式的例子
java.lang.Runtime
5.4.2 工厂模式
简单工厂模式、工厂方法模式、抽象工厂模式
简单工厂模式
引入例子
1、抽象类披萨类Pizza
2、chess披萨和greek披萨继承Pizza类
3、Order类用if判断传进来的是什么披萨,然后输出
问题:如果增加一个pepper披萨,那其他orderPizza也会做改变
使用简单工厂模式改进上面的披萨订购
创建一个SimpleFactory类来专门给orderPizza披萨
静态工厂类
工厂方法模式
应用案例
大工厂分为小工厂
抽象工厂模式
JDK中工厂模式----Calendar类
5.4.3 原型模式
场景:假如想创建几个同样的对象
原始的方法就是new,并且一个个的赋值,这样效率很低,接下来用原型模式去改进
第一步改进:实现Cloneable接口,重写clone()方法
5.4.4 建造者模式
场景,建一个房子需要:打地基、砌墙、封顶
解决方案:将产品和产品建造过程进行解耦==》建造者模式
在JDK StringBuilder用到了建造者模式
5.5 结构型模式
5.5.1 适配器模式
类适配器
对象适配器
和类适配器比,就是adapter改变了,从继承变成了聚合
接口适配器
5.5.2 桥接模式
JDBC里面用到了
5.5.3 装饰者模式
5.5.4 组合模式
代理模式
静态代理
JDK动态代理
CGlib代理
6、Spring&MVC&Boot
6.1 Spring Boot框架和传统Spring框架相比有哪些优势?
- 遵循"习惯优于配置"原则,springboot只需要很少的配置
- 可以完全不使用xml文件配置,只使用自动配置和JAVA Config即可
- 内嵌的Tomcat服务器,不需要额外配置服务器
- 提供的starters 简化构建配置,减少了依赖冲突的问题
- 项目可以快速构建,另外还可以更加简单的配置整合第三方框架
6.1 Spring和MVC的关系
1、Spring和SpringMVC是父子容器的关系
2、Spring的核心思想就是容器,用来管理Bean的生命周期,而一个项目里面有很多的Bean,并且他们是分上下层的关系的,目前最常用的一个场景就是在一个项目中导入Spring和SpringMVC框架,而他们就是两个容器,Spring是父容器,MVC是子容器,Spring中注册是Bean对MVC是可见的
3、官方文档推荐,就是不同的Bean用不同的容器来管理,SpringMVC主要就是为我们搭建Web应用程序,那么SpringMVC子容器就用来注册web组件的Bean,比如控制器、处理器映射、视图解析器;而Spring是用来注册驱动应用后端的中间层和数据层组件
6.1 SpringBoot自动装配原理
自动装配简单来说就是自动把第三方组件的Bean装载到IOC
容器中,不需要开发人员再去写Bean相关的配置,在SpringBoot中只需要在启动类上加上@SpringBootApplication
注解就可以实现自动装配,而这个注解又是一个复合注解,其中最重要的,实现自动装配的注解是@EnableAutoConfiguration
,自动装配的实现主要依赖三个核心技术
- 1、引入starter,启动依赖组件的时候,这个组件里面必须有
@Configuration
配置类,而在这个配置类里面,我们需要通过@Bean
注解将Bean对象装配到IOC
容器中 - 2、这个配置类是放在第三方的jar包中的,然后通过SrpingBoot中约定优于配置这样的一个理念,去把这个配置类的全路径放在
classpath:/META-INF/spring.factories
文件里面,这样SpringBoot就可以知道第三方jar包里面这个配置类的位置,这个步骤主要是用到了Spring里面的SpringFactoriesLoader来完成的 - 3、SpringBoot拿到所有的第三方jar包里面的配置类以后,再通过Spring的ImportSelector这个接口来实现对这些配置类的动态加载,从而去完成自动装配这样一个动作
6.2 Spring在代码中获取bean的几种方式
方法一:在初始化时保存ApplicationContext对象 :
ApplicationContext ac = new FileSystemXmlApplicationContext("applicationContext.xml");
ac.getBean("userService");
//比如:在application.xml中配置:
<bean id="userService" class="com.cloud.service.impl.UserServiceImpl"></bean>
说明:这样的方式适用于Spring框架的独立应用程序,须要程序通过配置文件初始化Spring。
方法二:通过Spring提供的工具类获取ApplicationContext对象
ApplicationContext ac1 = WebApplicationContextUtils.getRequiredWebApplicationContext(ServletContext sc);
ApplicationContext ac2 = WebApplicationContextUtils.getWebApplicationContext(ServletContext sc);
ac1.getBean("beanId");
ac2.getBean("beanId");
说明:这样的方式适合于採用Spring框架的B/S系统,通过ServletContext对象获取ApplicationContext对象。然后在通过它获取须要的类实例。上面两个工具方式的差别是,前者在获取失败时抛出异常。后者返回null。
方法三:实现接口ApplicationContextAware:(推荐)
/**
* Spring ApplicationContext 工具类
*/
@SuppressWarnings("unchecked")
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext = applicationContext;
}
public static <T> T getBean(String beanName) {
if(applicationContext.containsBean(beanName)){
return (T) applicationContext.getBean(beanName);
}else{
return null;
}
}
public static <T> Map<String, T> getBeansOfType(Class<T> baseType){
return applicationContext.getBeansOfType(baseType);
}
}
说明:实现该接口的setApplicationContext(ApplicationContext context)方法,并保存ApplicationContext 对象。Spring初始化时,扫描到该类,就会通过该方法将ApplicationContext对象注入。然后在代码中就可以获取spring容器bean了。例如:
LoadExploreTree bean = SpringUtils.getBean(“loadExploreTree”);
方法四:继承自抽象类ApplicationObjectSupport
@Service
public class SpringContextHelper2 extends ApplicationObjectSupport {
//提供一个接口,获取容器中的Bean实例,根据名称获取
public Object getBean(String beanName){
return getApplicationContext().getBean(beanName);
}
}
继承类的方式,是调用父类的getApplicationContext()方法,获取Spring容器对象。
方法五:继承自抽象类WebApplicationObjectSupport
说明:类似上面方法。调用getWebApplicationContext()获取WebApplicationContext
6.3 SpringMVC执行流程
1、客户端发送请求直接到DispatcherServlet
2、DispatcherServlet对请求URL进行解析,判断请求URL对应的映射
- 不存在
- 判断是否配置了默认的servlet(mvc:default-servlet-handler)
- 没有配置,控制台报找不到映射,客户端404
- 有配置,访问目标资源,找不到客户端也会404
- 判断是否配置了默认的servlet(mvc:default-servlet-handler)
- 存在则继续执行
3、根据该URL,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回
4、DispatcherServlet根据获得的Handler,选择以一个适合的HandlerAdapter
5、执行拦截器的preHandler()方法
6、提取Request中的模型数据,填充Handler入参,开始执行Handler(也就是Controller)方法,处理业务逻辑。在填充Handler入参的过程中,根据配置,Spring将做一些额外的工作
- HttpMessageConveter:将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的相应信息
- 数据转换:将请求消息进行数据转换(如String转换为Integer、Double等)
- 数据格式化:对请求消息进行数据格式化(如将字符串转换成格式化数字或者格式化日期等)
- 数据验证:验证数据的有效性(长度、格式等),验证结果存储到BindingResult或者Error中
7、Handler执行完成后,向DispatcherServlet返回一个ModelAndView对象
8、执行拦截器的postHandler()方法
9、根据返回的ModelAndView对象选择一个合适的ViewResolver进行视图解析,根据Model和View进行视图渲染
10、渲染视图完毕执行拦截器的afterCompletion()方法
11、将渲染结果返回给客户端
6.4 bean的生命周期
-
Bean 容器找到配置文件中Bean对象的定义并利用反射创建一个Bean实例。
-
填充对象属性。
-
查看Bean是否实现了一系列Aware接口,比如
BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
,调用相应的set方法 -
如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessBeforeInitialization()
方法 -
如果 Bean 实现了
InitializingBean
接口,执行afterPropertiesSet()
方法。 -
如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
-
如果有和加载这个 Bean 的 Spring 容器相关的
BeanPostProcessor
对象,执行postProcessAfterInitialization()
方法 -
当要销毁 Bean 的时候,如果 Bean 实现了
DisposableBean
接口,执行destroy()
方法。 -
如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
6.5 BeanFactory和FactoryBean有什么区别
- BeanFactory是个Factory,也就是IOC容器或对象工厂。FactoryBean是个Bean。但这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean。
- BeanFactory和FactoryBean都可以用来创建对象,只不过创建的流程和方式不同
- 当使用BeanFactory的时候,必须要严格的遵守bean的生命周期,比较复杂
- 而FactoryBean是用户可以自定义bean对象的创建流程,不需要按照bean的生命周期来创建,在此接口中包含了三个方法
- isSingleton:判断是否是单例对象
- getObjectType:获取对象的类型
- getObject:在此方法中可以自己创建对象,使用new的方式或者使用代理的方式都可以
1、 BeanFactory
BeanFactory定义了IOC容器的最基本形式,并提供了IOC容器应遵守的最基本的接口。在Spring中,BeanFactory只是个接口,Spring容器给出了很多种实现,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,都是附加了某种功能的实现。
使用场景
- 从Ioc容器中获取Bean(byName or byType)
- 检索Ioc容器中是否包含指定的Bean
- 判断Bean是否为单例
2、FactoryBean
一般情况下,Spring通过反射机制利用的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。
使用场景
- FactoryBean在Spring中最为典型的一个应用就是用来创建AOP的代理对象。
- 我们知道AOP实际上是Spring在运行时创建了一个代理对象,也就是说这个对象,是我们在运行时创建的,而不是一开始就定义好的,这很符合工厂方法模式。更形象地说,AOP代理对象通过Java的反射机制,在运行时创建了一个代理对象,在代理对象的目标方法中根据业务要求织入了相应的方法。这个对象在Spring中就是——ProxyFactoryBean。
所以,FactoryBean为我们实例化Bean提供了一个更为灵活的方式,我们可以通过FactoryBean创建出更为复杂的Bean实例。
6.6 Spring是如何解决循环依赖的
三级缓存、提前暴露对象、aop
总
首先循环依赖就是,比如A依赖B,B依赖A
分
先说明bean的创建过程:实例化,初始化(填充属性)
1、先创建A对象,实例化A对象,此时A对象中的b属性为空,填充属性b
2、从容器中查找B对象,如果找到了,直接赋值不存在循环依赖问题(不通),找不到直接创建B对象
3、实例化B对象,此时B对象中的a属性为空,填充属性a
4、从容器中查找A对象,找不到,直接创建
这样就形成了闭环
但是,其实现在是存在A对象的,只不过此时的A对象不是一个完整的状态,只完成了实例化但是未完成初始化,我们可以提前暴露某个不完整对象的引用,所以解决问题的核心在于实例化和初始化分开操作,这也是解决循环依赖问题的关键
当所有的对象都完成实例化和初始化操作之后,还要把完整对象放到容器中,此时在容器中存在对象的两种状态,完成实例化但未完成初始化(半成品),完整状态(成品),因为都在容器中,所以要使用不同的map结构来进行存储,此时就有了一级缓存(singletonObjects)和二级缓存(earlySingletonObjects)。一级缓存中放的是成品,二级缓存中放的是半成品。
其实二级缓存已经能解决循环依赖的问题了,但是由于aop动态代理的关系,所以需要三级缓存
三级缓存简单来说就是,因为容器中的对象都是单例的,根据名称就只能得到一个对象,在程序运行过程中,我们也无法确认什么时候会调用当前对象,所以必须保证容器中任何时候都只有一个对象供外部调用,所以在三级缓存中用lambda表达式完成了代理对象替换非代理对象的工作
6.7 谈谈对IOC的理解,原理和实现
总
**控制反转:**理论思想,把对象交给spring进行管理,其中也用到了依赖注入,就是把对应的属性值注入到具体对象中,比如@Autowired
容器:使用map结构来存储对象,在spring中一般存储在三级缓存(解决循环依赖)
,用的比较多的是singletonObject(还有没有其他map结构)
存放完整的bean对象,整个bean的生命周期,从创建到使用到销毁的过程全部都是由容器来管理
分
1、一般聊IOC容器的时候要涉及到容器的创建(beanFactory,DefaultListablBeanFactory),向bean工厂中设置一些参数(BeanPostProcessor,Aware接口的子类)等等属性
2、加载解析bean对象,准备要创建的bean对象的定义对象BeanDefinition(涉及到xml或者注解的解析过程)
3、beanFactoryPostProcessor的处理,此处是给开发人员扩展点,可以自定义一些东西,
4、BeanPostProcessor的注册功能
5、通过反射的方式奖BeanDefinition对象实例化成具体的bena对象
6、bean对象的初始化过程(填充属性,调用aware子类的方法、BeanPostProcessor()前置处理方法,init-method(),BeanPostProcessor()后置处理方法)
7、生成完整的bean对象,通过getBean方法可以直接获取
8、销毁过程
如果上面的记不太清,就说:
具体的细节我记不太清了,但是 spring中的bean都是通过反射的方式生成的,同时其中包含了很多的扩展点,比如最常用的对BeanFactorye的扩展,对bean的扩展(对占位符的处理),除此之外,ioc中最核心的也就是填充具体bean的属性和生命周期(背一下)。
6.8 谈一下IOC的底层原理
1、先通过createBeanFactor创建出一个Bean工厂( DefaultListableBeanFactory)
2、然后开始循环创建对象,因为容器中的bean默认都是单例的,所以优先通过 getBean, doGetBean从容器中查找,找不到的话
3、通过 createBean、doCreateBean方法,以反射的方式创建对象,(一般情况下使用的是无参的构造方法( getDeclaredConstructor,newInstance))
4、然后进行对象的属性填充 populateBean
5、最后进行其他的初始化操作( initializingBean)
6.9 Spring AOP底层原理
总:aop概念、应用场景、动态代理
分
bean的创建过程中有一个步骤可以对bean进行扩展实现,aop本身就是一个扩展功能,所以在BeanPostProcessor的后置处理方法中来进行实现
1、用来减少重复的代码,降低耦合度,以后也方便拓展和维护,比如日志管理、权限控制,这些跟业务逻辑无关的代码,就可以抽取出来形成增强,然后通过定义切点的方式,就可以实现无侵入的方法级别的增强,切点和增强也统称为切面
2、其次,他是基于动态代理的,通过jdk或者cglib的方式来生成代理对象,如果实现了接口,那么就是JDK,如果没有接口,JDK无法处理,就是用cglib
Spring AOP也集成了AspectJ
6.10 Spring事务
传播特性
7、MyBatis
7.1 mybatis如何实现分页,interceptor在请求之前还是之后?为什么
拦截器分页
7.2 mybatis中的一级缓存和二级缓存
8、Redis
8.1 Redis为什么这么快
1、Redis是基于内存存储,读写速度快,没有磁盘IO上的开销
2、单线程实现(Redis6.0以前)避免了多个线程之间线程切换和锁资源竞争的开销
3、Redis采用IO多路复用技术,单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络IO上浪费过多的时间
4、高效的数据结构,Redis每种数据结构都做了底层优化,目的就是为了追求更快的速度
8.2 Redis主从结构
全量同步
增量同步
8.3 哨兵
问题引入:slave宕机了可以找master恢复数据,那如果master宕机了呢?
解决:选一个slave当master,之前的master恢复过后作为slave
8.4 底层数据结构
9、计算机网络
9.1 三次握手
过程
-
第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;(服务端得知:客户端发送正常、服务端接收正常)
-
第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;(客户端得知:客户端发送/接收正常,服务端发送正常)
-
第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。(服务端得知:客户端发送/接收正常,服务端发送/接收正常)
为什么要三次握手
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
9.2 四次挥手
过程
-
第一次挥手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number=x,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
-
第二次挥手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number=x+1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
-
第三次挥手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
-
第四次挥手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
为什么要四次分手
TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接