基础
- Java 语言有哪些特点
-
简单易学;
-
面向对象(封装,继承,多态);
-
平台无关性( Java 虚拟机实现平台无关性);
-
可靠性;
-
安全性;
-
支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系 统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程 支持);
-
支持网络编程并且很方便
-
编译与解释并存;
-
面向对象和面向过程的区别
面向过程 :
优点: 性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗 资源;比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发,性能是最重要的因素。
缺点: 没有面向对象易维护、易复用、易扩展
面向对象 :
优点: 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特 性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
缺点: 性能比面向过程低
-
字符型常量和字符串常量的区别
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的 若干个字符
含义上: 字符常量相当于一个整形值( ASCII 值),可以参加表达式,运算字符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小:字符常量只占 2 个字节(char 在 Java 中占两个字节), 字符串常量占若干个字节
-
构造器 Constructor 是否可被 override
父类的私有属性和构造方法并不能被继承,所以 Constructor 也就不能被 override(重写),但是可以 overload(重载)
-
重载和重写的区别
**重载:** 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序 不同,方法返回值和访问修饰符可以不同,发生在编译时。 **重写:** 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父 类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
-
为什么要使用封装?
隐藏对象内部的复杂性,确保数据安全 , 防止误操作,只对外公开简单的接口,便于外界使用
-
java为什么类不支持多继承,接口可以?
Java中的接口支持多继承(接口与接口之间为继承关系),因为接口不提供具体实现方式,只是一种规范,所以支持;Java的类不支持多继承的原因是Java是强类型语言,多继承会导致调用的不确定性,编译器无法确定要调用哪个类方法,甚至在调用哪个类方法时也无法确定优先级。
-
接口和抽象类的区别是什么
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始 接口方法可以有默认实现),抽象类可以有非抽象的方法
- 接口中不能包含变量,所有的变量都是常量,默认为public static final类型的,而抽象类中则不一定
- 接口可以多继承,抽象类只能单一集成
- 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
- 接口是没有构造器的,不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
- 成员变量与局部变量的区别有那些
- 从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数,成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是成员变量和局部变量都能被 final 所修饰;
- 从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存
- 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
- 成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外被 final 修饰的成员变量也必须显示地赋值);而局部变量则不会自动赋值。
- 对象的相等与指向他们的引用相等,两者有什么不同?
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。
- Java 中的异常处理
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。
Throwable: 有两个重要的子类:Exception(异常) 和 Error(错 误) ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大 多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟 机)出现的问题。
**Exception(异常)😗*是程序本身可以处理的异常。
注:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
-
在以下 4 种特殊情况下,finally 块不会被执行
- 在 finally 语句块中发生了异常。、
- 在前面的代码中用了 System.exit()退出程序。
- 程序所在的线程死亡。
- 关闭 CPU。
-
获取用键盘输入常用的的两种方法
方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法 2:通过 BufferedReader
BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
一、集合框架
大纲:
Java集合可以分为Collection和Map两种体系:
Collection接口:
List:元素有序,可重复的集合
ArrayList: 底层数组实现,有利于随机访问get
LinkedList:底层是链表,有利于频繁的插入、删除操作(ArrayList删除和插入要扩容,浪费性能)
Vector:古老的实现类,线程安全的,性能较差,效率低于ArrayList,不建议使用
Set:元素无序、不可重复的集合
LinkedHashSet:使用链表维护了一个添加进集合中的顺序。
注:要求添加进Set元素所在的类,一定要重写equals()和hashCode()方法
List
ArrayList
-
List集合实现类ArrayList底层采用数组实现,学习ArrayList源码前先了解数组的扩容机制
数组的两种扩容方式
Object [] objects = {1, 2, 3, 4 ,5, 6}; /** * 数组扩容,用Arrays.copyOf方法 * 底层用的也是System.arraycopy * System.arraycopy(原数组, 0, 需要扩容的新数组, 0, Math.min(original.length,newLength)); * 参数Math.min():返回最小数,即需要扩容的内容数量 */ Object[] newObjects = Arrays.copyOf(objects, 3); System.out.println(Arrays.toString(newObjects));
/** * System.arraycopy:底层数组copy技术,该方法用native修饰 * 参数:src:原数组 * srcPos:原数组的起始位置;即从原数组的起始位置开始复制 * dest:目标数组 * destPos:目标数组起始位置;即从目标组的起始位置往里复制 * length:复制长度;即需要从原数组复制到目标数组的个数 */ int[] fun = {0, 1, 2, 3, 4, 5, 6}; System.arraycopy(fun, 3, fun, 0, 4); System.out.println(Arrays.toString(fun));
-
JDK1.7以后,ArrayList数组默认初始化大小放在add方法内,无参构造方法默认为一个空数组,有参构造方法可以指定其数组长度
//初始化时默认为空数组 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //elementData:ArrayList存放数据的数组 transient Object[] elementData; //无参构造方法默认为一个空数组 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } //指定长度的有参构造 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); } }
-
ArrayList数组默认初始化大小为10
private static final int DEFAULT_CAPACITY = 10;
-
ArrayList中可以存放null
//ArrayList中需要存放的元素为泛型,故可以存放null public boolean add(E e) { ensureCapacityInternal(size + 1);//扩容方法 elementData[size++] = e; return true; }
-
size属性
//ArrayList中元素的具体个数,数组长度不等于元素个数 //扩容需要用到该属性,当size == elementData.length时,触发扩容 private int size;
-
ArrayList中elementData为什么被transient修饰?
一、transient修饰符基本概念:
transient用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient修饰的变量的值是不包括在序行化的表示中的。
transient用于修饰不需要序列化的字段,如果一个引用类型被transient修饰,则其反序列化的值为null,如果一个基本类型被transient修饰,则其反序列化的值为0,如果字段的引用类型是不可序列化的类,则也应该使用transient修饰,它在序列化时会被直接跳过。
二、elementData用transient修饰:
ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData。
三、为什么不直接用elementData来序列化,而采用上述的方式来实现序列化呢?
原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上述的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
-
序列化性能比较
-
ArrayList中modCount的作用
-
ArrayList底层每次数组扩容为原数组长度的1.5倍
当扩容时,原长度为1,最小扩容量则为2
//参数为最小扩容量 private void ensureCapacityInternal(int minCapacity) { if(size == elementData.length){ int oldCapacity = elementData.length; //当size等于数组的长度时,需要扩容 // 新扩容容量为原来大小的1.5倍,此处采用位运算符 int newCapacity = oldCapacity + (oldCapacity >> 1); //当长度为1时,新容量通过位运算的结果还是为1,当新容量小于最小扩容量时,用最小扩容量 if(newCapacity - minCapacity < 0){ newCapacity = minCapacity; } elementData = Arrays.copyOf(elementData, newCapacit); } }
LinkedList
-
概述
LinkedList:是双向链表实现的List,有序可重复的集合。
基于链表(存在上下节点),所以保证了有序性
可重复:链表中每个节点内容(Data)都是一个Object对象,所以可重复
双向链表:元素中有上一个节点(preNode)与下一个节点(NextNode),节点内容(Data)
LinkedList:是非线程安全的
LinkedList:元素允许为null,允许重复元素
LinkedList:是基于链表实现的,因此插入删除效率高(只需要改变前后两个节点指针指向即可)
LinkedList:查找效率低,默认从头结点遍历查找,不能根据索引随机访问,时间复杂度为O(n)
LinkedList:是基于链表实现的,因此不存在容量不足的问题,所以没有扩容的方法
-
基础属性
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { //LinkedList的长度,具体存放的元素个数 transient int size = 0; //头结点,设计头结点得目的是为了查询,从first开始遍历 transient Node<E> first; //尾结点,作为添加开始的节点,新元素在last后添加 transient Node<E> last; //如果链表中只有一个节点(Node),那么first,last都指向Node }
-
LinkedList中存放元素为Node对象
private static class Node<E> { //元素,集合中具体的值 E item; //指向后一个元素的指针 Node<E> next; //指向前一个元素的指针 Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
-
add方法
public boolean add(E e) { linkLast(e); return true; } void linkLast(E e) { //将尾结点赋值给临时节点l final Node<E> l = last; //根据需要添加的元素创建一个新的node节点 final Node<E> newNode = new Node<>(l, e, null); //将新node节点置为尾结点 last = newNode; //如果之前的尾结点为空,则表示e为添加的第一个元素,则首节点也指向newNode if (l == null){ first = newNode; } else { //如果之前的尾结点不为空,则将l的下一个节点next指向newNode l.next = newNode; } //元素个数自增1 size++; //修改统计自增1 modCount++; }
-
get方法
public E get(int index) { //验证下标的方法 checkElementIndex(index); return node(index).item; } //根据下标循环遍历,此方法根据长度进行了折半查找,故时间复杂度为O(n/2) Node<E> node(int index) { //将元素分为两半查找,如果index在前一半中,则从0顺序遍历 if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { //如果index在后一半中,则从最后一个元素开始,反向遍历 Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
-
remove方法
public E remove(int index) { //验证下标方法 checkElementIndex(index); //根据node方法,用下标index找到该node对象,用unlink方法删除 return unlink(node(index)); } //删除节点时,需要改变上一个节点的next,以及下一个节点的prev //例如删除B元素 // 删除前 A → B → C 删除后 A → c // A ← B ← C 删除后 A ← c E unlink(Node<E> x) { //item为当前node节点的元素 final E element = x.item; //next为当前node节点指向的下一个节点 final Node<E> next = x.next; //prev为当前node节点指向的上一个节点 final Node<E> prev = x.prev; //如果上一个节点为空,表示当前删除的是首节点 //当前节点被删除,需要将当前节点的next置位首节点 if (prev == null) { first = next; } else { //如果上一个节点不为空,则将上一个节点的next指向 当前节点x的下一个节点next prev.next = next; //当前删除节点的上节点置位null,当前节点X中的元素一步步置位null,是为了方便对象回收 x.prev = null; } //如果下一个节点为空,表示当前删除的是尾节点 //当前节点被删除,需要将当前节点的上一个节点置位尾节点 if (next == null) { last = prev; } else { //如果下一个节点不为空,则将x的下一个节点next的prev指向 当前节点x的上一个节点prev next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
总结:
一、 三种集合的使用场景
-
Vector很少用,有其他线程安全的List集合
-
如果需要大量的添加和删除则可以选择LinkedList
原因:插入和删除的时候无需移动节点,只需要修改上下节点即可
-
如果需要大量的查询和修改则可以选择ArrayList
原因:底层为数组,查询与修改的时候根据下标来操作
二、使用线程安全的List集合,有什么办法?
1.可以使用Vector
2.自己重写类似于ArrayList的但是线程安全的集合
3.可以使用Collections.synchronizedList(); 将ArrayList变成一个线程安全的集合
4.可以使用java.util.concurrent包下的CopyOnWriteArrayList,它是线程安全的
三、CopyOnWriteArrayList是怎么实现线程安全的
他的设计思想是:读写分离,最终一致,写时复制
缺点:1.底层是数组,删除插入的效率不高,写的时候需要复制,占用内存,浪费空间,
如果集合足够大的时候容易触发GC
2.数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
当执行add或remove操作没完成时,get获取的仍然是旧数组的元素
3.CopyOnWriteArrayList读取时不加锁只是写入和删除时加锁
四、CopyOnWriteArrayList和Collections.synchronizedList区别
CopyOnWriteArrayList 读操作性能较好,写操作性能较差,因为读操作没有加锁
Collections.synchronizedList 写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好。
而读操作因为是采用了synchronized关键字的方式,性能并不如CopyOnWriteArrayList。
CopyOnWriteArrayList采用lock锁,每次写操作时都需要进行数组扩容,
而Collections.synchronizedList用synchronized锁add方法,只有达到阈值才会扩容
故CopyOnWriteArrayList写操作性能差
Map
HashMap
前提:HashMap在jdk1.8中用数组+链表+红黑树来存放数据
数组通过index用来存放map元素key-value,index用map中key的hash值与该数组的长度进行与运算得到
不同的key经过计算得到的index会存在相同,因此就产生hash碰撞,为了解决这一问题,就采用了链表的形式挂载到数组中。即多个map中key值不同,但index相同,该数组下标index下存放一个链表。
当hash碰撞频率越高,链表就越长,查询效率就越低,为了解决这个问题,当链表长度大于阈值(默认为8)时,就将链表转换为红黑树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NrGTamRJ-1631955925717)(C:\Users\admin\Desktop\笔记\img\结构图.png)]
第一部分,基础入门
1.数组的优势/劣势
适合随机访问与修改,查询效率高,但数组须固定长度,且内存分配空间必须是连续的
2.链表的优势/劣势
适合添加与删除,不用指定长度,可散列分配空间,但查询效率低,遍历性能差
3.有没有一种方式整合两种数据结构的优势?
散列表
4.什么是哈希?
核心理论:Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。
这个映射的规则就是对应的Hash算法,而原始数据映射后的二进制串就是哈希值。
5.Hash的特点:
从hash值不可以反向推导出原始的数据
输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值
哈希算法的执行效率高效,长的文本也能快速地计算出哈希值
hash算法的冲突概率小
第二部分,HashMap原理讲解:
1.Node数据结构分析
//hashMap用内部类Node对象来存放数据
static class Node<K,V> implements Map.Entry<K,V> {
//存放map中Key的hash值
final int hash;
//存放map中Key
final K key;
//存放map中value值
V value;
//存放下一个元素的Node对象(单向链表)
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
2.hashMap全局参数
//缺省数组大小,默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//table数组最大的长度
static final int MAXIMUM_CAPACITY = 1 << 30;
//缺省的负载因子,默认为0.75(扩容时用到)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化阈值,当链表的元素个数大于8时,转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树降级为链表的阈值,红黑树内数量<6时,则将红黑树转换成链表
static final int UNTREEIFY_THRESHOLD = 6;
//树化的另一个参数,当数组的长度>=64时,才会进行树化
static final int MIN_TREEIFY_CAPACITY = 64;
//哈希表
transient Node<K,V>[] table;
//当前哈希表中元素个数
transient int size;
//当前hash表结构修改次数,每次元素进行添加或删除时,自增1
transient int modCount;
//扩容阈值,当哈希表中的元素超过阈值时,触发扩容。默认是(数组长度 x loadFactor)
int threshold;
//负载因子
final float loadFactor;
3.构造方法
//hashMap提供了4个构造方法,第4个构造方法不常用省略
//1、无参构造只初始化了负载因子为 0.75,哈希表初始化时用了懒加载,在put时才会初始化
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//2
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//3、 initialCapacity为数组的长度,loadFactor为负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//数组的长度不能超过int的最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//tableSizeFor方法通过位运算,不管指定的长度是多少,其结果只会为16的倍数,
//无参构造threshold为缺省,有参构造时,该参数与数组的长度一致,扩容时用到
//当哈希表中元素个数>threshold时,触发扩容
this.threshold = tableSizeFor(initialCapacity);
}
4.hash算法
//异或:相同则返回0,不同则返回1
//将key的hash值再次进行异或运算,是为了让key的hash值高16位也参与路由寻址运算,
//如果一个hash值低位一样,高位不参与运算,会发生hash碰撞,如果高位参加运算,会使散列更加均匀
static final int hash(Object key) {
int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
5.put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//onlyIfAbsent:如果为true,则不更改现有值,只做新增
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
//tab:当前hashMap的散列表
//p:表示散列表中当前的元素
//n:表示散列表数组的length
//i:散列表中数组的index
Node<K,V>[] tab; Node<K,V> p; int n, i;
//当前散列表懒加载,当table为null时,调用resize方法进行输出,并将table数组的长度给n
if ((tab = table) == null || (n = tab.length) == 0){
n = (tab = resize()).length;
}
//最简单的一种情况,当前插入的元素,根据下标index刚好为null,则直接将node放入散列表中
if ((p = tab[i = (n - 1) & hash]) == null){
tab[i] = newNode(hash, key, value, null);
} else {
//e:临时的node元素
//如果e不为null,表示找到了一个与当前要插入的key-value中key为
//k:临时的key
Node<K,V> e; K k;
//如果当前散列表中,数组已存在的第一个元素刚好与当前要插入的元素key一致,则将p赋值给临时e
//用来进行后续替换value值的操作
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果p是红黑树,则单独处理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果与当前散列表中的头元素key不一致,则往下遍历,依次对比key
else {
//循环从0开始,但实际上是从链表中第二个元素开始
for (int binCount = 0; ; ++binCount) {
//如果链表当前元素的下一个节点是空的,则表示是新增,插在当前链表末尾
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果新增的key-value大于8时,讲该链表转换为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//循环链表的某个元素,key与当前要插入节点的key一致,表示需要替换,循环break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))){
break;
}
//遍历时,没有找到一致的key,将当前遍历的e赋值给p,用来继续往下找next节点
p = e;
}
}
//e不等于null时,表示需要用来替换
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//新增时modCount自增,替换元素不纳入统计,在上面步骤中已经return了
++modCount;
//如果当前散列表中的元素个数大于扩容阈值时,触发扩容
if (++size > threshold){
resize();
}
afterNodeInsertion(evict);
return null;
}
6.扩容方法resize
为什么需要扩容?
为了解决哈希冲突导致的链化,影响查询效率。扩容会用空间换时间,缓解该问题
final Node<K,V>[] resize() {
//oldTab:表示扩容前老的哈希表
Node<K,V>[] oldTab = table;
//oldCap:表示扩容前老的哈希表数组的length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:扩容前老的扩容阈值,触发本次扩容的阈值
int oldThr = threshold;
//newCap:扩容之后新的table数组长度
//newCap:扩容之后新的扩容阈值,用来下次进行扩容的条件
int newCap, newThr = 0;
//oldCap>0表示table已经被初始化,且有元素,这是一次正常的扩容
if (oldCap > 0) {
//扩容之前,table数组大小已经达到最大阈值,则不进行扩容,扩容条件设置为int最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容后,新table数组的大小在最大阈值范围内,且老的数组length>=16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//正常扩容后,新的扩容阈值为原来的1倍
newThr = oldThr << 1; // double threshold
}
//下面的判断条件均为老的table为空进行
//当老的table为空,且老的扩容阈值 > 0 ,表示第一次初始化,初始化长度为老的扩容阈值
//同时表示初始化时,调用的为有参构造方法
else if (oldThr > 0)
newCap = oldThr;
else {
//老的table与老的扩容阈值都为空,则表示无参构造初始化,赋默认值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新的扩容阈值为0,则表示老的table或老的扩容阈值已经达到最大范围
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//新的扩容阈值赋值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//根据新的扩容长度,new一个新的table数组,并将新的newTab赋值table
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//oldTab != null 表示扩容之前,老的table有值,需要将老table的值移到新newTab中
if (oldTab != null) {
//遍历老的table
for (int j = 0; j < oldCap; ++j) {
//e:临时node节点
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//将老table中的数组元素置为null,方便垃圾回收整个老的table
oldTab[j] = null;
//如果老table数组中,该下标j的元素只有一个,从未发生过碰撞,直接将该元素丢到新的table
if (e.next == null)
//重新根据新的数组长度,求下标index,此处为重新计算index的公式
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果e为红黑树,单独处理
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//如果老的table数组中,下标为j的元素为链表,则循环将链表的每个元素丢到新table中
//低链位表:存放在扩容之后的数组的下标位置,与当前数组的下标位置一致
Node<K,V> loHead = null, loTail = null;
//高链位表:存放在扩容之后的数组的下标位置为 当前数组的下标位置 + 扩容之前数组的长度
//假如原来组数的长度为16,且元素index为15,扩容后存放在新table中index为31的位置
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//等于0表示原来的key的哈希值为低位,扩容后index不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);//next为空时,表示已经遍历完
if (loTail != null) {
//将最后一个元素的next节点置为null,表示loTail为最后一个元素
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//将最后一个元素的next节点置为null,表示hiTail为最后一个元素
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
//将扩容后的新table返回
return newTab;
}
7.get方法
public V get(Object key) {
Node<K,V> e;
//通过key计算hash值,在table中遍历查找
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
//tab:引用当前hashMap的散列表
//first:散列表数组中的头元素
//e:临时node节点
//n:table数组的length
//k:临时key
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//表示table数组中的头元素不为空,如果该头元素与查找key一致,则表示找到了
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//头元素key不一致,继续遍历该链表
if ((e = first.next) != null) {
//如果node节点是红黑树,则用二叉树查找法查询
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历时,链表中的某个元素key与当前查找key一致,表示找到了,直接返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//返回null表示table中没有找到该key的元素
return null;
}
8.remove方法
public V remove(Object key) {
Node<K,V> e;
//调用removeNode方法
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab:引用当前hashMap的散列表
//p:当前链表的元素
//n:table数组的length
//index:根据key计算得到的存放与哈希表数组中的下标
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//表示根据index找到了元素,且不为空,赋值给p
//node:表示找到了需要删除的node元素
//e:临时node节点
//k:临时node节点的key
//v:临时node节点的value值
Node<K,V> node = null, e; K k; V v;
//当前找到的node元素p与要删除的元素一致
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果当前node节点不一致,则遍历找next节点
else if ((e = p.next) != null) {
//p的next节点不为空,且p为红黑树,调用红黑树的查找方法
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
//遍历链表时,链表中的某个元素与要删除的key一致,表示找到了
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
//没找到,将临时节点置位p,继续找next节点遍历
p = e;
} while ((e = e.next) != null);
}
}
//node不为空的情况下,表示根据key找到了需要删除的node元素
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//第一种情况:node为红黑树,调用红黑树的删除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//第二种情况:node与p相等,表示数组中的首元素就是要删除的对象,
else if (node == p)
//将node元素的next置位首元素,即只删除首元素,将第二个元素置位首元素
tab[index] = node.next;
else
//第三种情况,将当前要删除的node元素的next,指向node上一个元素的next,node删除
p.next = node.next;
//删除后,修改统计自增1
++modCount;
//删除后,元素个数自减1
--size;
afterNodeRemoval(node);
//将要删除的node元素返回
return node;
}
}
return null;
}
9.replace方法
//replace有两个,一个根剧key替换value,另一个根剧key与老的value替换新的value,此方法为第一种
public V replace(K key, V value) {
Node<K,V> e;
//调用getNode方法,通过key找到node元素,getNode方法与在7.中一致
if ((e = getNode(hash(key), key)) != null) {
//将找到的node元素value值替换成新value值,老的value值返回
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
总结:
一、hashMap是否线程安全?与HashTable之间的区别
HashMap非线程安全,HashTable是线程安全的
二、hashMap是否可以存放key为空的对象?hashMap中put方法如何实现?
可以,因为元素node对象的key是一个泛型,可以为空,也可以是一个自定义对象
put方法的实现,根据当前key计算hash值,再使用hash值获取index位置
//存放map中Key
final K key;
//当key为空时,hash值取0, 用0与数组长度进行逻辑与运算,结果也为0,所以key为空时,存放在数组第一个位置
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//取值 逻辑与运算:二进制数同时为1时 则为1
tab[(n - 1) & hash])
三、hashMap如何减少index下标冲突问题
扩容,用空间来换时间。
四、hashMap负载因子为什么是0.75f,而不是其他的呢?
加载因子越小,扩容阈值就越小,hashMap底层扩容就越频繁,index发生冲突的概率越小
加载因子越大,扩容阈值就越大,hashMap底层扩容会不频繁,index发生冲突的概率越大
index冲突越大,链表越长,即空间利用率越高,查询越慢
index冲突越小,链表不会很长,数组会越来越大,即空间利用率越低,查询很快
因此,必须在空间与时间之间平衡与折中,经过平衡测试,选择了 0.75f
五、hashMap是如何解决hashCode冲突的?index冲突会导致什么问题?
hashCode冲突会导致index冲突,当两者冲突时用链表存储解决,链表越长,查询效率越低。时间复杂度为O(N)。当链表长度超过8时,转换为红黑树来解决查询效率低的问题
六、hashMap中index冲突与hash冲突存在哪些区别?
index冲突:是因为底层做二进制运算产生相同的index,对象不同,index可能会相同
hash冲突:Node元素key不同,但是key经过hashcode运算时可能会产生相同的值,在hashMap中为了确保相同的key,使用equals方法比较
七、jdk7中的hashMap存在哪些问题?
链表过长效率低问题,hashMap线程不安全,在多线程环境下,底层扩容的时候可能会出现死循环。
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
//该行代码容易出现死循环
//当一个线程把扩容的元素放入新的newTable中,新newTable的链表是反转的,
//此时另外一个线程去取newTable[i]容易发生死循环
//例如原来链表有两个元素AB, A的next为B
//但是在新的newTable中A是先插入的,即B的next为A
//多线程下就导致A的next为B B的next为A,while就变成死循环了
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
八、 HashMap和Hashtable的区别
1、两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全。Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步
2、HashMap可以使用null作为key,不过建议还是尽量避免这样使用。HashMap以null作为key时,总是存储在table数组的第一个节点上。而Hashtable则不允许null作为key。
3、HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75。
4、HashMap扩容时是当前容量翻倍即:capacity * 2,Hashtable扩容时是容量翻倍+1 即:capacity * 2 + 1
5、两者计算hash的方法不同:
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模:
HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸:
时间复杂度
O(1):
容量增大也不会影响到查询效率,如ArrayList直接通过下标访问
O(N):
容量增大几倍,耗时也增大几倍,如linkedList,HashMap中链表;每次查找都需要依次遍历。
linkedList在遍历查找时,进行了折中查找,其时间复杂度也可以说是O(N/2)
O(logN):
当数据增大n倍时,耗时增大logN倍,如数据增大256倍时,耗时只增大8倍,是比线性还要低的时间复杂度,二分查找就是O(logN)的算法,如HashMap中的红黑树
红黑树
二、字符串操作
一、Java 中操作字符串都有哪些类?它们之间有什么区别?
-
String、StringBuffer、StringBuilder
-
String : final修饰,是不可变的,所以线程安全,String类的方法都是返回new String。即对String对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象。
//String类是final修饰,底层是用char数组来存储,也是用final修饰,不可变的类都是线程安全的 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { //存放数组 private final char value[]; }
-
StringBuffer:线程安全的,对字符串的操作的方法都加了synchronized,保证线程安全
-
StringBuilder :线程不安全
//StringBuffer与StringBuilder都继承AbstractStringBuilder,底层也是用char数组存储
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
//默认数组长度16
public StringBuilder() {
super(16);
}
//如果调用有参构造,长度则为16 + 字符串长度
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
//StringBuilder与StringBuffer在appen时都调用父类的append方法
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
}
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
public StringBuffer() {
super(16);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
//StringBuffer类中,所有的方法都加了synchronized,所以线程安全
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
//StringBuffer与StringBuilder的父类
abstract class AbstractStringBuilder implements Appendable, CharSequence {
//用char数组来存放数据,没有用final修饰,所以可变
char[] value;
//字符串具体的长度, char数组的长度!=字符串长度
int count;
//char数组的长度调用capacity方法
public int capacity() {
return value.length;
}
//每次调用append时,都产生一个新的char数组,并两次进行数组copy
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//数组扩容,每次复制一个新的数组
ensureCapacityInternal(count + len);
//将要添加的字符串复制到新数组中
str.getChars(0, len, value, count);
count += len;
return this;
}
}
二、StringBuffer与StringBuilder底层扩容机制
abstract class AbstractStringBuilder implements Appendable, CharSequence {
//底层扩容,调用数组copyOf方法
private void ensureCapacityInternal(int minimumCapacity) {
//如果append时,最小长度大于当前数组的长度,出发扩容
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
//扩容后新数组的长度 = 老数组的长度2倍 + 2
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
//不超过int最大范围
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
}
三、string类为什么是final的?
-
主要是为了“效率” 和 “安全性” 的缘故。
1、 由于String类不能被继承,所以就不会没修改,这就避免了因为继承引起的安全隐患
2、若 String允许被继承, 由于它的高度被使用率, 可能会降低程序的性能,所以String被定义成final。
-
String存放数据的char数组value用final修饰,表示该数组引用地址不可变,数组中内容可以通过反射来修改
三、Spring
基础知识
-
什么是 Spring 框架?Spring 框架有哪些主要模块?
Spring 是一种开源轻量级框架,是为了解决企业应用程序开发复杂性而创建的 Spring 框架本身亦是按照设计模式精心打造,这使得我们可以在开发环境中安心的集成 Spring 框
架,不必担心 Spring 是如何在后台进行工作的。 Spring 框架至今已集成了20 多个模块。这些模块主要被分如下图所示的核心容器、数据访问/集
成,、Web、AOP(面向切面编程)、工具、消息和测试模块。 -
Spring容器中Bean默认为单例,通过@Scope方法指定 (bean的作用域)
prototype:多实例,IOC容器启动的时候,并不会去调用方法创建对象,而是每次获取的时候才会调用方法去创建
singleton:单实例,IOC容器容器启动的时候就会调用方法创建对象放入到IOC容器中,以后每次获取直接从容器中拿同一个bean(大Map.get()拿)
request:主要针对web应用,递交一次请求,创建一个对象
session:同一个session创建一个实例
//Scope属性指定多例,缺省默认单例
@Scope("prototype")
@Bean
public Person person(){
return new Person("aaa", 20);
}
- @Lazy 懒加载
主要针对单实例bean,单实例bean默认在容器启动时创建,加上@Lazy注解表示容器启动时不创建对向,仅当第一次获取时才创建初始化
//懒加载机制
@Lazy
@Bean
public Person person(){
return new Person("aaa", 20);
}
- useDefaultFilters = true注解
当在@ComponentScan注解中添加扫包路径时,可以指定过滤信息,过滤掉某些注解,如果用了useDefaultFilters = true属性,表示使用默认的过滤,即默认添加@Component注解信息
@Configuration
//此Filters注解表示扫包时,默认只包含@Controller的注解的bean,但useDefaultFilters = true时,将其他的注解如@Service也包含进去了,因为@Controller与@Service等相关注解都属于@Component子注解
@ComponentScan(value = "com.shadow", includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})},
useDefaultFilters = true
)
public class AppConfig {
@Bean
public Person person(){
return new Person("aaa", 20);
}
}
//Controller也属于Component
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
}
//ClassPathBeanDefinitionScanner类 165行
//源码部分,如果useDefaultFilters为true,则调用registerDefaultFilters方法添加Component信息
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry,
boolean useDefaultFilters,
Environment environment, @Nullable ResourceLoader resourceLoader) {
this.registry = registry;
if (useDefaultFilters) {
registerDefaultFilters();
}
}
protected void registerDefaultFilters() {
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
}
- 什么是控制反转(IOC)?什么是依赖注入?
IOC:把bean的创建、初始化、销毁交给 spring 来管理,而不是由开发者控制,实现控制反转。
依然注入有以下三种实现方式: 1. 构造器注入 2. Setter 方法注入 3. 接口注入
-
BeanFactory 和 FactoryBean 有什么区别?
-
@Conditional条件注册bean
//Conditional注解条件注册bean,该注解value值必须为Condition类型
@Conditional(WindCondition.class)
@Bean
public Person person(){
return new Person("aaa", 20);
}
//上述Conditional注解配置的条件类
public class WindCondition implements Condition {
/**
* 测试条件注册bean,如果当前操作系统为windows则注入bean
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取ioc容器的beanFactory
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
//获取当前环境信息
Environment environment = context.getEnvironment();
//environment获取当前操作系统
String property = environment.getProperty("os.name");
if(property.contains("Windows")){
return true; //返回true则注入bean
}
return false;
}
}
- @Import注册bean
@Configuration
@ComponentScan(value = "com.shadow")
@Controller
//用Import注解注册bean,可放入多个class,指定的class需要提供无参构造方法,bean的id默认为全类名
@Import({Person.class})
public class AppConfig {
}
//还可以通过ImportBeanDefinitionRegistrar自定义注册,向容器中注册bean;
//新建一个类CustomImportBeanDefinitionRegistrar实现该接口,通过Import注解导入
@Import({Person.class, CustomImportBeanDefinitionRegistrar.class})
public class AppConfig {
}
public class CustomImportBeanDefinitionRegistrar
implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//在容器中注册自定义bean
RootBeanDefinition beanDefinition = new RootBeanDefinition(Order.class);
registry.registerBeanDefinition("order", beanDefinition);
}
}
-
在 Spring 中如何配置 Bean ?
1.通过@ComponentScan方式进行扫包,一般针对我们自己写的类,如controller,service
2.@Bean 的配置方式
3.@Import注解方式:快速给容器导入一个组件,结合@Import注解可以实现两个接口自定义注册
ImportSelector 与 ImportBeanDefinitionRegistrar
4.使用spring提供的FactoryBean(工厂bean)
源码细节:通过FactoryBean创建的bean在容器获取的时候,会调用实现了FactoryBean接口的类中,重写的getObject方法,如下代码
//实现FactoryBean注册bean public class CustomFactoryBean implements FactoryBean<Cat> { //通过FactoryBean向容器中注册bean时,调用getObject方法创建实例 @Override public Cat getObject() throws Exception { return new Cat(); } @Override public Class<?> getObjectType() { return null; } //是否为单例 @Override public boolean isSingleton() { return true; } } @Configuration @ComponentScan(value = "com.shadow") public class AppConfig { //将具体FactoryBean当做bean注册到容器 @Bean public CustomFactoryBean customFactoryBean(){ return new CustomFactoryBean(); } } public class Test { public static void main(String[] args) { AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(AppConfig.class); System.out.println("ioc容器初始化完成"); //customFactoryBean在容器中也是一个bean //根据id在调用getBean时在源码中约定,如果bean的id是&开头,则取工厂自身的bean //如果不是&开头,则取类中实现了getObject()方法的bean Object object = app.getBean("customFactoryBean"); System.out.println(object.getClass()); } } //源码部分 public static boolean isFactoryDereference(@Nullable String name) { return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); } //如果bean的name是&开头的,则直接取容器中的bean if (BeanFactoryUtils.isFactoryDereference(name)) { return beanInstance; } //如果不是,当前factory是单列且在容器中 if (factory.isSingleton() && containsSingleton(beanName)) { //调用doGetObjectFromFactoryBean方法获取具体的实例 Object object = doGetObjectFromFactoryBean(factory, beanName); } //调用factory对象中getObject获取具体的实例 private Object doGetObjectFromFactoryBean(FactoryBean<?> factory, String beanName){ return factory.getObject(); }
-
BeanFactory 和 ApplicationContext 有什么区别?
-
Spring 有几种配置方式?
- 基于 XML 的配置
- 基于注解的配置
- 基于 Java 的配置
-
请解释 Spring Bean 的生命周期?
spring的生命周期只针对单实例bean
bean的生命周期:指 bean创建-----初始化----销毁 的过程
bean的生命周期是由容器进行管理的
我们可以自定义 bean初始化和销毁方法,容器在bean进行到当前生命周期的时候, 来调用自定义的初始化和销毁方法
实现BeanPostProcessor 接口,重写该接口中的两个方法,也可以对bean的生命周期进行管理
执行顺序: -> 调用构造方法创建实例对象bean -> 底层调用populateBean方法对bean进行属性复制 -> 调用BeanPostProcessor实现类中的方法前置处理器postProcessBeforeInitialization() > 执行bean的初始化 init-method 方法 -> 调用BeanPostProcessor实现类中postProcessAfterInitialization()后置处理器,在init-method 之后进行后置处理工作
//测试声明周期 public class Person { public Person() { System.out.println("bean对象创建!"); } public void init(){ System.out.println("bean对象初始化!"); } public void destory(){ System.out.println("bean对象销毁!"); } } @Configuration @ComponentScan(value = "com.shadow") public class AppConfig { @Bean(initMethod = "init", destroyMethod = "destory") public Person person(){ return new Person(); } } //容器启动时,调用构造方法创建bean,并调用init方法对bean初始化,当容器close时,调用bean的销毁方法 public class Test { public static void main(String[] args) { new AnnotationConfigApplicationContext(AppConfig.class).close(); } }
-
Spring 框架中的单例 Beans 是线程安全的么?
Spring 框架并没有对单例 bean 进行任何多线程的封装处理。关于单例 bean 的线程安全和并发问题需要
开发者自行去搞定。但实际上,大部分的 Spring bean 并没有可变的状态(比如 Serview 类 和 DAO 类),所以
在某种程度上说 Spring 的单例 bean 是线程安全的。如果你的 bean 有多种状 态的话,就需要自行保证线程
安全。 最浅显的解决办法就是将多态 bean 的作用域由“singleton”变更为“prototype”。
-
如何开启基于注解的自动装配?
要使用
@Autowired
,需要注册AutowiredAnnotationBeanPostProcessor
,有两种方式来实现:- 在配置文件中添加bean配置
<beans> <context:annotation-config /> </beans>
- 配置文件中直接引入 AutowiredAnnotationBeanPostProcessor
<beans> <bean class="org.springframework.beans.factory.annotation.AutowiredAnnotati onBeanPostProcessor"/> </beans>
-
Spring 框架中都用到了哪些设计模式?
-
代理模式—在 AOP 和 remoting 中被用的比较多。
-
单例模式—在 spring 配置文件中定义的 bean 默认为单例模式。
-
模板方法—用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTempl ate。
-
工厂模式—BeanFactory 用来创建对象的实例
-
责任链模式:在 AOP切面拦截的方法具体执行中
-
-
Spring AOP与IOC原理
-
Spring中循环依赖问题如何解决
-
IOC 容器对 Bean 的生命周期
- 通过构造器或工厂方法创建 Bean 实例
- 为 Bean 的属性设置值和对其他 Bean 的引用
- 将 Bean 实 例 传 递 给 Bean 后 置 处 理 器 的 postProcessBeforeInitialization 方 法
- 调用 Bean 的初始化方法(init-method)
- 将 Bean 实 例 传 递 给 Bean 后 置 处 理 器 的 postProcessAfterInitialization 方法
- Bean 可以使用了
- 当容器关闭时, 调用 Bean 的销毁方法(destroy-method)
四、SpringMVC
面试题
-
什么是 SpringMvc?
SpringMvc 是 spring 的一个模块,基于 MVC 的一个框架,无需中间整合层来整合。
-
SpringMVC 工作原理
-
客户端发送请求到 DispatcherServlet
-
DispatcherServlet 查询 handlerMapping 找到处理请求的 Controlle
-
Controller 调用业务逻辑后,返回 ModelAndView
-
DispatcherServlet 查询 ModelAndView,找到指定视图
-
视图将结果返回到客户端
-
-
SpringMVC 流程?
-
用户发送请求至前端控制器 DispatcherServlet。
-
DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。
-
处理器映射器找到具体的处理器,生成处理器对象 及处理器拦截器一并返回给 DispatcherServlet。
-
DispatcherServlet 调用 HandlerAdapter 处理器适配器。
-
HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)。
-
Controller 执行完成返回 ModelAndView
-
HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet。
-
DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。
-
ViewReslover 解析后返回具体 View
-
DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。
-
DispatcherServlet 响应用户。
-
-
SpringMvc 的控制器是不是单例模式,如果是,有什么问题,怎么解决?
-
是单例模式,所以在多线程访问的时候有线程安全问题
-
不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。 或者设置为多例模式
-
-
如果在拦截请求中,我想拦截 get 方式提交的方法,怎么配置?
可以在@RequestMapping 注解里面加上 method=RequestMethod.GET
-
怎么样在方法里面得到 Request,或者 Session?
直接在方法的形参中声明 request,SpringMvc 就自动把 request 对象传入
-
SpringMvc 用什么对象从后台向前台传递数据的?
通过 ModelMap 对象,可以在这个对象里面用 put 方法,把对象加到里面,前台就可以通 过 el 表达式拿到。
-
SpringMvc 里面拦截器是怎么写的?
有两种写法,一种是实现接口,另外一种是继承适配器类,然后在 SpringMvc 的配置文件中 配置拦截器即可
<!-- 配置 SpringMvc 的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的 Bean 就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.et.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.et.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors>
-
讲下 SpringMvc 的执行流程
系统启动的时候根据配置文件创建 spring 的容器, 首先是发送 http 请求到核心控制器 disPatherServlet,spring 容器通过映射器去寻找业务控制器,使用适配器找到相应的业务 类,在进业务类时进行数据封装,在封装前可能会涉及到类型转换,执行完业务类后使用 ModelAndView 进行视图转发,数据放在 ModelMap 中传递数据,进行页面显示。
-
session与cookie
1.cookie在客户端,session在服务端,cookie的产生是在服务端产生的。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie。客户端会把Cookie保存起来。 当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。
2.每个用户访问服务器都会建立一个session,用户与服务器建立连接的同时,服务器会自动为其分配一个SessionId。每次请求 cookie都把SessionId 自动附加在HTTP头信息中,带到服务器,当服务器处理完后,将结果返回给SessionId所对应的用户
3.session和cookie的存储都存在时效性,这是很有必要的
4.单个cookie保存的数据不能超过4kb,很多浏览器都限制了一个站点最多保存20个cookie
5.不管是cookie还是session,都是建立在安全性的大前提下,session中不仅仅有cookie的信息,同时会有该用户的相关重要且安全的信息存储,所以session是在服务器的,而cookie只是服务器将一些不重要的信息拿出来丢给客户的存在,以备以后快速匹配校验用。
-
拦截器与过滤器
-
Filter需要在web.xml中配置,依赖于Servlet;
-
Interceptor需要在SpringMVC中配置,依赖于 SpringMVC 框架;
-
Filter的执行顺序在Interceptor之前
-
两者的本质区别:拦截器(Interceptor)是基于Java的反射机制(动态代理),而过滤器(Filter)是基于函数回调。从灵活性上说拦截器功能更强大些,Filter能做的事情,都能做,而且可以在请求前,请求后执行,比较灵活。Filter主要是针对URL地址做一个编码的事情、过滤掉没用的参数、安全校验,太细的话,还是建议用interceptor;
-
拦截器不依赖servlet容器,过滤器需要依赖于servlet容器。
-
在action生命周期中,拦截器可以调用多次,过滤器只在容器初始化的时候调用一次。
-
五、SpringBoot
基础知识
SpringBoot启动原理
启动流程:主要分为三个部分 ,第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器,第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块,第三部分是自动化配置模块,该模块作为springboot自动配置核心
**启动:**每个SpringBoot程序都有一个主入口,也就是main方法,main里面调用SpringApplication.run()启动整个spring-boot程序,
1.首先进入run方法 ,run方法中去创建了一个SpringApplication实例,在该构造方法内,我们可以发现其调用了一个初始化的initialize方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SzjvkYSw-1631955925719)(C:\Users\admin\Desktop\笔记\img\Boot初始化.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svN6bIVp-1631955925720)(C:\Users\admin\Desktop\笔记\img\boot初始化2.png)]
这里主要是为SpringApplication对象赋一些初值。构造函数执行完毕后,我们回到run方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7JxrnTq-1631955925722)(C:\Users\admin\Desktop\笔记\img\boot初始化3.png)]
该方法中实现了如下几个关键步骤:
1.创建了应用的监听器SpringApplicationRunListeners并开始监听
该方法中采用SPI机制,去读取 META-INF/spring.factories 目录的配置文件内容, 把配置文件中的类加载到 spring 容器中
2.加载SpringBoot配置环境(ConfigurableEnvironment),如果是通过web容器发布,会加载StandardEnvironment,其最终也是继承了ConfigurableEnvironment,类图如下
3.配置环境(Environment)加入到监听器对象中(SpringApplicationRunListeners)
4.创建run方法的返回对象:ConfigurableApplicationContext(应用配置上下文)
创建 springboot 的上下文对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JD6Rd4gT-1631955925723)(C:\Users\admin\Desktop\笔记\img\boot初始化4.png)]
在这个上下文对象构造函数中把 ConfigurationClassPostProcessor变成 beanDefinition 对象。
5.回到run方法内,prepareContext方法将listeners、environment、applicationArguments、banner等重要组件与上下文对象关联 :
6.接下来的refreshContext(context)方法,实现spring-boot-starter-*(mybatis、redis等)自动化配置的关键,包括spring.factories的加载,Spring容器及bean的实例化等核心工作。
7.内置 tomcat 的启动和部署,Tomcat 的启动在 onRefresh()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XaAiIwlB-1631955925724)(C:\Users\admin\Desktop\笔记\img\boot初始化5.png)]
springboot 的启动就是核心就是完成了两件事, 一个是 spring容器的启动调用了 refresh 核心方法, 一个是 tomcat 的启动, new 出了一个内置的 tomcat。 配置结束后,Springboot做了一些基本的收尾工作,返回了应用环境上下文。回顾整体流程,Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean,至此,通过SpringBoot启动的程序已经构造完成
SpringBoot 自动配置源码
自动配置功能开启, 我们看看启动类:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-he8qnlva-1631955925724)(C:\Users\admin\Desktop\笔记\img\boot自动装配1.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DmbzvoHj-1631955925725)(C:\Users\admin\Desktop\笔记\img\boot自动装配2.png)]
我们看看这个类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ylpZzpdY-1631955925725)(C:\Users\admin\Desktop\笔记\img\boot自动装配3.png)]
实现了 DeferredImportSelector 接口。这个类的核心功能是通过 SPI 机制收集 EnableAutoConfiguration 为 key 的所有类, 然后通过 ConfigurationClassPostProcessor 这个类调用到该类中的方法,把收集到的类变成beanDefinition 对象最终实例化加入到 spring 容器
EnableAutoConfiguration 为 key 的所有类: 该配置文件在spring-boot-autoconfigure jar 包中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6Pg0CEF-1631955925726)(C:\Users\admin\Desktop\笔记\img\boot自动装配5.png)]
该类AutoConfigurationImportSelector 中两个方法会被ConfigurationClassPostProcessor 调到:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UMI6tSaV-1631955925726)(C:\Users\admin\Desktop\笔记\img\boot自动装配6.png)]
在这两个方法中完成了 SPI 类的收集。ConfigurationClassPostProcessor 类只是把收集到的类变成 beanDefinition并加入到 spring 容器。
ConfigurationClassPostProcessor 类调用的地方
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ftOudwRp-1631955925727)(C:\Users\admin\Desktop\笔记\img\boot自动装配7.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RS5kPwCb-1631955925727)(C:\Users\admin\Desktop\笔记\img\boot自动装配8.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r53c631j-1631955925728)(C:\Users\admin\Desktop\笔记\img\boot自动装配9.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XjT6gs3n-1631955925729)(C:\Users\admin\Desktop\笔记\img\boot自动装配10.png)]
这就是这两个方法的调用地方。上述就是 EnableAutoConfiguration 为 key 自动配置类的收集过程。 有自动配置
类的收集并加入到 spring 容器, 其中有 aop, 事务, 缓存, mvc 功能等,就已经导入到 springboot 工程了。
面试题
-
什么是 Spring Boot?
Spring Boot 已经建立在现有 spring 框架之上。使用 spring 启动,避免了之前我们必须做的所有样板代码和配置。因此,SpringBoot 可以帮助我们以最少的工作量,更加健壮地使用现有的 Spring 功能。
-
Spring Boot 有哪些优点?
-
减少开发成本,提高效率。
-
使用 JavaConfig 有助于避免使用 XML。
-
避免大量的 Maven 导入和各种版本冲突。
-
提供意见发展方法。 通过提供默认值快速开始开发。 没有单独的 Web 服务器需要。这意味着你不再需要启动 Tomca。
-
减少配置,因为没有 web.xml 文件。
-
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
- @EnableAutoConfiguration:打开自动配置的功能
- @ComponentScan:Spring组件扫描。
-
Spring Boot 自动配置原理是什么?
SpringBoot的自动配置注解是@EnableAutoConfiguration,点进去发现 @Import的类 中有执行自动加载配置的代码,其中 loadFactoryNames 方法会加载类路径及所有jar包下META-INF/spring.factories配置中映射的自动配置的类。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
- 如何重新加载 Spring Boot 上的更改,而无需重新启动服务器?
- 使用springloaded配置pom.xml文件 (生产环境推荐)
- 使用devtool工具包 (开发环境推荐)
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
- Spring Boot 如何定义多套不同环境配置
假如有开发、测试、生产三个不同的环境,需要定义三个不同环境下的配置。
-
applcation.properties
-
application-dev.properties
-
application-test.properties
-
application-prod.properties
需要指定环境时,在applcation.properties文件中指定当前的环境spring.profiles.active=dev,用dev环境
spring:
profiles:
active: dev
- springboot实现热部署的原理
深层原理是使用了两个ClassLoader,一个ClassLoader加载那些不会改变的类(第三方jar包),另一个ClassLoader加载会更改的类,称为restartClassLoader ,这样在有代码更改的时候,原来的restartClassLoader被丢弃,重新建一个restartClassLoader,由于需要加载的类相比较少,所以实现了较快的重启时间
- 如何在自定义端口上运行 Spring Boot 应用程序?
为了在自定义端口上运行 Spring Boot 应用程序,您可以在 application.properties 中指定端 口。
server.port = 8090
-
如何实现 Spring Boot 应用程序的安全性?
使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展 WebSecurityConfigurerAdapter 并覆 盖其方法。
-
什么是 Swagger?你用 Spring Boot 实现了它吗?
Swagger 广泛用于可视化 API。Swagger 是 用于生成 RESTful Web 服务的可视化表示的工具,规范和完整框架实现。它使文档能够以 与服务器相同的速度更新。
-
如何使用 Spring Boot 实现异常处理
Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个 ControlerAdvice 类,来处理控制器类抛出的所有异常。
六、Spring Cloud
面试题
-
什么是Spring Cloud?
Spring cloud 应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成,更专注于服务治理。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。
-
Spring Cloud和Dubbo的区别
-
Dubbo关注的领域是Spring Cloud的一个子集。Dubbo专注于服务治理,其在服务治理、灰度发布、流量分发方面比Spring Cloud更全面。Spring Cloud覆盖整个微服务架构领域。
-
Dubbo使用RPC调用效率高一些,Spring Cloud使用HTTP调用效率低,使用更简单。
-
-
REST和RPC的区别
-
REST风格的系统交互更方便,RPC调用服务提供方和调用方之间依赖太强。
-
REST调用系统性能较低,RPC调用效率比REST高。
-
REST的灵活性可以跨系统跨语言调用,RPC只能在同语言内调用。
-
REST可以和Swagger等工具整合,自动输出接口API文档。
- SpringCloud如何实现服务的注册和发现
-
服务在发布时,指定对应的服务名(服务名包括了IP地址和端口) 将服务注册到注册中心(eureka或者zookeeper)。
-
这一过程是springcloud自动实现 只需要在main方法添加@EnableDisscoveryClient 同一个服务修改端口就可以启动多个实例。
-
调用方法:传递服务名称通过注册中心获取所有的可用实例 通过负载均衡策略调用(ribbon和feign)对应的服务。
- 什么是服务熔断和服务降级?
熔断机制:是应对雪崩效应的一种微服务链路保护机制。当某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。
服务降级:一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。这样做,虽然会出现局部的错误,但可以避免因为一个服务挂机,而影响到整个架构的稳定性。
Hystrix相关注解:
-
@EnableHystrix:开启熔断
-
@HystrixCommand(fallbackMethod=”XXX”):声明一个失败回滚处理函数XXX,当被注解的方法执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。
-
项目中zuul常用的功能
- 提供动态路由
- 提供安全、鉴权处理
- 跨域处理
- 全局动态路由的hystrix(熔断、降级、限流)处理
-
服务网关的作用
-
简化客户端调用复杂度,统一处理外部请求。
-
数据裁剪以及聚合,根据不同的接口需求,对数据加工后对外。
-
多渠道支持,针对不同的客户端提供不同的网关支持。
-
遗留系统的微服务化改造,可以作为新老系统的中转组件。
-
统一处理调用过程中的安全、权限问题。
-
Spring Cloud中的网关有:Zuul和Spring Cloud Gateway,最新版本中推荐使用后者。
-
ribbon和feign区别
Ribbon添加maven依赖 spring-starter-ribbon
使用@RibbonClient(value=“服务名称”) ,使用RestTemplate调用远程服务对应的方法。
feign添加maven依赖 spring-starter-feign
服务提供方提供对外接口 调用方使用 在接口上使用@FeignClient(“指定服务名”)
- 启动类使用的注解不同,Ribbon用的是@RibbonClient,Feign用的@EnableFeignClients。
- 服务的指定位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。
- 调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。 Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建http请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致。
-
ribbon的负载均衡策略
-
RoundRobinRule: 轮询策略
-
RandomRule: 随机策略
-
BestAvailableRule: 最大可用策略,即先过滤出故障服务器后,选择一个当前并发请求数最小的;
-
WeightedResponseTimeRule: 带有加权的轮询策略,对各个服务器响应时间进行加权处理,然后在采用轮询的方式来获取相应的服务器;
-
-
简述什么是CAP,并说明Eureka包含CAP中的哪些?
**CAP理论:**一个分布式系统不可能同时满足C (一致性),A(可用性),P(分区容错性).由于分区容错性P在分布式系统中是必须要保证的,因此我们只能从A和C中进行权衡.
Eureka 遵守 AP:
- Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。
- 而Eureka的客户端在向某个Eureka 注册或查询时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查的信息可能不最新的**不保证强一致性**。
- Eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?
Zookeeper保证了CP(C:一致性,P:分区容错性)
Eureka保证了AP(A:高可用)
- 链路跟踪Sleuth
当我们项目中引入Spring Cloud Sleuth后,每次链路请求都会添加一串追踪信息,格式是[server-name, main-traceId,sub-spanId,boolean]:
- server-name:服务结点名称。
- main-traceId:一条链路唯一的ID,为TraceID。
- sub-spanId:链路中每一环的ID,为SpanID。
- boolean:是否将信息输出到Zipkin等服务收集和展示。
七、Mybatis
面试题
-
什么是 MyBatis?
MyBatis 是一个可以自定义 SQL、存储过程和高级映射的持久层框架。
-
讲下 MyBatis 的缓存
MyBatis 的缓存分为一级缓存和二级缓存
一级缓存放在 session 里面,默认就有,
二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现 Serializable 序列化接口,可在它的映射文件中配置
-
Mybatis 是如何进行分页的?分页插件的原理是什么?
Mybatis 使用 RowBounds 对象进行分页,可直接编写 sql 实现分页,也可以使用 Mybatis 的分页插件。
分页插件的原理:实现 Mybatis 提供的接口(),实现自定义插件,在插件的拦截方法内拦 截待执行的 sql,然后重写 sql。
<!-- 第一步:引入mybatis的 pagehelper 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
<!-- 第二步:在mybatis的全局配置文件中配置PageHelper分页插件 -->
<plugins>
<!--自4.0.0版本以后实现这个接口了 com.github.pagehelper.PageInterceptor -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 设置数据库类型 Oracle,Mysql,MariaDB,SQLite,Hsqldb,PostgreSQL六种数据库-->
<!-- 自4.0.0以后的版本已经可以自动识别数据库了,所以不需要我们再去指定数据库 -->
<!--<property name="dialect" value="Mysql"/>-->
</plugin>
</plugins>
举例:select * from student,拦截 sql 后重写为:select t.* from (select * from student)t
limit 0,10
具体参考:
https://www.cnblogs.com/helf/p/11098105.html
-
简述 Mybatis 的插件运行原理,以及如何编写一个插件?
Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、 Executor 这 4 种接口的插件,Mybatis 通过动态代理,为需要拦截的接口生成代理对象以实 现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后在给插件编写注解,指定 要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。
-
#{}和${}的区别是什么?
- #{}是预编译处理,${}是字符串替换。
- Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 的 set 方法 来赋值;
- Mybatis 在处理 时 , 就 是 把 {}时,就是把 时,就是把{}替换成变量的值。
- 使用#{}可以有效的防止 SQL 注入,提高系统安全性。
-
Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否 启用延迟加载 lazyLoadingEnabled=true|false。
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方 法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单 独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的 对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。
-
MyBatis 的好处是什么?
- MyBatis 把 sql 语句从 Java 源程序中独立出来,放在单独的 XML 文件中编写,给程序的 维护带来了很大便利。
- MyBatis 封装了底层 JDBC API 的调用细节,并能自动将结果集转换成 Java Bean 对象, 大大简化了 Java 数据库编程的重复工作。
- 因为 MyBatis 需要程序员自己去编写 sql 语句,程序员可以结合数据库自身的特点灵活 控制 sql 语句,因此能够实现比 Hibernate 等全自动 orm 框架更高的查询效率,能够完成复 杂查询。
- 简述 Mybatis 的 Xml 映射文件和 Mybatis 内部数据结构之间的映射关系?
Mybatis 将所有 Xml 配置信息都封装到 All-In-One 重量级对象 Configuration 内部。在 Xml 映射文件中,标签会被解析为 ParameterMap 对象,其每个子元素会 被解析为 ParameterMapping 对象。标签会被解析为 ResultMap 对象,其每个子 元素会被解析为 ResultMapping 对象。每一个 select 、 insert 、 update 、 delete 标签 均会被解析为 MappedStatement 对象,标签内的 sql 会被解析为 BoundSql 对象。
-
接口绑定有几种实现方式,分别是怎么实现的?
接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select@Update 等注解里面包含 Sql 语句来绑定,另外一种就是通过 xml 里面写 SQL 来绑 定,在这种情况下,要指定 xml 映射文件里面的 namespace 必须为接口的全路径名.
-
模糊查询 like 语句该怎么写
- 在 java 中拼接通配符,通过#{}赋值
- 在 Sql 语句中拼接通配符 (不安全 会引起 Sql 注入)
-
Mybatis 中如何执行批处理?
使用 BatchExecutor 完成批处理。
-
**Mybatis 都有哪些 Executor 执行器?它们之间的区别是什么? **
Mybatis 有三种基本的 Executor 执行器,SimpleExecutor、ReuseExecutor、 BatchExecutor。
- SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对 象,用完立刻关闭 Statement 对象。
- ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象, 而是放置于 Map
- BatchExecutor:完成批处理。
-
如何获取自动生成的(主)键值
配置文件设置 usegeneratedkeys 为 true
八、Redis
面试题
-
什么是 Redis?简述它的优缺点?
Redis 本质上是一个 Key-Value 类型的内存数据库,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存。
**优点:**因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB。 Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构。
**缺点:**数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上
-
Redis 与 memcached 相比有哪些优势?
- memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类型
- redis 的速度比 memcached 快很多
- redis 可以持久化其数据 redis 可以持久化其数据
-
Redis 支持哪几种数据类型?
字符串 String、 哈希Hash、列表List、集合 Set、有序集合 ZSet。如果是高级用户,还需要加上下面几种数据结构 HyperLogLog、 Geo、Pub/Sub。
-
Redis 有哪几种数据淘汰策略?
1. noeviction: 不删除数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误。
2. allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
3. volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
4. allkeys-random: 回收随机的键使得新添加的数据有空间存放。
5. volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
6. volatile-ttl: 回收过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。 -
一个字符串类型的值能存储最大容量是多少?
512M
-
为什么 Redis 需要把所有数据放到内存中?
Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。 所以 redis 具有快速和数据持久化的特征,如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的 性能。
-
Redis 集群方案应该怎么做?都有哪些方案?
-
Twemproxy
-
codis
-
Redis-cluster(本身提供了自动将数据分散到 Redis Cluster 不同节点的能力,整个数据集 合的某个数据子集存储在哪个节点对于用户来说是透明的)
redis-cluster 分片原理:Cluster 中有一个 16384 长度的槽(虚拟槽),编号分别为 0-16383。 每个 Master 节点都会负责一部分的槽,当有某个 key 被映射到某个 Master 负责的槽,那 么这个 Master 负责为这个 key 提供服务,至于哪个 Master 节点负责哪个槽,可以由用户 指定,也可以在初始化的时候自动生成,只有 Master 才拥有槽的所有权。Master 节点维 护着一个 16384/8 字节的位序列,Master 节点用 bit 来标识对于某个槽自己是否拥有。比 如对于编号为 1 的槽,Master 只要判断序列的第二位(索引从 0 开始)是不是为 1 即可。 这种结构很容易添加或者删除节点。比如如果我想新添加个节点 D, 我需要从节点 A、B、 C 中得部分槽到 D 上。
-
Redis 集群方案什么情况下会导致整个集群不可用?
有 A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点 B 失败了,那么整个集群就会以为缺少 5501-11000 这个范围的槽而不可用。
-
Redis 支持的 Java 客户端都有哪些?官方推荐用哪个?
Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。
-
说说 Redis 哈希槽的概念?
Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通 过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
-
Redis 集群的主从复制模型是怎样的?
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型, 每个节点都会有 N-1 个复制品.
-
Redis 集群会有写操作丢失吗?为什么?
Redis 并不能保证数据的强一致性,这意味在实际中集群在特定的条件下可能会丢失写操作。
-
Redis 集群最大节点个数是多少?
16384 个
-
怎么理解 Redis 事务?
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行
-
Redis 事务相关的命令有哪几个?
MULTI、EXEC、DISCARD、WATCH
-
Redis key 的过期时间和永久有效分别怎么设置?
EXPIRE 和 PERSIST 命令
-
Redis 如何做内存优化?
尽可能使用散列表(hashes),散列表使用的内存非常小
-
Redis 回收进程如何工作的?
一个客户端运行了新的命令,添加了新的数据。 Redis检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。
-
使用过 Redis 分布式锁么,它是怎么实现的
先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。 如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样? set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用的!
-
什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?
缓存穿透: 一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就会去DB查找。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免:
- 对查询结果为空的情况进行缓存,缓存时间设置短一点,或该 key 对应的数据 insert 了之后清理缓存。
- 对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 bitmap 过滤。
**缓存雪崩:**当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免:
-
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
-
做二级缓存,A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为 短期,A2 设置为长期
-
不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀
-
redis 主从复制如何实现的?redis 的 key 是如何寻址的?
主从复制实现:主节点将自己内存中的数据做一份快照,将快照发给从节点,从节点将数据恢复到内存中。之后再每次增加新数据的时候,主节点以类似于 mysql 的二进制日志方式将语句发送给从节点,从节点拿到主节点发送过来的语句进行重放。
寻址计算:
-
**hash 算法:**来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致大部分的请求过来,全部无法拿到有效的缓存,导致大量的流量涌入数据库。
-
**一致性hash算法:**一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
然而,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
-
-
缓存与数据库不一致怎么办?
**描述:**假设采用的主存分离,读写分离的数据库, 如果一个线程 A 先删除缓存数据,然后将数据写入到主库当中,这个时候,主库和从库同步没有完成,线程 B 从缓存当中读取数据失败,从从库当中读取到旧数据,然后更新至缓存,这个时候,缓存当中的就是旧的数据。 发生上述不一致的原因在于,主从库数据不一致问题,加入了缓存之后,主从不一致的时间被拉长了
**处理思路:**在从库有数据更新之后,将缓存当中的数据也同时进行更新,即当从库发生了数据更新之后,向缓存发出删除,淘汰这段时间写入的旧数据。
-
主从数据库不一致如何解决
**场景描述:**对于主从库,读写分离,如果主从库更新同步有时差,就会导致主从库数据的不一致
解决思路:
- 忽略这个数据不一致,在数据一致性要求不高的业务下,未必需要时时一致性
- 强制读主库,使用一个高可用的主库,数据库读写都在主库,添加一个缓存,提升数据读取的性能。
- 选择性读主库,添加一个缓存,用来记录必须读主库的数据,将哪个库,哪个表,哪个主键,作为缓存的 key,设置缓存失效的时间为主从库同步的时间,如果缓存当中有这个数据,直接读取主库,如果缓存当中没有这个主键,就到对应的从库中读取。
-
怎么发现热key,如何防止
发现:
- 凭借业务经验,进行预估哪些是热key
- 在客户端进行收集
redis-faina
工具, 但是该命令在高并发的条件下,有内存增暴增的隐患,还会降低redis的性能。- hotkeys参数
- 自己抓包评估
防止:
- 利用二级缓存
- 备份热key,在多个redis上都存一份不就好了
九、设计模式
十、JUC
基础概念
-
CPU核心数和线程数的关系
CPU核心数与线程数1:1
使用了超线程技术后为—> 1:2
区别:
CPU的核心数是指硬件上的真实对象。
CPU线程数只是一个逻辑概念,不是一个真正的对象,只是为了更好地描述CPU的运行能力。
-
CPU时间片轮转机制(RR调度)
时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一时间段,称作它的时间片,即该进程允许运行的时间。
系统会维护一张就绪进程列表,其实就是一个先进先出的队列,新来的进程就会被加到队列的末尾,然后每次执行进程调度的时候,都会选择队列的队首进程,让它在CPU上运行一个时间片的时间,不过如果分配的时间片已经消耗光了而进程还在运行,调度程序就会停止该进程的运行,同时把它移到队列的末尾,CPU会被剥夺并分配给队首进程,而如果进程在时间片结束前阻塞或者结束了,则CPU就会进行切换。
-
什么是进程和线程
进程:程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源
线程:CPU调度的最小单位,必须依赖进程而存在。
-
并行和并发
**并行(parallel):**同一时刻,可以同时处理事情的能力,有多条指令在多个处理器上同时执行。互不干扰
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yD0JIfEL-1631955925730)(C:\Users\admin\Desktop\笔记\img\并行.png)]
**并发(concurrency):**与单位时间相关,在时间段内可以处理事情的能力
同一时刻只能够执行一条指令,但是多条指令被快速的进行切换,给人造成了它们同时执行的感觉。但在微观来说,并不同时进行的,只是划分时间段,分别进行执行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lVFybI6Q-1631955925730)(C:\Users\admin\Desktop\笔记\img\并发.png)]
-
高并发编程的意义、好处和注意事项
**好处:**充分利用cpu的资源、加快用户响应的时间,程序模块化,异步化
问题: 线程共享资源,存在冲突;
容易导致死锁;
启用太多的线程,就有搞垮机器的可能
Java里的线程
一、启动线程的方式
三种:类Thread、接口Runnable、接口Callable
public class NewThread {
/*第一种:扩展自Thread类*/
static class UseThread extends Thread{
@Override
public void run() {
System.out.println("I am extends Thread");
}
}
/*第二种:实现Runnable接口*/
private static class UseRun implements Runnable{
@Override
public void run() {
System.out.println("I am implements Runnable");
}
}
/*第三种:实现Callable接口,允许有返回值*/
private static class UseCall implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("I am implements Callable");
return "CallResult";
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
//第一种
new UseThread().start();
//第二种
UseRun useRun = new UseRun();
new Thread(useRun).start();
//第三种
UseCall useCall = new UseCall();
FutureTask<String> futureTask = new FutureTask<>(useCall);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
二、线程安全停止
-
线程自然终止:自然执行完或抛出未处理异常
-
stop(),resume()与suspend()搭配, 已不建议使用,
stop(): 会立即释放CPU资源和释放锁。但不安全,会导致线程不会正确释放资源
resume(): 恢复线程的执行
suspend():线程挂起,不会释放资源,容易导致死锁
参考: https://blog.youkuaiyun.com/Mrs_Yu/article/details/107550821
-
interrupt()、 isInterrupted() 、static方法interrupted() 中止
-
java线程是协作式,而非抢占式
调用一个线程的interrupt() 方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身决定。
isInterrupted() 判定当前线程是否处于中断状态。
static方法interrupted() 判定当前线程是否处于中断状态,同时中断标志位改为false。方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
三、线程常用方法和线程的状态
线程只有5种状态。整个生命周期就是这几种状态的切换
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ruNEDa3T-1631955925731)(C:\Users\admin\Desktop\笔记\img\线程状态.png)]
四、守护线程
和主线程共死,finally不能保证一定执行
五、run()和start()
run方法就是普通对象的普通方法,只有调用了start()后,Java才会将线程对象和操作系统中实际的线程进行映射,再来执行run方法。
六、yield()
让出cpu的执行权,将线程从运行转到可运行状态,但是下个时间片,该线程依然有可能被再次选中运行。
七、等待和通知
wait() 与 notify()/notifyAll() 对象上的方法
标准范式:
等待方:
1. 获取对象的锁;
2. 循环里判断条件是否满足,不满足调用wait方法
3. 条件满足执行业务逻辑
通知方来说:
- 获取对象的锁;
- 改变条件
- 通知所有等待在对象的线程
//代码示例
public class Express {
public final static String CITY = "ShangHai";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
public Express(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public synchronized void changeKm(){
this.km = 101;
notifyAll();
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public synchronized void changeSite(){
this.site = "BeiJing";
notify();
}
public synchronized void waitKm(){
while(this.km<=100) {
try {
wait();
System.out.println("km thread["+Thread.currentThread().getId()+"]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the km is"+this.km+",I will change db.");
}
public synchronized void waitSite(){
while(CITY.equals(this.site)) {
try {
wait();
System.out.println("site thread["+Thread.currentThread().getId()+"]");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("the site is"+this.site+",I will call user.");
}
}
public class TestWN {
private static Express express = new Express(0,Express.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
//测试唤醒通知
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){//三个线程
new CheckSite().start();
}
for(int i=0;i<3;i++){//里程数的变化
new CheckKm().start();
}
Thread.sleep(1000); //延迟一秒,让所有的线程创建并启动
express.changeKm();//快递地点变化
express.changeSite();//快递地点变化
}
}
八、notify和notifyAll应该用谁?
应该尽量使用notifyAll,使用notify因为有可能发生信号丢失的的情况
九、join方法
线程A,执行了线程B的join方法,线程A必须要等待B执行完成了以后,线程A才能继续自己的工作
public class UseJoin {
static class JumpQueue implements Runnable {
private Thread thread;//用来插队的线程
public JumpQueue(Thread thread) {
this.thread = thread;
}
public void run() {
try {
String currentName = Thread.currentThread().getName()
System.out.println(thread.getName()+" join before " +currentName);
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" terminted.");
}
}
public static void main(String[] args) throws Exception {
Thread previous = Thread.currentThread();//现在是主线程
for (int i = 0; i < 10; i++) {
//i=0,previous 是主线程,i=1;previous是i=0这个线程
Thread thread =
new Thread(new JumpQueue(previous), String.valueOf(i));
System.out.println(previous.getName()+" jump a queue the thread:"
+thread.getName());
thread.start();
previous = thread;
}
SleepTools.second(2);//让主线程休眠2秒
System.out.println(Thread.currentThread().getName() + " terminate....");
}
}
十、调用yield() 、sleep()、wait()、notify()等方法对锁有何影响?
- 线程在执行yield()以后,持有的锁是不释放的
- sleep()方法被调用以后,持有的锁是不释放的
- 调动wait()方法之前,必须要持有锁。调用了wait()方法以后,锁就会被释放,当wait方法返回(唤醒)的时候,线程会重新持有锁
- 调动notify()方法之前,必须要持有锁,调用notify()方法本身不会释放锁,只有notify()所在的整个代码块跑完了才释放锁
synchronized
对象锁,锁的是类的对象实例。
类锁 ,锁的是每个类的的Class对象,每个类的的Class对象在一个虚拟机中只有一个,所以类锁也只有一个。
public class SynClzAndInst {
//使用类锁的线程
private static class SynClass extends Thread{
private SynClzAndInst synClzAndInst;
public SynClass(SynClzAndInst synClzAndInst){
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestClass is running..." + synClzAndInst);
synClass(synClzAndInst);
}
}
//使用对象锁的线程
private static class InstanceSyn implements Runnable{
private SynClzAndInst synClzAndInst;
public InstanceSyn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance is running..." + synClzAndInst);
synClzAndInst.instance();
}
}
//使用对象锁的线程
private static class Instance2Syn implements Runnable{
private SynClzAndInst synClzAndInst;
public Instance2Syn(SynClzAndInst synClzAndInst) {
this.synClzAndInst = synClzAndInst;
}
@Override
public void run() {
System.out.println("TestInstance2 is running..." + synClzAndInst);
synClzAndInst.instance2();
}
}
//锁对象
private synchronized void instance(){
SleepTools.second(3);
System.out.println("synInstance is going..." + this.toString());
SleepTools.second(3);
System.out.println("synInstance ended " + this.toString());
}
//锁对象
private synchronized void instance2(){
SleepTools.second(3);
System.out.println("synInstance2 is going..." + this.toString());
SleepTools.second(3);
System.out.println("synInstance2 ended " + this.toString());
}
//类锁,实际是锁类的class对象
private static synchronized void synClass(SynClzAndInst synClzAndInst){
SleepTools.second(1);
System.out.println("synClass going..." + synClzAndInst);
SleepTools.second(1);
System.out.println("synClass end" + synClzAndInst);
}
public static void main(String[] args) {
SynClzAndInst synClzAndInst = new SynClzAndInst();
Thread t1 = new Thread(new InstanceSyn(synClzAndInst));
SynClzAndInst synClzAndInst2 = new SynClzAndInst();
//Thread t2 = new Thread(new Instance2Syn(synClzAndInst));
//t1.start();
//t2.start();
SynClass synClass = new SynClass(synClzAndInst);
synClass.start();
SynClass synClass2 = new SynClass(synClzAndInst2);
synClass2.start();
SleepTools.second(1);
}
}
synchronized的内存语义和它是如何保证并发三大特性的?
-
synchronized实现原子性
synchronized实现原子性需要多个线程之间使用相同的对象锁。这样临界区里所有的代码可以看做一个原子操作。
临界区实际是
monitorenter
和monitorexit
中间包裹的代码块, -
synchronized实现有序性
synchronized实现的只是禁止“工作内存与主内存同步延迟” ,并不会禁止指令重排序
(指令依然会重排序)
为什么synchronized不能禁止指令重排序又能保证有序性?
synchronized是通过
互斥锁
来保证有序性的,同步块里是单线程的,即在单线程下不管怎么重排序,程序的执行结果不能被改变。 -
synchronized实现可见性
线程解锁前,必须把工作内存中共享变量的最新值刷新到主内存中
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值(注意:加锁与解锁需要是同一把锁)
synchronized可重入特性
在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入
。
synchronized的锁对象中有一个计数器(recursions变量),会记录线程获得几次锁
synchronized不可中断特性
当一个线程获得锁后,另一个线程一直处于阻塞或等待状态,前一个线程不释放锁,后一个线程会一直阻塞或等待,不可被中断
synchronized与Lock的区别
-
synchronized是关键字,而Lock是一个接口
-
synchronized当代码块走完后,会自动释放锁,而Lock必须在finally中手动释放锁
-
synchronized是不可中断的,Lock可以中断也可以不中断
-
synchronized可锁住方法和代码块,而Lock只能锁代码块
-
Lock可以使用读锁提高多线程效率
-
synchronized是非公平锁,而 ReentrantLock 可自己选择是否公平锁
synchronized锁升级过程
JDK早期,synchronized叫重量级锁,因为申请资源必须通过内核(用户态与内核态交互
)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8NEtzoT-1631955925732)(C:\Users\admin\Desktop\笔记\img\锁升级过程.png)]
当new一个对象时,如果没有延迟3秒,则表示偏向锁未启动(new出来的为普通对象
),当有轻度竞争锁时,会升级成轻量级锁,如果有重度竞争,则会升级成重量级锁,相反,如果延迟3秒后创建对象并加锁,则会启用偏向锁,只要有多个线程存在竞争,偏向锁将会直接升级成轻量级锁
volatile关键字
//代码演示volatile不能保证其原子性,非线程安全的
public class VolatileUnsafe {
private static class VolatileVar implements Runnable {
private volatile int a = 0;
@Override
public void run() {
String threadName = Thread.currentThread().getName();
a = a++;
System.out.println(threadName+":======" + a);
SleepTools.ms(100);
a = a + 1;
System.out.println(threadName+":======" + a);
}
}
public static void main(String[] args) {
VolatileVar v = new VolatileVar();
Thread t1 = new Thread(v);
Thread t2 = new Thread(v);
Thread t3 = new Thread(v);
Thread t4 = new Thread(v);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
并发工具类
一、CountDownLatch
作用:是一组线程等待其他的线程完成工作以后在执行,加强版join,await用来等待,countDown负责计数器的减一
public class UseCountDownLatch {
static CountDownLatch latch = new CountDownLatch(6);
//初始化线程(只有一步,有4个)
private static class InitThread implements Runnable{
@Override
public void run() {
System.out.println("Thread_"+Thread.currentThread().getId()
+" ready init work......");
latch.countDown();//初始化线程完成工作了,countDown方法只扣减一次;
for(int i =0;i<2;i++) {
System.out.println("Thread_"+Thread.currentThread().getId()
+" ........continue do its work");
}
}
}
//业务线程
private static class BusiThread implements Runnable{
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i =0;i<3;i++) {
System.out.println("BusiThread_"+Thread.currentThread().getId()
+" do business-----");
}
}
}
public static void main(String[] args) throws InterruptedException {
//单独的初始化线程,初始化分为2步,需要扣减两次
new Thread(new Runnable() {
@Override
public void run() {
SleepTools.ms(1);
System.out.println("Thread_"+Thread.currentThread().getId()
+" ready init work step 1st......");
latch.countDown();//每完成一步初始化工作,扣减一次
System.out.println("begin step 2nd.......");
SleepTools.ms(1);
System.out.println("Thread_"+Thread.currentThread().getId()
+" ready init work step 2nd......");
latch.countDown();//每完成一步初始化工作,扣减一次
}
}).start();
new Thread(new BusiThread()).start();
for(int i=0;i<=3;i++){
Thread thread = new Thread(new InitThread());
thread.start();
}
latch.await();
System.out.println("Main do ites work........");
}
}
二、CyclicBarrier
让一组线程达到某个屏障,被阻塞,一直到组内最后一个线程达到屏障时,屏障开放,所有被阻塞的线程会继续运行CyclicBarrier(int parties)
//构造方法,当屏障开放,barrierAction定义的任务会执行
public CyclicBarrier(int parties, Runnable barrierAction) {
}
//CyclicBarrier示例
public class UseCyclicBarrier {
private static CyclicBarrier barrier = new CyclicBarrier(5,new CollectThread());
//存放子线程工作结果的容器
private static ConcurrentHashMap<String,Long> resultMap = new ConcurrentHashMap<>();
public static void main(String[] args) {
for(int i=0;i<=4;i++){
Thread thread = new Thread(new SubThread());
thread.start();
}
}
//负责屏障开放以后的工作
private static class CollectThread implements Runnable{
@Override
public void run() {
StringBuilder result = new StringBuilder();
for(Map.Entry<String,Long> workResult : resultMap.entrySet()){
result.append("["+workResult.getValue()+"]");
}
System.out.println(" the result = "+ result);
System.out.println("do other business........");
}
}
//工作线程
private static class SubThread implements Runnable{
@Override
public void run() {
long id = Thread.currentThread().getId();//线程本身的处理结果
resultMap.put(Thread.currentThread().getId()+"",id);
Random r = new Random();//随机决定工作线程的是否睡眠
try {
if(r.nextBoolean()) {
Thread.sleep(2000+id);
System.out.println("Thread_"+id+" ....do something ");
}
System.out.println(id+"....is await");
barrier.await();
Thread.sleep(1000+id);
System.out.println("Thread_"+id+" ....do its business ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
三、CountDownLatch与CyclicBarrier区别
- countdownlatch放行由第三者控制,CyclicBarrier放行由一组线程本身控制
- countdownlatch放行条件 >= 线程数,CyclicBarrier放行条件 = 线程数
四、Semaphore
控制同时访问某个特定资源的线程数量,用在流量控制
五、Exchange
两个线程间的数据交换
六、Callable、Future和FutureTask
结构图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kqhVOvD9-1631955925732)(C:\Users\admin\Desktop\笔记\img\Future.png)]
方法:
isDone,结束,正常还是异常结束,或者自己取消,返回true;
isCancelled:任务完成前被取消,返回true;
cancel(boolean):
1、 任务还没开始,返回false
2、 任务已经启动,cancel(true),中断正在运行的任务,中断成功,返回true,cancel(false),不会去中断已经运行的任务
3、 任务已经结束,返回false
/**
*类说明:演示Future等的使用
*/
public class UseFuture {
/*实现Callable接口,允许有返回值*/
private static class UseCallable implements Callable<Integer> {
private int sum;
@Override
public Integer call() throws Exception {
System.out.println("Callable子线程开始计算");
Thread.sleep(2000);
for (int i = 0; i < 5000; i++) {
sum = sum + i;
}
System.out.println("Callable子线程计算完成,结果=" + sum);
return sum;
}
}
public static void main(String[] args) throws Exception {
UseCallable useCallable = new UseCallable();
FutureTask<Integer> futureTask = new FutureTask<Integer>(useCallable);
new Thread(futureTask).start();
Random r = new Random();
SleepTools.second(1);
if (r.nextBoolean()) {//随机决定是获得结果还是终止任务
System.out.println("Get UseCallable result = " + futureTask.get());
} else {
System.out.println("中断计算");
futureTask.cancel(true);
}
System.out.println("是否结束:" + futureTask.isDone());
System.out.println("是否任务完成前中断结束:" + futureTask.isCancelled());
}
}
CAS
一、什么是原子操作?如何实现原子操作?
syn基于阻塞的锁的机制,会带来如下问题:
1. 被阻塞的线程优先级很高
2. 拿到锁的线程一直不释放锁怎么办?
- 大量的竞争,消耗Cpu,同时带来死锁或者其他安全。
- CAS的原理:指令级别保证这是一个原子操作
三个运算符: 一个内存地址V,一个期望的值A,一个新值B
基本思路:如果地址V上的值和期望的值A相等,就给地址V赋给新值B,如果不是,不做更新且在
死循环(自旋)里不断的进行CAS操作
//AtomicInteger类 如果期望值一致,返回并更新,否则死循环
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());//10--->11 先获取再自增1
System.out.println(ai.incrementAndGet());//11--->12 先自增1再获取
System.out.println(ai.get());
//打印结果
10 12 12
}
}
-
CAS的问题
-
ABA问题
-
开销问题,CAS操作长期不成功,cpu不断的循环
-
只能保证一个共享变量的原子操作
-
二、Jdk中相关原子操作类的使用
更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
原子更新字段类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
/**
* 类说明:演示带版本戳的原子操作类
* 第一个线程修改值后,版本号自增,第二个线程用老版本号无法进行修改
*/
public class UseAtomicStampedReference {
static AtomicStampedReference<String> asr = new AtomicStampedReference<>("dw", 0);
public static void main(String[] args) throws InterruptedException {
final int oldStamp = asr.getStamp();//拿初始的版本号
final String oldReferenc = asr.getReference();
System.out.println(oldReferenc + "===========" + oldStamp);
Thread rightStampThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ "当前变量值:" + oldReferenc + "当前版本戳:" + oldStamp + "-"
+ asr.compareAndSet(oldReferenc, oldReferenc + "Java",
oldStamp, oldStamp + 1));
}
});
Thread errorStampThread = new Thread(new Runnable() {
@Override
public void run() {
String reference = asr.getReference();
System.out.println(Thread.currentThread().getName()
+ "当前变量值:" + reference + "当前版本戳:" + asr.getStamp() + "-"
+ asr.compareAndSet(reference, reference + "C",
oldStamp, oldStamp + 1));
}
});
rightStampThread.start();
rightStampThread.join();
errorStampThread.start();
errorStampThread.join();
System.out.println(asr.getReference() + "===========" + asr.getStamp());
}
}
显示锁Lock
一、Lock接口和核心方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5PBxxHjm-1631955925733)(C:\Users\admin\Desktop\笔记\img\Lock.png)]
二、Lock接口和synchronized的比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VlxkVzr5-1631955925734)(C:\Users\admin\Desktop\笔记\img\Lock与Sync区别.png)]
Lock应用场景:获取锁可以被中断,超时获取锁,尝试获取锁
三、可重入锁ReentrantLock、所谓锁的公平和非公平
**ReentrantLock是Lock接口的一个实现类**
**可重入:**一个线程在持有一个锁的时候,它内部能否再次(多次)申请该锁。如果一个线程已经获得了锁,其内部还可以多次申请该锁成功。那么我们就称该锁为可重入锁
//可重入锁示例
void methodA(){
lock.lock(); // 获取锁
methodB();
lock.unlock() // 释放锁
}
void methodB(){
lock.lock(); // 获取锁
// 其他业务
lock.unlock();// 释放锁
}
可重入锁可以理解为锁的一个标识。该标识具备计数器功能。标识的初始值为0,表示当前锁没有被任何线程持有。每次线程获得一个可重入锁的时候,该锁的计数器就被加1。每次一个线程释放该所的时候,该锁的计数器就减1。前提是:当前线程已经获得了该锁,是在线程的内部出现再次获取锁的场景
参考: https://www.cnblogs.com/1013wang/p/11820373.html
**公平和非公平:**如果在时间上,先对锁进行获取的请求,一定先被满足,这个锁就是公平的,不满足,就是非公平的,非公平的效率一般来讲更高
/**
* 使用显示锁的范式,释放锁unlock必须在finally代码块中
*/
public class LockDemo {
private Lock lock = new ReentrantLock();
private int count;
public void increament() {
lock.lock();
try {
count++;
}finally {
lock.unlock();
}
}
}
四、ReadWriteLock接口和读写锁ReentrantReadWriteLock
ReentrantLock和synchronized关键字,都是排他锁
**读写锁:**同一时刻允许多个读线程同时访问,但是写线程访问的时候,所有的读和写都被阻塞,最适宜与读多写少的情况
//代码示例 用synchronized与ReadWriteLock展示读写的性能
/**
* 类说明:商品的实体类
*/
public class GoodsInfo {
private final String name;
private double totalMoney;//总销售额
private int storeNumber;//库存数
public GoodsInfo(String name, int totalMoney, int storeNumber) {
this.name = name;
this.totalMoney = totalMoney;
this.storeNumber = storeNumber;
}
public double getTotalMoney() {
return totalMoney;
}
public int getStoreNumber() {
return storeNumber;
}
public void changeNumber(int sellNumber){
this.totalMoney += sellNumber*25;
this.storeNumber -= sellNumber;
}
}
/**
* 类说明:商品的服务的接口
*/
public interface GoodsService {
public GoodsInfo getNum();//获得商品的信息
public void setNum(int number);//设置商品的数量
}
/**
* 类说明:用synchronized内置锁来实现商品服务接口
*/
public class UseSyn implements GoodsService {
private GoodsInfo goodsInfo;
public UseSyn(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public synchronized GoodsInfo getNum() {
SleepTools.ms(5);
return this.goodsInfo;
}
@Override
public synchronized void setNum(int number) {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
}
}
/**
* 类说明:用读写锁来实现商品服务接口
*/
public class UseRwLock implements GoodsService {
private GoodsInfo goodsInfo; //商品实体
private final ReadWriteLock lock = new ReentrantReadWriteLock();//读写锁
private final Lock getLock = lock.readLock();//内部获取读锁
private final Lock setLock = lock.writeLock();//内部获取写锁
public UseRwLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public GoodsInfo getNum() {
getLock.lock();
try {
SleepTools.ms(5);
return this.goodsInfo;
}finally {
getLock.unlock();
}
}
@Override
public void setNum(int number) {
setLock.lock();
try {
SleepTools.ms(5);
goodsInfo.changeNumber(number);
}finally {
setLock.unlock();
}
}
}
/**
* 类说明:测试
*/
public class BusiApp {
static final int readWriteRatio = 10;//读写线程的比例
static final int minthreadCount = 3;//最少线程数
//定义读操作的线程
private static class GetThread implements Runnable{
private GoodsService goodsService;
public GetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
for(int i=0; i < 100; i++){//操作100次
goodsService.getNum();
}
System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:"
+(System.currentTimeMillis()-start)+"ms");
}
}
定义写操作的线程
private static class SetThread implements Runnable{
private GoodsService goodsService;
public SetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Random r = new Random();
for(int i=0; i < 10; i++){ //操作10次
SleepTools.ms(50);
goodsService.setNum(r.nextInt(10));
}
System.out.println(Thread.currentThread().getName()
+"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
GoodsInfo goodsInfo = new GoodsInfo("Cup", 100000, 10000);
//用synchronized与读写锁测试
GoodsService goodsService = new UseRwLock(goodsInfo);/*new UseSyn(goodsInfo);*/
//用三组线程来演示
for(int i = 0;i < minthreadCount; i++){
//每组读写线程的比例为1:10 总共为三组线程,读线程30个,写线程3个
Thread setT = new Thread(new SetThread(goodsService));
for(int j=0;j < readWriteRatio; j++) {
Thread getT = new Thread(new GetThread(goodsService));
getT.start(); //让读线程先启动
}
SleepTools.ms(100);
setT.start();
}
}
}
五、Lock中Condition
1. Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
2. Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
用Lock和Condition实现等待通知
/**
*类说明:用Lock和Condition实现等待通知
*/
public class ExpressCond {
public final static String CITY = "ShangHai";
private int km;/*快递运输里程数*/
private String site;/*快递到达地点*/
private Lock lock = new ReentrantLock();
private Condition kmCond = lock.newCondition(); //里程数Condition条件
private Condition siteCond = lock.newCondition(); //地点Condition条件
public ExpressCond(int km, String site) {
this.km = km;
this.site = site;
}
/* 变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理*/
public void changeKm(){
lock.lock();
try {
this.km = 101;
kmCond.signal();
}finally {
lock.unlock();
}
}
/* 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理*/
public void changeSite(){
lock.lock();
try {
this.site = "BeiJing";
siteCond.signal();
}finally {
lock.unlock();
}
}
/*当快递的里程数大于100时更新数据库*/
public void waitKm(){
lock.lock();
try {
while(this.km<=100) {
try {
kmCond.await();
System.out.println("check km thread["+Thread.currentThread().getId()
+"] is be notifed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
System.out.println("the Km is "+this.km+",I will change db");
}
/*当快递到达目的地时通知用户*/
public void waitSite(){
lock.lock();
try {
while(CITY.equals(this.site)) {
try {
siteCond.await();
System.out.println("check site["+Thread.currentThread().getId()
+"]is be notifed.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}finally {
lock.unlock();
}
System.out.println("the site is "+this.site+",I will call user");
}
}
/**
* 类说明:测试Lock和Condition实现等待通知
*/
public class TestCond {
private static ExpressCond express = new ExpressCond(0,ExpressCond.CITY);
/*检查里程数变化的线程,不满足条件,线程一直等待*/
private static class CheckKm extends Thread{
@Override
public void run() {
express.waitKm();
}
}
/*检查地点变化的线程,不满足条件,线程一直等待*/
private static class CheckSite extends Thread{
@Override
public void run() {
express.waitSite();
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<3;i++){
new CheckSite().start();
}
for(int i=0;i<3;i++){
new CheckKm().start();
}
Thread.sleep(1000);
express.changeKm();//快递里程变化
}
}
AQS
线程池
一、什么是线程池?为什么要用线程池?
二、线程池执行流程
-
当放入线程池中的线程小于corePoolSize时,线程池内部会启动线程去执行,
-
当不小于coolPoolSize时,且小于maximumPool时,线程往阻塞队列里面存,
-
当队列存放失败时,且小于最大线程数maximumPool时,会尝试开启新的线程去执行
-
当上述三条都不满足时,会调用reject方法,执行拒绝策略
三、线程池提交任务
execute:不需要返回
submit: 需要返回
四、各个参数含义
int corePoolSize :线程池中核心线程数,< corePoolSize ,就会创建新线程,= corePoolSize ,这个任务就会保存到BlockingQueue,如果调用prestartAllCoreThreads()方法就会一次性的启动corePoolSize 个数的线程。
int maximumPoolSize,:允许的最大线程数,BlockingQueue也满了,< maximumPoolSize时候就会再次创建新的线程
long keepAliveTime:线程空闲下来后,存活的时间,这个参数只在 > corePoolSize才有用
TimeUnit unit,:存活时间的单位值
BlockingQueue workQueue:保存任务的阻塞队列
ThreadFactory threadFactory,:创建线程的工厂,给新建的线程赋予名字
RejectedExecutionHandler handler :饱和策略
AbortPolicy :直接抛出异常,默认;
CallerRunsPolicy:用调用者所在的线程来执行任务
DiscardOldestPolicy:丢弃阻塞队列里最老的任务,队列里最靠前的任务
DiscardPolicy :当前任务直接丢弃
实现自己的饱和策略,实现RejectedExecutionHandler接口即可
五、预定义的线程池
FixedThreadPoo:
创建固定线程数量的,适用于负载较重的服务器,使用了无界队列
SingleThreadExecutor:
创建单个线程,需要顺序保证执行任务,不会有多个线程活动,使用了无界队列
CachedThreadPool:
会根据需要来创建新线程的,执行很多短期异步任务的程序,使用了SynchronousQueue
ScheduledThreadPoolExecutor:
需要定期执行周期任务,Timer不建议使用了。
六、合理配置线程池
根据任务的性质来:计算密集型(CPU),IO密集型,混合型
计算密集型:机器的Cpu核心数+1,为什么+1,防止页缺失
IO密集型:读取文件,数据库连接,网络通讯, 线程数适当大一点,机器的Cpu核心数 * 2
混合型:尽量拆分,范围在IO密集型~计算密集型之间
十一、Mysql
基础知识
一、Mysql体系架构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hztTgb2M-1631955925740)(C:\Users\admin\Desktop\笔记\img\mysql体系架构.png)]
可以看出 MySQL 最上层是连接组件。下面服务器是由**连接池**、**管理工具和服务**、**SQL 接口**、**解析器**、**优化器**、**缓存**、**存储引擎**、**文件系统**组成。
连接池:由于每次建立建立需要消耗很多时间,连接池的作用就是将这些连 接缓存下来,下次可以直接用已经建立好的连接,提升服务器性能。 管理工具和服务:系统管理和控制工具,例如备份恢复、Mysql 复制、集群 等
SQL 接口:接受用户的 SQL 命令,并且返回用户需要查询的结果。比如 select from 就是调用 SQL Interface
**解析器**: SQL 命令传递到解析器的时候会被解析器验证和解析。解析器主要功能:
a . 将 SQL 语句分解成数据结构,并将这个结构传递到后续步骤,以后 SQL 语句的传递和处理就是基于这个结构的
b. 如果在分解构成中遇到错误,那么就说明这个 sql 语句是不合理的
优化器:查询优化器,SQL 语句在查询之前会使用查询优化器对查询进行优化。
缓存器: 查询缓存,如果查询缓存有命中的查询结果,查询语句就可以直接去查询缓存中取数据。 这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key 缓存, 权限缓存等。
存储引擎 :目前用的最多的就是InnoDB(支持事务)、MyISAM(不支持事务)
文件系统:数据库存储的数据最终都会落地到本地磁盘中
二、MySQL逻辑架构 - 连接层、Server层和存储引擎
连接层:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DCKPyUy-1631955925741)(C:\Users\admin\Desktop\笔记\img\mysql连接层.png)]
当 MySQL 启动(MySQL 服务器就是一个进程) , 等待客户端连接, 每一个客户端连接请求, 服务器都会新建一个线程处理(如果是线程池的话, 则是分配一个空的线程) , 每个线程独立, 拥有各自的内存处理空间
查看数据库最大连接数 :show VARIABLES like '%max_connections%'
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ls2J2EOX-1631955925741)(C:\Users\admin\Desktop\笔记\img\mysql连接验证.png)]
连接到服务器, 服务器需要对其进行验证, 也就是用户名、 IP、 密码验证,一旦连接成功, 还要验证是否具有执行某个特定查询的权限(例如, 是否允许客户端对某个数据库某个表的某个操作)
Service层:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LcciNO5E-1631955925742)(C:\Users\admin\Desktop\笔记\img\service层.png)]
这一层主要功能有: SQL 语句的解析、 优化, 缓存的查询, MySQL 内置函数的实现, 跨存储引擎功能(所谓跨存储引擎就是说每个引擎都需提供的功能(引擎需对外提供接口)) , 例如: 存储过程、 触发器、 视图等。
1.如果是查询语句(select 语句) , 首先会查询缓存是否已有相应结果, 有则返回结果, 无则进行下一步(如果不是查询语句, 同样调到下一步)
2.解析查询, 创建一个内部数据结构(解析树) , 这个解析树主要用来 SQL语句的语义与语法解析;
3.优化 SQL 语句, 例如重写查询, 决定表的读取顺序, 以及选择需要的索引等。 这一阶段用户是可以查询的, 查询服务器优化器是如何进行优化的,便于用户重构查询和修改相关配置, 达到最优化。 这一阶段还涉及到存储引擎,优化器会询问存储引擎, 比如某个操作的开销信息、 是否对特定索引有查询优化等
从 8.0 开始, MySQL 不再使用查询缓存,因为无法预测其性能,且会导致大量的互斥锁争用
三、MySQL 官方引擎
InnoDB
:InnoDB 是 MySQL 的默认事务型引擎, 也是最重要、 使用最广泛的存储引擎。
MylSAM
: 在 MySQL 5.1 及之前的版本, MyISAM 是默认的存储引擎。 MyISAM 提供了大量的特性, 包括全文索引、 压缩、 空间函数(GIS) 等, 但 MyISAM 不支持事务和行级锁, 而且有一个毫无疑问的缺陷就是崩溃后无法安全恢复
查看mysql提供的引擎:show engines;
查看mysql默认的存储引擎:show variables like '%storage_engine%';
四、MyISAM 和 InnoDB 比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GfrO2jUx-1631955925742)(C:\Users\admin\Desktop\笔记\img\引擎比较.png)]
五、MySQL 中的数据目录
查看mysql中的数据目录:show variables like 'datadir'; --这个目录可以通过配置文件进行修改
对于 InnoDB 数据会以xxx.frm(存放元数据)
与xxx.idb(存表数据和索引)
格式存放
对于 MyISAM 数据和索引是分开存放的 ,以xxx.frm(存放元数据)
、xxx.MYI(存放索引)
、xxx.MYD(存表数据)
格式存放
六、日志文件
常见的日志文件有: 错误日志(error log) 、 慢查询日志(slow query log) 、查询日志(query log) 、 二进制文件(bin log) 。
错误日志
: 错误日志文件对 MySQL 的启动、 运行、 关闭过程进行了记录。 遇到问题时应该首先查看该文件以便定位问题。 该文件不仅记录了所有的错误信息, 也记录一些警告信息或正确的信息
查看错误日志文件的位置:show variables like 'log_error';
查询日志
: 查询日志记录了所有对 MySQL 数据库请求的信息, 无论这些请求是否得到了正确的执行。默认文件名: 主机名.log
二进制日志(binlog)
: 二进制日志记录了对 MySQL 数据库执行更改的所有操作, 若操作本身没有导致数据库发生变化, 该操作可能也会写入二进制文件。 但是不包括 select 和show 这类操作(因为这些操作对数据本身不会进行修改)
MySQL 中的系统库
系统库简介
performance_schema
: 这个数据库里主要保存 MySQL 服务器运行过程中的一些状态信息, 算是对MySQL 服务器的一个性能监控。 包括统计最近执行了哪些语句, 在执行过程的每个阶段都花费了多长时间, 内存的使用情况等等信息。
information_schema
: 这个数据库保存着 MySQL 服务器维护的所有其他数据库的信息, 比如有哪些表、 哪些视图、 哪些触发器、 哪些列、 哪些索引。 这些信息并不是真实的用户数据, 而是一些描述性信息, 有时候也称之为元数据。
sys
: 这个数据库主要是通过视图的形式把 information_schema 和performance_schema 结合起来, 让程序员可以更方便的了解 MySQL 服务器的一些性能信息。
mysql
: 这个数据库核心, 它存储了 MySQL 的用户账户和权限信息, 一些存储过程、事件的定义信息, 一些运行过程中产生的日志信息, 一些帮助信息以及时区信息等。
Mysql中的事务
事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位(不可再进行分割),由一个有限的数据库操作序列构成(多个DML语句,select语句不包含事务),要不全部成功,要不全部不成功。
事务特性
事务应该具有 4 个属性: 原子性、 一致性、 隔离性、 持久性。 这四个属性通常称为 ACID 特性。
原子性(atomicity)
一致性(consistency)
隔离性(isolation)
持久性(durability)
原子性(atomicity)
一个事务必须被视为一个不可分割的最小单元, 整个事务中的所有操作要么全部提交成功, 要么全部失败, 对于一个事务来说, 不能只执行其中的一部分操作。
一致性(consistency)
一致性是指事务将数据库从一种一致性转换到另外一种一致性状态, 在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏。
例子:A账户给B账户转账1000元
1.A工资卡扣除 1000 元
2.B工资卡增加 1000
扣除的钱(-1000) 与增加的钱(1000) 相加应该为 0, 或者说 A和B账户的钱加起来, 前后应该不变。
持久性(durability)
一旦事务提交,所做的修改就会永久保存到数据库中。 此时即使系统崩溃, 已经提交的修改数据也不会丢失。
隔离性(isolation)
一个事务的执行不能被其他事务干扰。 即一个事务内部的操作及使用的数据对并发的其他事务是隔离的, 并发执行的各个事务之间不能互相干扰。
如果隔离性不能保证, 会导致什么问题?
例:A给B转账两次, 每次都是 500, A卡里开始有 1200, B卡里开始有 300, 从理论上转完后, A卡里有 200, B卡里应该有 1300。
我们将同时进行的两次转账操作分别称为 T1 和 T2, 在现实世界中 T1 和 T2 是应该没有关系的, 可以先执行完 T1, 再执行 T2, 或者先执行完 T2, 再执行 T1, 结果都是一样的。 但是很不幸, 真实的数据库中 T1 和T2 的操作可能交替执行的, 执行顺序就有可能是:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gb5rZQnI-1631955925743)(C:\Users\admin\Desktop\笔记\img\隔离性.png)]
对数据库操作来说, 不仅要保证这些操作以原子性的方式执行完成, 而且要保证其它的状态转换不会影响到本次状态转换, 这个规则被称之为隔离性。
事务并发引发的问题
脏读
当一个事务读取到了另外一个事务修改但未提交的数据, 被称为脏读
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ko0eEq9u-1631955925743)(C:\Users\admin\Desktop\笔记\img\脏读.png)]
不可重复读
当事务内相同的记录被检索两次, 且两次得到的结果不同时, 此现象称为不可重复读
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VKzo8BYQ-1631955925744)(C:\Users\admin\Desktop\笔记\img\不可重复读.png)]
事务 2 对记录做了修改并提交成功, 这意味着修改的记录对其他事务是可见的,因此事务 1 两次读取的 money 值不同
幻读
在事务执行过程中, 另一个事务将新记录添加到正在读取的事务中时, 会发生幻读。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJfU8rgC-1631955925744)(C:\Users\admin\Desktop\笔记\img\幻读.png)]
如果事务 2 中是删除了符合的记录而不是插入新记录,那事务 1 中之后再根据条件读取的记录变少了, 这种现象算不算幻读呢? 明确说一下, 在 MySQL 中这种现象不属于幻读, 幻读强调的是一个事务按照某个相同条件多次读取记录时, 后读取时读到了之前没有读到的记录。
SQL 标准中的四种隔离级别
脏读 > 不可重复读 > 幻读
隔离级别越低, 越严重的问题就越可能发生(未提交读级别最低
)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v0s7lwIH-1631955925745)(C:\Users\admin\Desktop\笔记\img\sql隔离级别.png)]
MySQL 中的隔离级别
不同的数据库厂商对 SQL 标准中规定的四种隔离级别支持不一样, 比方说Oracle 就只支持 READ COMMITTED 和 SERIALIZABLE 隔离级别。 MySQL 虽然支持 4 种隔离级别, 但与 SQL 标准中所规定的各级隔离级别允许发
生的问题却有些出入, MySQL 在 REPEATABLE READ 隔离级别下, 是可以禁止幻读问题的发生的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-16oZwUtF-1631955925745)(C:\Users\admin\Desktop\笔记\img\mysql隔离级别.png)]
MySQL 的默认隔离级别为 REPEATABLE READ
, 我们可以手动修改事务的隔离级别。
设置事务的隔离级别
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL READ COMMITTED;
使用 GLOBAL 关键字(在全局范围影响):
如:SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
只对执行完该语句之后产生的会话起作用。 当前已经存在的会话无效。
使用 SESSION 关键字(在会话范围影响):
如:SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
对当前会话的所有后续的事务有效,该语句可以在已经开启的事务中间执行, 但不会影响当前正在执行的事务。如果在事务之间执行, 则对后续的事务有效。
上述两个关键字都不用(只对执行语句后的下一个事务产生影响) :
如:SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
只对当前会话中下一个即将开启的事务有效。下一个事务执行完后,后续事务将恢复到之前的隔离级别。
该语句不能在已经开启的事务中间执行,会报错的。
如果我们在服务器启动时想改变事务的默认隔离级别, 可以修改启动参数transaction-isolation 的值, 比方说我们在启动服务器时指定了 --transaction-isolation=SERIALIZABLE, 那么事务的默认隔离级别就从原来的可重复读REPEATABLE READ 变成了 SERIALIZABLE。
想要查看当前会话默认的隔离级别可以通过查看系统变量 transaction_isolation
的值来确定:
SHOW VARIABLES LIKE 'transaction_isolation';
高性能索引
索引: 索引(Index) 是帮助 MySQL 高效获取数据的数据结构。
索引的本质: 索引是数据结构
索引的作用:高效获取数据(尽可能的多让数据顺序读写,少让数据随机读写,减少IO消耗
)
InnoDB 存储引擎支持以下几种常见的索引: B+树索引、 全文索引、 哈希索引, 其中比较关键的是 B+树索引
HashMap 适合做数据库索引吗?
- hash 表只能匹配是否相等, 不能实现范围查找;
- 当需要按照索引进行 order by 时, hash 值没办法支持排序;
- 组合索引可以支持部分索引查询, 如(a,b,c)的组合索引, 查询中只用到了a和 b 也可以查询的, 如果使用 hash 表, 组合索引会将几个字段合并 hash, 没办法支持部分索引;
- 当数据量很大时, hash 冲突的概率也会非常大。
B+Tree索引
B+树索引就是传统意义上的索引, 这是目前关系型数据库系统中查找最常用和最为有效的索引。 B+树索引的构造类似于二叉树, 根据键值(Key Value) 快速找到数据。 注意 B+树中的 B 不是代表二叉(binary), 而是代表平衡(balance), 因为 B+树是从最早的平衡二叉树、B树演化而来, 但是 B+树不是一个二叉树。
B+树是 B 树的一种变形形式, B+树上的叶子结点存储关键字以及相应记录的地址, 叶子结点以上各层作为索引使用,一棵 m 阶的 B+树定义如下:
-
每个节点最多可以有 m 个元素;
-
除了根节点外, 每个节点最少有 (m/2) 个元素;
-
如果根节点不是叶子节点, 那么它最少有 2 个孩子节点;
-
所有的叶子节点都在同一层;
-
一个有 k 个孩子节点的非叶子节点有 (k-1) 个元素, 按升序排列;
-
某个元素的左子树中的元素都比它小, 右子树的元素都大于或等于它;
-
非叶子节点只存放关键字和指向下一个孩子节点的索引, 记录只存放在叶子节点中;
-
相邻的叶子节点之间用指针相连。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hubk8tX4-1631955925746)(C:\Users\admin\Desktop\笔记\img\B+树.png)]
B+树的几个特征 :
- 相同节点数量的情况下, B+树高度远低于平衡二叉树;
- 非叶子节点只保存索引信息和下一层节点的指针信息, 不保存实际数据记录;
- 每个叶子页(LeafPage) 存储了实际的数据, 比如上图中每个叶子页就存放了 3 条数据记录, 当然可以更多, 叶子节点由小到大(有序) 串联在一起,叶子页中的数据也是排好序的;
- 索引节点指示该节点的左子树比这个索引值小, 而右子树大于等于这个索引值。
索引列的值在内部由小到大排好了序,根据索引的最左匹配列依次排序
聚集索引/聚簇索引
InnoDB 中使用了聚集索引, 就是将表的主键用来构造一棵 B+树, 并且将整张表的行记录数据存放在该 B+树的叶子节点中。 也就是所谓的索引即数据, 数据即索引。 由于聚集索引是利用表的主键构建的, 所以每张表只能拥有一个聚集索引。
优点 : 通过聚集索引能获取完整的整行数据。对于主键的排序查找和范围查找速度非常快。
如果我们没有定义主键, MySQL 会使用唯一性索引, 没有唯一性索引,MySQL 也会创建一个隐含列 RowID 来做主键, 然后用这个主键来建立聚集索引。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0E8ltmJW-1631955925746)(C:\Users\admin\Desktop\笔记\img\聚集索引.png)]
辅助索引/二级索引
所谓辅助索引(也称二级索引、 非聚集索引),就是非主键索引,用普通的列创建的索引
对于辅助索引, 叶子节点并不包含行记录的全部数据。而是包含键值及主键,比如辅助索引列为note
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BaCq4gDi-1631955925746)(C:\Users\admin\Desktop\笔记\img\辅助索引.png)]
回表
要查询的列在辅助索引中不存在,必须通过辅助索引找到主键,再用主键找聚集索引获取全部的列数据
辅助索引的存在并不影响数据在聚集索引中的组织, 因此每张表上可以有多个辅助索引。 当通过辅助索引来寻找数据时, InnoDB 存储引擎会遍历辅助索引,并通过叶级别的指针获得指向主键索引的主键, 然后再通过主键索引来找到一个完整的行记录。 这个过程也被称为回表。 也就是根据辅助索引的值查询一条完整的用户记录需要使用到 2 棵 B+树----一次辅助索引, 一次聚集索引。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qKaJOa64-1631955925747)(C:\Users\admin\Desktop\笔记\img\回表.png)]
组合索引/联合索引/复合索引
构建索引有多个字段组成,我们称之为组合索引、联合索引或者复合索引, 比如 index(a,b)就是将 a,b 两个
列组合起来构成一个索引。
千万要注意一点, 建立联合索引只会建立 1 棵 B+树, 多个列分别建立索引,会分别以每个列建立 B+树, 有几个索引就有几个 B+树, 比如, index(note)、index(b), 就分别对 note,b 两个列各构建了一个索引。
index(note, b)在索引构建上, 包含了两个意思:
- 先把各个记录按照 note 列进行排序。
- 在记录的 note 列相同的情况下, 采用 b 列进行排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e4oV5HFM-1631955925747)(C:\Users\admin\Desktop\笔记\img\组合索引.png)]
覆盖索引/索引覆盖
InnoDB 存储引擎支持覆盖索引(covering index, 或称索引覆盖), 即从辅助索引中就可以得到查询的记录, 而不需要回表操作。 使用覆盖索引的一个好处是辅助索引不包含整行记录的所有信息, 故其大小要远小于聚集索引, 因此可以减少大量的 IO 操作。 覆盖索引是一种概念,并不是索引类型的一种。
如图针对note、b列建立组合索引,而select语句中只查询b,在组合索引中已经包含了b,此时只查一次组合索引就可以得到数据,不需要回表,就称之为覆盖索引
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0z3QyjbM-1631955925748)(C:\Users\admin\Desktop\笔记\img\覆盖索引.png)]
自适应哈希索引
InnoDB 存储引擎除了我们前面所说的各种索引, 还有一种自适应哈希索引,我们知道 B+树的查找次数,取决于 B+树的高度,在生产环境中,B+树的高度一般为 3~4 层,故需要 3~4 次的 IO 查询。
所以在 InnoDB 存储引擎内部自己去监控索引表, 如果监控到某个索引经常用, 那么就认为是热数据, 然后内部自己创建一个 hash 索引, 称之为自适应哈希索引( Adaptive Hash Index,AHI), 创建以后, 如果下次又查询到这个索引,那么直接通过 hash 算法推导出记录的地址, 直接一次就能查到数据, 比重复去B+tree 索引中查询三四次节点的效率高了不少。
对于自适应哈希索引仅是数据库自身创建并使用的, 我们并不能对其进行干预。 通过命令 :
show engine innodb status\G 可以看到当前自适应哈希索引的使用状况,
全文检索之倒排索引
什么是全文检索(Full-Text Search) ? 它是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。 它可以根据需要获得全文中有关章、节、段、 句、词等信息, 也可以进行各种统计和分析。 我们比较熟知的 Elasticsearch、Solr 等就是全文检索引擎, 底层都是基于 Apache Lucene 的。
全文检索中倒排索引解释见Elasticsearch章节
深入思考索引在查询中的使用
索引在查询中的作用到底是什么? 在我们的查询中发挥着什么样的作用呢?请记住:
- 一个索引就是一个 B+树, 索引让我们的查询可以快速定位扫描到我们需要的数据记录上, 加快查询的速度。
- 一个 select 查询语句在执行过程中一般最多能使用一个二级索引, 即使在 where 条件中用了多个二级索引。
扫描区间
所谓扫描区间就是根据索引已经排好的顺序,在where条件中只过滤一个范围再进行查找。 由于 B+树叶子节点中的记录是按照索引列值由小到大的顺序排序的, 所以即使只扫描某个区间或者某些区间中的记录也可以明显减
少需要扫描的记录数量。 比如下面这个查询语句:
SELECT * FROM order_exp WHERE id >= 3 AND id<= 99;
这个语句其实是想查找 id 值在[3,99]区间中的所有聚簇索引记录。 我们定位到 id 值为 3 的那条聚簇索引记录, 然后沿着记录所在的单向链表向后扫描,直到某条聚簇索引记录的 id 值不在[3,99]区间中为止。 与全表扫描相比, 扫描 id 值在[3,99]区间中的记录已经很大程度地减少了需要扫描的记录数量, 所以提升了查询效率。
再看下面这个查询语句:
SELECT * FROM order_exp WHERE id in(3,9) OR (id>=23 AND id<= 99);
这里有几个扫描区间? 三个, 两个单独扫描区间[3,3]、 [9,9], 一个范围扫描区间[23,99]。
再看下面这个查询语句以及索引列信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AaRdWtJ3-1631955925748)(C:\Users\admin\Desktop\笔记\img\扫描区间索引.png)]
SELECT * FROM order_exp WHERE order_no < ‘DD00_10S’ AND expire_time >
‘2021-03-22 18:28:28’ AND order_note > ‘7 排’;
-
这个语句里, order_no 和 expire_time 都有索引, order_note 没有索引, 一个 Select 查询语句在执行过程中一般最多能使用一个二级索引。 那么也就是说:
-
如果用order_no列的索引执行查询, 那扫描区间就是[第一条记录, ‘DD00_10S’],expire_time> ‘2021-03-22 18:28:28’ AND order_note > '7 排’只能成为普通的搜索或者说判定条件。
-
如果说用 idx_expire_time 执行查询, 那扫描区间就是[‘2021-03-22 18:28:28’,最后一条记录],order_no <‘DD00_10S’ AND order_note > ‘7 排’ 只能成为普通的搜索或者说判定条件。无论用哪个索引执行查询, 都需要获取到索引中的记录后, 进行回表, 获取到完整的用户记录后再根据判定条件判断这条记录是否满足 SQL 语句的要求
范围区间扫描
其实对于 B+树索引来说, 只要索引列和常数使用=、 <=>、 IN、 NOT IN、 IS NULL、IS NOT NULL、 >、 <、 >=、 <=、 BETWEEN、 !=(不等于也可以写成<>) 或者 LIKE操作符连接起来, 就可以产生一个区间。
1、 IN 操作符的效果和若干个等值匹配操作符=
之间用OR
连接起来是一样的, 也就是说会产生多个单点区间, 比如下边这两个语句的效果是一样的:
SELECT * FROM order_exp WHERE insert_time IN (2021-03-22 18:23:42, yyyy);
SELECT * FROM order_exp WHERE insert_time= 2021-03-22 18:23:42 OR insert_time = yyyy;
2、 !=产生的扫描区间呢? 比如
SELECT * FROM order_exp WHERE order_ no != ‘DD00_9S’
此时使用 idx_expire_time 执行查询时对应的扫描区间就是[第一条记录 ,'DD00_9S']
和[ 'DD00_9S',最后一条记录]
。
3、 LIKE 操作符比较特殊, 只有在匹配完整的字符串或者匹配字符串前缀时才产生合适的扫描区间。
对于某个索引列来说, 字符串前缀相同的记录在由记录组成的单向链表中肯定是相邻的。 比如我们有一个搜索条件是 note LIKE' b%'
, 对于二级索引 idx_note
来说, 所有字符串前缀为’b’的二级索引记录肯定是相邻的。 这也就意味着我们只要定位到 idx_note
值的字符串前缀为'b'
的第一条记录, 就可以沿着记录所在的单向链表向后扫描, 直到某条二级索引记录的字符串前缀不为 b
为止。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-226CrlvH-1631955925749)(C:\Users\admin\Desktop\笔记\img\范围区间扫描.png)]
很显然, note LIKE' b%'
形成的扫描区间相当于['b', 'c')
。
范围扫描索引破坏
- 使用and的情况:
SELECT * FROM order_exp WHERE expire_time> ‘2021-03-22 18:35:09’ AND order_note = ‘abc’;
请注意, 这个查询语句中能利用的索引只有 idx_expire_time
一个, 在使用二级索引 idx_expire_time
定位记录的阶段用不到 order_note = 'abc'
这个条件,这个条件是在回表获取了完整的用户记录后才使用的, 所以在确定范围区间的时候不需要考虑 order_note = 'abc'
这个条件。
最终范围区间就是:('2021-03-2218:35:09', 最后一条记录)
。
- 使用 OR 的情况:
SELECT * FROM order_exp WHERE expire_time> ‘2021-03-22 18:35:09’ OR order_note = ‘abc’;
这条语句在搜索时可以化简为:SELECT * FROM order_exp ;
这也就说如果我们使用 idx_expire_time 执行查询的话, 对应的范围区间就是[第一条记录,最后一条记录], 也就是需要将全部二级索引的记录进行回表, 这个代价肯定比直接全表扫描都大了,所以mysql在内部优化选择时,宁愿不走索引改为走全表扫描
也就是说一个使用到索引的搜索条件和没有使用该索引的搜索条件使用 OR 连接起来后是无法使用该索引的。为什么? 道理很简单,idx_expire_time
这个二级索引的记录中不包含 order_note
这个字段, 那就是说, 即使二级索引 idx_expire_time
中找到了满足 expire_time> '2021-03-2218:35:09'
的记录, 是无法判定 order_note
是否满足 order_note = 'abc'
的, 又因为是 OR 条件, 所以必须要在主键索引中从第一条记录到最后一条记录逐条判定order_note 是否等于 ‘abc’
MyISAM 中的索引
1、MyISAM 的索引方案虽然也使用树形结构, 但是却将索引和数据分开存储的。
2、MyISAM 将表中的记录按照记录的插入顺序单独存储在一个文件中, 称之为数据文件。 这个文件并不划分为若干个数据页, 有多少记录就往这个文件中塞多少记录。 我们可以通过行号而快速访问到一条记录。
3、由于在插入数据的时候并没有刻意按照主键大小排序, 所以我们并不能在这些数据上使用二分法进行查找
4、使用 MyISAM 存储引擎的表会把索引信息另外存储到一个称为索引文件的另一个文件中。 MyISAM 会单独为表的主键创建一个索引
, 只不过在索引的叶子节点中存储的不是完整的用户记录, 而是主键值+行号的组合
。 也就是先通过索引找到对应的行号, 再通过行号去找对应的记录!
5、这一点和 InnoDB 是完全不相同的, 在 InnoDB 存储引擎中, 我们只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录, 而在 MyISAM 中却需要进行一次回表操作(用主键索引找行号,用行号查需要的数据)
, 意味着 MyISAM 中建立的索引相当于全部都是二级索引
!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fde0S3tW-1631955925749)(C:\Users\admin\Desktop\笔记\img\MyISAM文件结构.png)]
上图中,.frm
存放元数据,.MYD
存放表数据,.MYI
存放MyISAM表的索引信息
索引的代价
空间上的代价:
每建立一个索引都要为它建立一棵 B+树, 每一棵 B+树的每一个节点都是一个数据页, 一个页默认会占用 16KB 的存储空间, 一棵很大的 B+树由许多数据页组成会占据很多的存储空间。
时间上的代价:
每次对表中的数据进行增、 删、 改操作时, 都需要去修改各个 B+树索引, 所以存储引擎需要
额外的时间进行一些记录移位, 页面分裂、 页面回收的操作来维护好节点和记录的排序。 如果我们建了许多索引, 每个索引对应的 B+树都要进行相关的维护操作, 这必然会对性能造成影响
高性能的索引创建策略
1、索引列的类型尽量小
2、索引选择性和前缀索引,创建索引应该选择选择性/离散性高的列。 索引的选择性/离散性是指, 不重
复的索引值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUyjBfsu-1631955925750)(C:\Users\admin\Desktop\笔记\img\离散型.png)]
哪列做为索引字段最好? 当然是姓名字段, 因为里面的数据没有任何重复,、性别字段是最不适合做索引的, 因为数据的重复度非常高。
3、前缀索引
4、只为用于搜索、排序或分组的列创建索引
5、多列索引
6、设计三星索引
面试题
- 一张表,里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录, 再把 Mysql 重启,再 insert 一条记录,这条记录的 ID 是 18 还是 15 ?
- 如果表的类型是 MyISAM,那么是 18
因为 MyISAM 表会把自增主键的最大 ID 记录到数据文件里,重启 MySQL 自增主键的最大 ID 也不会丢失
- 如果表的类型是 InnoDB,那么是 15
InnoDB 表只是把自增主键的最大 ID 记录到内存中,所以重启数据库或者是对表进行 OPTIMIZE 操作,都会导致最大 ID 丢失
-
optimize table 表名; 进行操作后,就相当对表进行碎片处理,使表的空间大大的减少了
只对MyISAM, BDB和InnoDB引擎表起作用,且在优化中时会锁定表
-
与 Oracle 相比,Mysql 有什么优势?
Mysql 是开源软件,随时可用,无需付费。
Mysql 是便携式的
带有命令提示符的 GUI。
使用 Mysql 查询浏览器支持管理
-
如何区分 FLOAT 和 DOUBLE?
浮点数以 8 位精度存储在 FLOAT 中,并且有四个字节。
浮点数存储在 DOUBLE 中,精度为 18 位,有八个字节。
-
Mysql 中 InnoDB 支持的四种事务隔离级别, 以及逐级之间的区别?
- Read Uncommitted(读取未提交内容): 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称 之为脏读(Dirty Read)。
- Read Committed(读取提交内容):这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一 个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。
- Repeatable Read(可重读):这是 MySQL 的默认事务隔离级 别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现 有新的“幻影” 行。InnoDB 和 Falcon 存储引擎通过多版本并发控制 (MVCC,Multiversion Concurrency Control 间隙锁)机制解决了 该问题。注:其实多版本只是解决不可重复读问题,而加上间隙锁(也 就是它这里所谓的并发控制)才解决了幻读问题。
- Serializable(可串行化): 这是最高的隔离级别,它通过强制事务 排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
-
事务四个特征
- 原子性(atomicity) : 一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性
- 一致性(consistency) : 事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。
- 隔离性(isolation):事务的隔离性是指在并发环境中,并发的事务是相互隔离的,一个事务的执行不能被其他事务干扰。不同的事务并发操作相同的数据时,每个事务都有各自完成的数据空间,即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能相互干扰。
- 持久性(durability) : 一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中
-
mysql支持哪些存储引擎?
mysql5.6支持的存储引擎包括
InnoDB、MyISAM、MEMORY等
InnoDB支持事务,MyISAM不支持事务, MEMORY将数据存在内存 ,默认使用hash索引
-
Mysql 的默认存储引擎
Mysql在V5.1之前默认存储引擎是MyISAM;在此之后默认存储引擎是InnoDB
-
主键和候选键有什么区别?
表格的每一行都有主键唯一标识,一个表只有一个主键。 主键也是候选键。按照惯例,候选键可以被指定为主键,并且可以用于任何外键引用。
-
列设置为 AUTO INCREMENT 时,如果在表中达到最大值,会发生什么情况?
它会停止递增,任何进一步的插入都将产生错误,因为密钥已被使用。
-
LIKE 声明中的%和 _ 是什么意思?
%对应于 0 个或更多字符,_ 只是 LIKE 语句中的一个字符。
-
BLOB 和 TEXT 有什么区别?
BLOB 是一个二进制对象,可以容纳可变数量的数据
BLOB 值进行排序和比较时区分大小写,对 TEXT 值不区分大小写。
-
MyISAM 表格将在哪里存储,并且还提供其存储格式?
每个 MyISAM 表格以三种格式存储在磁盘上:
-
“.frm”文件存储表定义 元数据
-
数据文件具有“.MYD”(MYData)
-
索引文件具有“.MYI”(MYIndex)
-
可以使用多少列创建索引?
任何标准表最多可以创建 16 个索引列 ,实际中建议不超过五个
-
NOW()和 CURRENT_DATE()有什么区别?
NOW()命令用于显示当前年份,月份,日期,小时,分钟和秒。
CURRENT_DATE()仅显示当前年份,月份和日期。
-
MYSQL 数据表在什么情况下容易损坏?
服务器突然断电导致数据文件损坏。
强制关机,没有先关闭 mysql 服务等
-
Mysql 中有哪几种锁
MyISAM 支持表锁,InnoDB 支持表锁和行锁,默认为行锁
表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低
行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高
-
常用的索引有哪些种类?
- 普通索引: 即针对数据库表某列创建索引
- 唯一索引: 与普通索引类似,不同的就是:MySQL 数据库索引列的值 必须唯一,但允许有空值
- 主键索引: 它是一种特殊的唯一索引,不允许有空值。一般是在建表的 时候同时创建主键索引
- 组合索引: 为了进一步榨取 MySQL 的效率,就要考虑建立组合索引。 即将数据库表中的多个字段联合起来作为一个组合索引。
- mysql 数据库中索引的工作机制是什么?
数据库索引,是数据库管理系统中一个排序的数据结构,减少io消耗,以协助快速查询。
索引的实现通常使用 B+树
-
MySQL 中 InnoDB 引擎的行锁是通过什么实现的?
InnoDB 行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!
-
*[SELECT ] 和 [SELECT 全部字段] 的 2 种写法有何优缺点?
- 前者要解析数据字典,后者不需要
- 结果输出顺序,前者与建表列顺序相同,后者按指定字段顺序。
- 表字段改名,前者不需要修改,后者需要改
- 后者可以建立索引进行优化,前者无法优化
- 后者的可读性比前者要高
-
HAVNG 子句 和 WHERE 的异同点?
- 语法上:where 用表中列名,having 用 select 结果别名
- 影响结果范围:where 从表读出数据的行数,having 返回客户端的行 数
- 索引:where 可以使用索引,having 不能使用索引,只能在临时结果 集操作
- where 后面不能使用聚集函数,having 是专门使用聚集函数的。
- 为什么 MySQL 的索引要使用 B+树而不是 B 树?
B 树和 B+树的最大区别就是, B 树不管叶子节点还是非叶子节点, 都会保存数据, 这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出) , 指针少的情况下要保存大量数据, 只能增加树的高度, 导致 IO 操作变多,查询性能变低。
十二、分库分表
基础知识
十三、Nginx
面试题
-
请解释一下什么是 Nginx?
Nginx 是一个 web 服务器和反向代理服务器,用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。
-
请列举 Nginx 的一些特性 ?
- 反向代理
- 负载均衡器
- 高可用性和伸缩性
- 动静分离
-
请列举 Nginx 和 Apache 之间的不同点
Nginx 是轻量级web服务器,Nginx 比 Apache 占用更少的内存及资源
Nginx 处理请求是异步非阻塞的,抗并发性比较好,而 Apache 则是阻塞型的,
在高并发下 Nginx 能保持低资源低消耗高性能。
Nginx 对所有的请求都有一个线程处理,Apache 单个线程处理单个请求
核心的区别在于 Apache 是同步多进程模型,一个连接对应一个进程;Nginx 是异步的,多个连接(万级别)可以对应一个进程。
- 请解释 Nginx 如何处理 HTTP 请求。
首先,Nginx 在启动时,会解析配置文件,得到需要监听的端口与 IP 地址,然后在 Nginx 的 Master 进程里面先初始化好这个监控的Socket
然后,再 fork(一个现有进程可以调用 fork 函数创建一个新进程。由 fork 创建的新进程被称为子进程 )出多个子进程出来。
之后,子进程会竞争 accept 新的连接。此时,客户端就可以向 nginx 发起连接了。当客户端与nginx进行三次握手,与 nginx 建立好一个连接后。
此时,某一个子进程会 accept 成功,得到这个建立好的连接的 Socket ,然后创建 nginx 对连接的封装,即 ngx_connection_t 结构体。
接着,设置读写事件处理函数,并添加读写事件来与客户端进行数据的交换。
最后,Nginx 或客户端来主动关掉连接
-
使用“反向代理服务器”的优点是什么?
反向代理服务器可以隐藏源服务器的存在和特征。使用安全
提高访问速度
防火墙作用 :由于所有的客户机请求都必须通过代理服务器访问远程站点,因此可在代理服务器上设限,过滤某些不安全信息
通过代理服务器访问不能访问的目标站点:通俗说,我们使用的翻墙浏览器就是利用了代理服务器,可直接访问外网。
-
请解释 Nginx 服务器上的 Master 和 Worker 进程分别是什么?
Master 进程:读取及评估配置和维持
Worker 进程:处理请求
-
如何通过不同于 80 的端口开启 Nginx?
为了通过一个不同的端口开启 Nginx,你必须进入/etc/Nginx/sitesenabled/,如果这是默认文件,那么你必须打开名为“default”的文件。编辑文件,并放置在你想要的端口: Like server { listen 81; }
-
请陈述 stub_status 和 sub_filter 指令的作用是什么?
Stub_status 指令:该指令用于了解 Nginx 当前状态的当前状态,如当前的活动连接,接受和处理当前读/写/等待连接的总数
Sub_filter 指令:它用于搜索和替换响应中的内容,并快速修复陈旧的数据
-
Nginx负载均衡的5种策略及原理
-
轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
-
指定权重
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
-
IP绑定 ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
-
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。 -
url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
-
十四、ES
基础知识
REST
RESTful 指的是一组架构约束条件和原则,就是用来规范我们的 Http 接口API 的一种约束
RESTful 架构
1.架构里, 每一个 URI 代表一种资源;
2.客户端和服务器之间, 传递这种资源的某种表现层;
3.客户端通过四个 HTTP 动词(get、post、 put、 delete) 对服务器端资源进行操作, 实现” 表现层状态转化”
注意: REST 架构风格并不是绑定在 HTTP 上, 只不过目前 HTTP 是唯一与 REST相关的实例。 所以我们这里描述的 REST 也是通过 HTTP 实现的 REST。
十五、Linux
常用密令
ls命令 查看文件信息
cat 命令 查看文件内容
cd密令,进入目录
pwd 密令用于查看当前工作目录路径。
mkdir命令 创建目录’
rm -f 密令删除
vi、vim 文件名 进入文件修改, wq :保存修改 q:不保存修改
echo 打印命令。
十六、设计模式
面试题
Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
单例模式重点在于在整个系统上共享一些创建时较耗资源的对象。整个应用中只维护一个 特定类实例,它被所有组件共同使用。Java.lang.Runtime 是单例模式的经典例子。从 Java 5 开始你可以使用枚举(enum)来实现线程安全的单例。
使用工厂模式最主要的好处是什么?在哪里使用?
工厂模式的最大好处是增加了创建对象时的封装层次。如果你使用工厂来创建对象,之后你可以使用更高级和更高性能的实现来替换原始的产品实现或类,这不需要在调用层做任何修改。
排序和比较时区分大小写,对 TEXT 值不区分大小写。
-
MyISAM 表格将在哪里存储,并且还提供其存储格式?
每个 MyISAM 表格以三种格式存储在磁盘上:
-
“.frm”文件存储表定义 元数据
-
数据文件具有“.MYD”(MYData)
-
索引文件具有“.MYI”(MYIndex)
-
可以使用多少列创建索引?
任何标准表最多可以创建 16 个索引列 ,实际中建议不超过五个
-
NOW()和 CURRENT_DATE()有什么区别?
NOW()命令用于显示当前年份,月份,日期,小时,分钟和秒。
CURRENT_DATE()仅显示当前年份,月份和日期。
-
MYSQL 数据表在什么情况下容易损坏?
服务器突然断电导致数据文件损坏。
强制关机,没有先关闭 mysql 服务等
-
Mysql 中有哪几种锁
MyISAM 支持表锁,InnoDB 支持表锁和行锁,默认为行锁
表级锁:开销小,加锁快,不会出现死锁。锁定粒度大,发生锁冲突的概率最高,并发量最低
行级锁:开销大,加锁慢,会出现死锁。锁力度小,发生锁冲突的概率小,并发度最高
-
常用的索引有哪些种类?
- 普通索引: 即针对数据库表某列创建索引
- 唯一索引: 与普通索引类似,不同的就是:MySQL 数据库索引列的值 必须唯一,但允许有空值
- 主键索引: 它是一种特殊的唯一索引,不允许有空值。一般是在建表的 时候同时创建主键索引
- 组合索引: 为了进一步榨取 MySQL 的效率,就要考虑建立组合索引。 即将数据库表中的多个字段联合起来作为一个组合索引。
- mysql 数据库中索引的工作机制是什么?
数据库索引,是数据库管理系统中一个排序的数据结构,减少io消耗,以协助快速查询。
索引的实现通常使用 B+树
-
MySQL 中 InnoDB 引擎的行锁是通过什么实现的?
InnoDB 行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!
-
*[SELECT ] 和 [SELECT 全部字段] 的 2 种写法有何优缺点?
- 前者要解析数据字典,后者不需要
- 结果输出顺序,前者与建表列顺序相同,后者按指定字段顺序。
- 表字段改名,前者不需要修改,后者需要改
- 后者可以建立索引进行优化,前者无法优化
- 后者的可读性比前者要高
-
HAVNG 子句 和 WHERE 的异同点?
- 语法上:where 用表中列名,having 用 select 结果别名
- 影响结果范围:where 从表读出数据的行数,having 返回客户端的行 数
- 索引:where 可以使用索引,having 不能使用索引,只能在临时结果 集操作
- where 后面不能使用聚集函数,having 是专门使用聚集函数的。
- 为什么 MySQL 的索引要使用 B+树而不是 B 树?
B 树和 B+树的最大区别就是, B 树不管叶子节点还是非叶子节点, 都会保存数据, 这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出) , 指针少的情况下要保存大量数据, 只能增加树的高度, 导致 IO 操作变多,查询性能变低。
十二、分库分表
基础知识
十三、Nginx
面试题
-
请解释一下什么是 Nginx?
Nginx 是一个 web 服务器和反向代理服务器,用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。
-
请列举 Nginx 的一些特性 ?
- 反向代理
- 负载均衡器
- 高可用性和伸缩性
- 动静分离
-
请列举 Nginx 和 Apache 之间的不同点
Nginx 是轻量级web服务器,Nginx 比 Apache 占用更少的内存及资源
Nginx 处理请求是异步非阻塞的,抗并发性比较好,而 Apache 则是阻塞型的,
在高并发下 Nginx 能保持低资源低消耗高性能。
Nginx 对所有的请求都有一个线程处理,Apache 单个线程处理单个请求
核心的区别在于 Apache 是同步多进程模型,一个连接对应一个进程;Nginx 是异步的,多个连接(万级别)可以对应一个进程。
- 请解释 Nginx 如何处理 HTTP 请求。
首先,Nginx 在启动时,会解析配置文件,得到需要监听的端口与 IP 地址,然后在 Nginx 的 Master 进程里面先初始化好这个监控的Socket
然后,再 fork(一个现有进程可以调用 fork 函数创建一个新进程。由 fork 创建的新进程被称为子进程 )出多个子进程出来。
之后,子进程会竞争 accept 新的连接。此时,客户端就可以向 nginx 发起连接了。当客户端与nginx进行三次握手,与 nginx 建立好一个连接后。
此时,某一个子进程会 accept 成功,得到这个建立好的连接的 Socket ,然后创建 nginx 对连接的封装,即 ngx_connection_t 结构体。
接着,设置读写事件处理函数,并添加读写事件来与客户端进行数据的交换。
最后,Nginx 或客户端来主动关掉连接
-
使用“反向代理服务器”的优点是什么?
反向代理服务器可以隐藏源服务器的存在和特征。使用安全
提高访问速度
防火墙作用 :由于所有的客户机请求都必须通过代理服务器访问远程站点,因此可在代理服务器上设限,过滤某些不安全信息
通过代理服务器访问不能访问的目标站点:通俗说,我们使用的翻墙浏览器就是利用了代理服务器,可直接访问外网。
-
请解释 Nginx 服务器上的 Master 和 Worker 进程分别是什么?
Master 进程:读取及评估配置和维持
Worker 进程:处理请求
-
如何通过不同于 80 的端口开启 Nginx?
为了通过一个不同的端口开启 Nginx,你必须进入/etc/Nginx/sitesenabled/,如果这是默认文件,那么你必须打开名为“default”的文件。编辑文件,并放置在你想要的端口: Like server { listen 81; }
-
请陈述 stub_status 和 sub_filter 指令的作用是什么?
Stub_status 指令:该指令用于了解 Nginx 当前状态的当前状态,如当前的活动连接,接受和处理当前读/写/等待连接的总数
Sub_filter 指令:它用于搜索和替换响应中的内容,并快速修复陈旧的数据
-
Nginx负载均衡的5种策略及原理
-
轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
-
指定权重
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
-
IP绑定 ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
-
fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。 -
url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
-
十四、ES
基础知识
REST
RESTful 指的是一组架构约束条件和原则,就是用来规范我们的 Http 接口API 的一种约束
RESTful 架构
1.架构里, 每一个 URI 代表一种资源;
2.客户端和服务器之间, 传递这种资源的某种表现层;
3.客户端通过四个 HTTP 动词(get、post、 put、 delete) 对服务器端资源进行操作, 实现” 表现层状态转化”
注意: REST 架构风格并不是绑定在 HTTP 上, 只不过目前 HTTP 是唯一与 REST相关的实例。 所以我们这里描述的 REST 也是通过 HTTP 实现的 REST。
十五、Linux
常用密令
ls命令 查看文件信息
cat 命令 查看文件内容
cd密令,进入目录
pwd 密令用于查看当前工作目录路径。
mkdir命令 创建目录’
rm -f 密令删除
vi、vim 文件名 进入文件修改, wq :保存修改 q:不保存修改
echo 打印命令。
十六、设计模式
面试题
Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
单例模式重点在于在整个系统上共享一些创建时较耗资源的对象。整个应用中只维护一个 特定类实例,它被所有组件共同使用。Java.lang.Runtime 是单例模式的经典例子。从 Java 5 开始你可以使用枚举(enum)来实现线程安全的单例。
使用工厂模式最主要的好处是什么?在哪里使用?
工厂模式的最大好处是增加了创建对象时的封装层次。如果你使用工厂来创建对象,之后你可以使用更高级和更高性能的实现来替换原始的产品实现或类,这不需要在调用层做任何修改。