大家好,又见面了,我是你们的朋友全栈君。
腾讯
1.java基础
-
8种基本数据类型,int几个字节
类型 存储需求 取值范围 byte 1B -128~127 short 2B -32768~32767 int 4B -20亿~20亿 long 8B float 4B 小数点后6~7位 double 8B 小数点后15位 char 2B boolean true/false -
static,final关键字
- static
- 被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载了,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。
-
静态变量(类变量):静态变量被所有的对象所共享,也就是说我们创建了一个类的多个对象,多个对象共享着一个静态变量,如果我们修改了静态变量的值,那么其他对象的静态变量也会随之修改。
非静态变量(实例变量):如果我们创建了一个类的多个对象,那么每个对象都有它自己该有的非静态变量。当你修改其中一个对象中的非静态变量时,不会引起其他对象非静态变量值得改变。
-
static修饰的成员方法称作静态方法,这样我们就可以通过“类名. 方法名”进行调用。由于静态方法在类加载的时候就存在了,所以它不依赖于任何对象的实例就可以进行调用,因此对于静态方法而言,是木有当前对象的概念,即没有this、super关键字的。因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
-
被static修饰的代码块也叫静态代码块,会随着JVM加载类的时候而加载这些静态代码块,并且会自动执行。它们可以有多个,可以存在于该类的任何地方。JVM会按照它们的先后顺序依次执行它们,而且每个静态代码块只会被初始化一次,不会进行多次初始化。
-
普通类是不允许声明为静态的,只有内部类才可以,被static修饰的内部类可以直接作为一个普通类来使用,而不需先实例一个外部类。
- 静态内部类只能访问外部类的静态成员,否则编译会报错。
- 不管是静态方法还是非静态方法都可以在非静态内部类中访问。
- 如果需要调用内部类的非静态方法,必须先new一个OuterClass的对象outerClass,然后通过outer。new生成内部类的对象,而static内部类则不需要。
- final
- 修饰类:当用final修饰一个类时,表明这个类不能被继承。
- 修饰方法:方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
- 修饰变量:当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
- static
-
static和abstract能同时用吗
不能,因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
内部类可以调用外部的数据吗?如果是静态的呢?
可以。静态内部类只能访问外部类的静态成员,否则编译会报错。
重写(override)和重载(overload)的区别
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
-
返回值不同的重载,可以吗?为什么?
不可以。在java语言中,要重载(overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此java语言里面无法仅仅依靠返回值不同来对一个已有的方法进行重载。
- java代码层面的特征签名:方法名称+参数顺序+参数类型
- 字节码文件的特征签名:以上+方法返回值+受查异常表
-
equals和==
- == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
- equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖写 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类override了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
-
举个例子:
public class test1 { public static void main(String[] args) { String a = new String("ab"); // a 为一个引用 String b = new String("ab"); // b为另一个引用,对象的内容一样 String aa = "ab"; // 放在常量池中 String bb = "ab"; // 从常量池中查找 if (aa == bb) // true System.out.println("aa==bb"); if (a == b) // false,非同一对象 System.out.println("a==b"); if (a.equals(b)) // true System.out.println("aEQb"); if (42 == 42.0) { // true System.out.println("true"); } } }
说明:
- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
-
String和StringBuffer、 StringBuilder
- 可变性:String 类中使用 final 关键字修饰字符数组来保存字符串,
private final char value[]
,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value
但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - 线程安全性:String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁(synchronized),所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
- 性能:每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder;多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer。
-
hashCode 与 equals (重要)
- hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)。
- 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相同的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用
equals()
方法来检查 hashcode 相等的对象。如果equals方法返回为true,HashSet 就不会让其加入操作成功。如果返回false,就会重新散列到其他位置。 - hashCode()与equals()的相关规定
- 如果不需要将该类的对象存放到哈希的集合中,比较对象相等时只需要重写equals()方法
- 如果需要将该类的对象存放到哈希的集合中,则需要重写hashCode()方法
- 例子
class Person {
int age;
String name;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " - " +age;
}
/**
* @desc重写hashCode
*/
@Override
public int hashCode(){
int nameHash = name.toUpperCase().hashCode();
return nameHash ^ age;
}
/**
* @desc 覆盖equals方法
*/
@Override
public boolean equals(Object obj){
if(obj == null){
return false;
}
//如果是同一个对象返回true,反之返回false
if(this == obj){
return true;
}
//判断是否类型相同
if(this.getClass() != obj.getClass()){
return false;
}
Person person = (Person)obj;
return name.equals(person.name) && age==person.age;
}
}
-
Object类有哪些方法
- hashCode(),equals()
- toString()
- clone()
- wait(),notify(),notifyAll()
- finalize()
-
collection和collections
-
length,length()和size()
-
java 中的length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.
-
java 中的length()方法是针对字符串String说的,如果想看这个字符串的长度则用到 length()这个方法.
-
.java 中的size()方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!
-
-
ArrayList简介
ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity
操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。
它继承于 AbstractList,实现了 List, RandomAccess, Cloneable, java.io.Serializable 这些接口。
在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)
- ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
- ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
- ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆。
- ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。
- 和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。
LinkedList简介
LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:
List list=Collections.synchronizedList(new LinkedList(...));
看LinkedList类中的一个内部私有类Node就很好理解了:
private static class Node<E> {
Node<E> prev;//前驱节点
E item;//节点值
Node<E> next;//后继节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。
获取尾节点(index=-1)数据方法:getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只是会返回 null
删除方法:removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null
-
ArrayList和LinkedList的区别,为什么Arraylist快
-
是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
-
底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构;
-
插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行
add(E e)
方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。 -
是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于
get(int index)
方法)。 -
内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
ArrayList扩容机制
/**
* 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
* @param minCapacity 所需的最小容量
*/
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
//判断是否需要扩容,上面两个方法都要调用
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。
//这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1
if (minCapacity - elementData.length > 0)
//调用grow方法进行扩容,调用此方法代表已经开始扩容了
grow(minCapacity);
}
/**
* 允许分配的数组最大容量
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法
* @param minCapacity:最小扩容量
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量=数组当前长度
int oldCapacity = elementData.length;
// newCapacity为新容量=oldCapacity的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 然后检查新容量是否大于最小需要容量,若还是小于最小扩容量,那么就把最小扩容量当作数组的新容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 再检查新容量是否超出了ArrayList所定义的最大容量,
// 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
// 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
-
ArrayList和Vector
- Vector与ArrayList一样,也是通过数组实现的,Vector类的所有方法都是同步的。它也是线程安全的,而Arraylist是线程异步(ASynchronized)的,是不安全的。如果不考虑到线程的安全因素,一般用Arraylist效率比较高。
-
使用ArrayList时,如果不指定大小,会生成一个空的数组;
使用Vector时,如果不指定大小,会默认生成一个10个元素大小的数组
-
Vector 实现类中有一个变量 capacityIncrement 用来表示每次容量自增时应该增加多少,如果不指定,默认为0
在扩容时,会判断,如果指定了capacityIncrement,会先把数组容量扩大到oldCapacity + capacityIncrement,如果没有指定capacityIncrement,会先把数组容量扩大到2倍的oldCapacity, 然后再进行判断扩充后的容量是否满足要求,如果不满足要求,直接将容量扩大到指定大小,源码如下:
private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); elementData = Arrays.copyOf(elementData, newCapacity); }
-
Set不能存放重复元素,其底层是如何实现的
- HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了
clone()
、writeObject()
、readObject()
是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 - 当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
- HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了
-
HashMap原理
-
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过
(n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 -
JDK 1.8 HashMap 的 hash 方法源码:
static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//hashcode的高16位与低16位异或 }
-
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可:
-
-
为什么HashMap无序?为什么不安全?
因为元素在底层数组中的索引位置是经过hash算法(key的哈希值+扰动)计算出来的,与原来的顺序没有关系。线程不安全,多线程访问的情况下,会出现问题。
Hash碰撞的解决方法
- 数组+链表
- 当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
HashMap满了之后怎么扩容(rehash)
loadFactor负载因子/装填因子
loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。
loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。
给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
-
threshold
threshold = capacity * loadFactor,当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
- 进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
-
下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。
-
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // oldCap旧容量:当前table的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // if (oldCap > 0) { // 如果旧容量超过最大值就不再扩容了,就只好随你碰撞去吧 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 没超过最大值,newCap新容量为旧容量的2倍,newThr新阈值也扩容为旧阈值的2倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 计算新的resize上限 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"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { // 把oldTab中每个bucket都移动到新的buckets中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; // oldTable该索引位置有key if ((e = oldTab[j]) != null) { oldTab[j] = null; // 该位置无冲突Node if (e.next == null) // 计算该key在新table中索引位置:e.hash&(newCap-1),存放在此 newTab[e.hash & (newCap - 1)] = e; // 有冲突,是红黑树Node else if (e instanceof TreeNode) // 插入红黑树 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 有冲突,是链表Node else { Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do {// 遍历插入 next = e.next; // 原索引位置 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 原索引位置+oldCap else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原索引放到bucket里 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // 原索引+oldCap放到bucket里 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
-
HashMap 的长度为什么是2的幂次方
散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
HashMap put操作的最差情况解决方案
HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。
对putVal方法添加元素的分析如下:
- ①如果定位到的数组位置没有元素 就直接插入。
- ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)
将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。 -
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
-
public V put(K key, V value) { // 对key的hashCode()做hash return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 步骤①:tab为空则创建 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步骤②:计算index,并对null做处理 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 步骤③:节点key存在,直接覆盖value if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 步骤④:判断该链为红黑树 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 步骤⑤:该链为链表 else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key,value,null); //链表长度大于8转换为红黑树进行处理 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // key已经存在直接覆盖value if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 表示在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { V oldValue = e.value;// 记录e的value if (!onlyIfAbsent || oldValue == null) e.value = value;// 用新值替换旧值 afterNodeAccess(e);// 访问后回调 return oldValue;// 返回旧值 } } ++modCount; // 步骤⑥:超过最大容量 就扩容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
-
海量数据存进HashMap性能会变差吗?
当一个链表太长的时候,HashMap会动态的将它替换成一个红黑树,这话的话会将时间复杂度从O(n)降为O(logn)。
HashMap和HashTable的区别,并说明其底层实现数据结构
- 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
synchronized
修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); - 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
- 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
- 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的
tableSizeFor()
方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 - 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
- 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过
说说TreeMap的实现原理
- TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
海量数据怎么查找敏感词
Java中异常的分类
- Error:程序无法处理的系统问题,一般与JVM相关,编译器不做检查,如:系统奔溃,虚拟机错误,系统内存不足,调用栈溢出。
- NoClassDefFoundError-找不到class定义的异常,原因:
- 类依赖的class或者jar不存在(e.g. maven中漏引了某个jar包)
- 类文件存在,但是存在不同的域中
- 大小写问题,javac编译的时候是无视大小写的,很有可能编译出来的class文件就与想要的不一样
- StackOverFlowError-深递归导致栈被耗尽而抛出的异常
- OutOfMemoryError-内存溢出异常
- Exception:程序可以处理的异常,可以被程序捕获、处理和恢复。
- RuntimeException
- NullPointerException:调用空对象的方法或属性
- ClassCastException:将某个实例赋值给非父类对象
- IllegalArgumentException:给方法传递了不满足要求的参数(个数或类型)
- IndexOutOfBoundsException:数组下标异常
- NumberFormatException:试图将一个字符串转换为指定数字类型,而该字符串确实不满足该数字类型
- 非RuntimeException
- ClassNotFoundException-找不到指定class的异常
- IOException-IO操作异常
-
Jvm是怎样进行内存分配的
- 线程隔离区域:
- 程序计数器:指示当前线程所执行的字节码的行号
- 虚拟机栈:一个方法对应一个栈帧,调用==>完成====入栈==>出栈
- 本地方法栈:
- 线程共享区域:
- 方法区:
- 存储类的信息,常量和静态变量
- 内存回收的目标主要是:对常量池的回收和对类型的卸载
- 不需要连续的内存空间,可扩展
- 堆:
- 虚拟机启动时创建
- 所有的对象实例以及数组都在堆上分配
- 不需要连续的内存空间,可扩展
- 方法区:
- 线程隔离区域:
-
java内存模型中栈与堆的联系与区别
- 联系:栈中定义的引用变量保存堆中目标对象的首地址
- 区别:
- 管理方式:栈的内存空间自动释放,而堆内存空间的释放需要垃圾回收
- 空间大小:一般情况下,栈的空间比堆的空间占用要小
- 碎片相关:栈产生的碎片远小于堆
- 分配方式:栈空间支持静态分配(编译器分配好了)和动态分配,堆空间仅支持动态分配
- 效率:栈的效率比堆高
-
String a=new String(“Hello”);说说栈、堆、常量池的存储情况
- 字面量”Hello”存放到方法区的常量池中
- new String(), 这里是通过new创建了一个String对象,放在heap(堆)中
- String s , 这个语句声明一个类String的引用变量 s [我们常常称之为句柄],它不是对象,而对象一般通过new创建。
-
jdk6和jdk6+的intern()方法的区别
- jdk6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
- jdk6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回引用。
-
垃圾回收算法
- 标记-清除(Mark-Sweep)
- 效率问题:标记和清除这两个过程的效率都不高
- 空间问题:会产生大量不连续的内存碎片
- 复制(Copying)
- 内存划分:Eden:From Survivor:To Survivor=8:1:1
- 当To Survivor空间不够存放新生代存活下来的对象时,需要老年代进行分配担保
- 标记-整理(Mark-Compact)
- 将存活下来的对象移动到一端,直接清理掉端边界以外的内存
- 分代回收策略:
- 新生代(对象存活率低):少量存活,复制算法
- 老年代(对象存活率高):大量存活,标记-整理算法
- 标记-清除(Mark-Sweep)
-
垃圾回收器
- 单线程收集器(一个收集线程)
- Serial收集器–新生代:进行垃圾收集时,必须暂停其他所有工作线程(Stop The World)
- Serial Old收集器:Serial收集器的老年代版本
- Serial收集器–新生代:进行垃圾收集时,必须暂停其他所有工作线程(Stop The World)
- 多线程并行收集器(多个收集线程)
- Parallel New收集器–新生代:Serial收集器的多线程版本
- Parallel Scavenge收集器–新生代:
- 目标:优化吞吐量=(执行用户代码时间)/(执行用户代码时间+执行垃圾回收时间)
- 支持(-XX:+UseAdaptiveSizePolicy):自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间或者最大的吞吐量
- Parallel Old收集器:Parallel Scavenge的老年代版本
- Parallel New收集器–新生代:Serial收集器的多线程版本
- 并发收集器(收集线程可与工作线程同时执行)
- CMS(Concurrent Mark Sweep)收集器–老年代:目标:获取最短的回收停顿时间
- 初始标记(单线程):标记GC Roots能直接关联到的对象
- 并发标记:遍历标记GC Roots的可达链中的对象
- 重新标记(多线程并行):修正并发标记阶段发生变动的标记记录
- 并发清除:并发清除阶段程序会产生新的垃圾,即浮动垃圾,只能留待下一次GC时再清除
- G1收集器–不区分新生代和老年代
- 用户可以指定垃圾回收时间的上限
- 将整个java堆划分成为多个大小相等的独立区域(Region),对所有Region维护一张回收优先列表,回收性价比(回收所获得的空间大,花费时间短,性价比就高)越高越优先回收
- CMS(Concurrent Mark Sweep)收集器–老年代:目标:获取最短的回收停顿时间
-
老年代还有内存,但是程序显示OOM什么原因?
在进行Minor GC回收新生代中的内存时,若存活下来的对象的大小总和超过To Survivor空间的大小,则需要转移到老年代(HandlePromotionFailure开启:可以冒险地执行Minor GC)
为什么Java语言具有平台无关性,讲一下类加载机制
- 类加载的时机:
- new 1个对象
- 访问静态变量/方法
- 反射
- 子类初始化==>父类初始化
- 启动类(包含mian方法的类)
- 加载机制的过程:
- 加载阶段
- 将二进制数据读取到内存中
- 将静态存储结构转化为方法区的运行时数据结构
- 在堆中生成一个该类的Class对象,作为方法区这个类各种数据结构的访问入口
- 连接阶段
- 验证:验证文件格式,元数据,字节码,符号引用
- 准备:为类的静态变量分配内存(方法区),设置初始值
- 解析:在常量池中寻找类,字段,方法,接口的符号引用,替换为直接引用
- 初始化
- 执行类构造器<clinit>方法:编译阶段收集了类的所有类变量的赋值动作和静态代码块中的语句
- 父类的<clinit>方法先于子类的<clinit>方法执行
JVM内存模型
- 主内存:所有变量都存储在主内存
- 工作内存:每条线程都有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝
- 线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量
- 内存之间的交互操作
- lock:把主内存中的变量标识为一条线程独占
- unlock:把主内存中处于锁定状态的变量释放
- read:将主内存中的一个变量读取到工作内存
- load:把read进工作内存的变量值存放到变量副本中
- use:
- assign:
- store:把工作内存中一个变量的值传送到主内存中
- write:把store进主内存中的变量值存放到主内存的变量中
先行发生(happen-brefore)原则
若两个操作满足以下的规则之一,则这两个操作无需进行任何同步就能保证顺序性
- 程序依次规则:在控制流顺序中书写在前的操作先于书写在后的操作发生
- 管程锁定规则:unlock操作先于后面对同一个锁的lock操作发生
- volatile变量规则:对一个volatile变量的写操作先于后面对这个变量的读操作发生
- 线程启动规则:Thread对象的start()方法先于此线程的每一个动作发生
- 线程终止规则:线程中的所有操作都先于此线程的终止检测发生
- 线程中断规则:对线程的interrupt()方法的调用先于被中断线程的代码检测到中断事件的发生发生
- 对象终结原则:一个对象的初始化完成先于它的finalize()方法的开始发生
- 传递性
-
JVM参数,线程数调整
线程数量过多会把内存容量耗尽:OutOfMemoryError
性能调优看哪些命令
- -Xss:规定了每个线程虚拟机栈的大小
- -Xms:堆的初始大小
- -Xmx:堆大小能达到的最大值
- -XX:PermSize:方法区空间初始值
- -XX:MaxPermSize:方法区空间的最大值
- -XX:SurvivorRatio:Eden和Survivor的比值,默认8:1
- -XX:NewRatio:老年代和年轻代内存大小的比例
- -XX:MaxTennuringThreshold:对象从年轻代晋升到老年代经过GC次数的最大阈值
Rest ful API的规范
2.java高并发
线程的几种状态,waiting和blocked的转换?
- New:当用new操作符创建一个新线程时,该线程还没有开始运行
- Runnable:一旦线程调用start方法,线程处于runnable状态,包括Running和Ready状态的进程
- Waiting:该状态下的线程不会被分配CPU时间片,等待被其他线程显式地唤醒
- Object.wait()
- Thread.join()
- LockSupport.park()
- Timed Waiting:该状态下的线程不会被分配CPU时间片,无需等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒
- Thread.sleep()
- Object.wait(Timeout)
- Thread.join(TimeOut)
- LockSupport.parkNanos()
- Blocked:线程被阻塞了,线程等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生
- Terminated:已终止线程的线程状态,线程已经执行结束
StringBuilder和StringBuffer使用单线程执行,有区别吗?
StringBuffer是线程安全的,而StringBuilder是非线程安全的,在单线程的情况下,StringBuffer由于同步机制性能降低,建议使用StringBuilder。
java并发集合有哪些
- ConcurrentHashMap:一个ConcurrentHashMap维护多个Segment数组,一个Segment维护多个HashEntry数组,分段锁
- CopyOnWriteArrayList:
- 底层是Object数组
- 读操作不用加锁:存在若一致性问题(例如,线程x可能读取到已经被线程yremove掉的数据)
- 写时复制策略(修改list时,将底层数组copy一份,不改动原来的数组,修改后替换原来的数组),加ReentrantLock独占锁
- ConcurrentLinkedQueue:
- 底层:单向链表–无界
- 使用Unsafe类的CAS函数实现出队和入队的原子性–非阻塞
- LinkedBlockingQueue:
- 底层:单向链表–无界
- 使用两把ReentrantLock独占锁(putLock和takeLock)分别实现入队和出队操作的原子性–阻塞
- 使用两个条件变量(notEmpty和notFull)的条件队列分别存放入队和出队时被阻塞的线程
- ArrayBlockingQueue:
- 底层:Object数组–有界
- 使用1把ReentrantLock独占锁保证入队和出队操作的原子性–阻塞
- 使用两个条件变量(notEmpty和notFull)的条件队列分别存放入队和出队时被阻塞的线程
- PriorityBlockingQueue:
- 底层:平衡二叉树堆
- 使用1把ReentrantLock独占锁保证入队和出队操作的原子性–阻塞
- 使用1把自旋锁独占锁:CAS操作实现同一时刻只有一个线程扩容
CurrentHashMap原理
一个ConcurrentHashMap维护多个Segment数组,一个Segment维护多个HashEntry数组。
-
对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。
-
ConcurrentHashMap 和 Hashtable 的区别
- HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
- HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的”分段锁“思想。
-
ConcurrentHashMap在不扩容时,get和put的操作过程,在扩容时get和put怎么操作
- get方法:先定位Segment,再定位HashEntry,无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。
- put方法:
- 1.计算得到的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
- 2.调用segment的put方法
- tryLock:不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。定位HashEntry
- 定位到HashEntry,put
- 若size超过阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列
- 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
-
synchronized静态方法和实例所属方法的区别
- synchronized修饰非静态成员方法,线程争抢的是该类的实例关联的监视器
- synchronized修饰静态成员方法,线程争抢的是Class类的对象关联的监视器
-
类中的两个方法,一个public synchronized修饰,一个public static synchronized修饰,两个线程分别调用,会出现正常还是死锁
- 分别调用
-
volatile了解哪些?如何实现你说的可见性
- 保证可见性:当共享变量使用volatile关键字修饰时,对于共享变量的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享变量进行了修改,会导致当前线程在本地内存中的共享变量的副本失效,必须从从主内存中再次获取);对于共享变量的写操作当然是先要修改本地内存,但是修改结束后会立即将其刷新到主内存中
- 保证有序性:禁止JVM和处理器对volatile修饰的指令重排序,但是对于volatile前后无依赖关系的指令则可以随便怎么排序
- 不保证原子性:
-
用volatile修饰的a,多线程调用会不会出现问题?为什么?
- 多线程对volatile修饰的共享变量的操作有可能会被中途打断
-
CAS原理
- CAS即Compare and Swap,使用硬件指令集支持的函数保证操作(比较-更新)的原子性
- 如果内存中某个对象的偏移量为valueOffset的变量的值等于预期值expect,则使用新的值update旧的值expect,不等于,则可以一直循环自旋,而无需阻塞
- ABA问题:线程1拿到X时,其值为A,线程2也拿到了X,值为A,并修改为B,然后又修改回A;此时,线程1可以执行CAS操作,但是这个A已经不是线程1获取时的A了。
- JDK中的AutomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生
-
说说ReentrantLock是基于哪个类的?
- 继承自AbstractQueuedSynchronizer
- 两个子类:
- FairSync():公平锁:线程获取写锁时,判断当前线程是否有前驱节点,如果有,则放弃获取写锁的权利
- NonFairSync():非公平锁
-
说说Lock
- 独占锁:ReentrantLock
- 读写锁:ReentrantReadWriteLock
- 读锁:lock.readLock()—-state的高16位表示获取到读锁的次数
- 写锁:lock.writeLock()—-state的低16位表示获得到写锁的次数
-
讲讲线程,线程池
- 不使用线程池时,每当需要执行异步任务时都直接new一个线程来运行,线程的创建和销毁是需要开销的
- 线程池里面的线程是可复用的,不需要每次执行异步任务时都重新创建和销毁现场
- 线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数,动态新增线程
- 不使用线程池时,每当需要执行异步任务时都直接new一个线程来运行,线程的创建和销毁是需要开销的
-
Executors工具类有哪几种构造线程池的方法,说说线程池创建时的参数
- 构造参数:核心线程个数,最大线程个数,限制线程存活时间,任务阻塞队列,线程工厂
- newFixedThreadPool:corePoolSize=maxPoolSize=n;keeyAliveTime=0;LinkedBlockingQueue
- newSingleThreadExecutor:corePoolSize=maxPoolSize=1;keeyAliveTime=0;LinkedBlockingQueue
- newCachedThreadPool:corePoolSize=0,maxPoolSize=Integer.MAX_VALUE;keeyAliveTime=60;SynchronousQueue(同步队列,同步队列中最多只能有一个任务)
-
线程池处理task的流程
- 如果任务为null,则抛出异常
- 若当前线程个数<corePoolSize,则开启新线程运行该任务
- 若当前线程池状态为Runing,则将当前任务添加到任务阻塞队列
- 若任务阻塞队列满了,则新增线程,若新增失败则执行拒绝策略
- 抛出异常
- 使用调用者所在线程运行任务
- 丢弃任务阻塞队列中队首任务,执行当前任务
- 丢弃当前任务,不抛出异常
-
说说队列同步器
- CountDownLatch
- 作用:让主线程阻塞,等待子线程完成后返回
- 原理:基于AQS(使用自旋的CAS,保证了对state变量操作的原子性),将计数器的值count赋给AQS的状态变量state
- await()方法返回的情况:
- 计数器的值递减至0
- 其他线程interrupt方法中断了当前的线程
- CyclicBarrier:线程调用await方法就会被阻塞,这个阻塞点称为屏障点,等所有的线程都调用了await方法,线程们就会冲破屏障,继续向下执行
- 作用:实现了状态可重用的同步器,适用于分段任务有序执行的场景
- 原理:基于AQS
- parties:记录线程个数,表示多少个线程调用await方法后,所有的线程才会冲破屏障继续往下运行
- count:赋给AQS的状态变量,递减至0后,会将parties的值赋给count
- Semphore
- 作用:
- 原理:基于AQS,将信号量的个数赋值给AQS的状态变量state
- acquire(num):阻塞当前线程,直到信号量个数递增了num才返回,返回后信号个数减num
- CountDownLatch
3.数据结构与算法
-
排序算法
- 冒泡排序:重复走访待排序列,比较相邻元素,若顺序错误则交换,每走访一轮,可让该轮的最大值放置到正确位置
- 改进版:设置一个swap标志,若当前轮次没有发生交换,说明数组已经有序,没有必要再进行下一轮,使算法在最佳情况下有O(n)的时间复杂度
- 快速排序:每轮从待排序列中选择一个元素作为pivot,通过一趟排序将待排序列表划为两部分,前半部分所有元素值小于pivot,后半部分所有元素值大于等于pivot,而后分别递归对两个子表重复上述过程
- 直接插入排序:将待排序表分成已排序和未排序两个子序列,从第2个元素开始,查找未排序子序列的值在已排序子序列中合适的位置,插入其中
- 稳定性:稳定,因为只后移大于当前值的数,相等的元素不会后移
- 希尔排序:选择步长d1,将表分成d1个组,各组内部使用直接插入排序;步长减半,重复上述过程,直到步长dt=1,只有一组,进行直接插入排序
- 稳定性:不稳定,当相同关键字的记录被划分到不同的子表时,可能会改变他们之间的相对次序
- 选择排序:第一轮在未排序序列中找到最小值元素,放置到待排序序列的起始位置,以后从剩余的未排序子序列中继续寻找最小元素放到已排序子序列末尾(交换)
- 稳定性:不稳定,存在交换,会导致关键字相等的元素的相对位置发生改变
- 堆排序:将待排序列L[1…n]看成是一棵完全二叉树;
- 小根堆(最小元素存放在根节点):L[i]<=L[2i]&&L[i]<=L[2i+1]
- 大根堆(最大元素存放在根节点):L[i]>=L[2i]&&L[i]>=L[2i+1]
- 建堆后,每次输出堆顶元素后,将堆底元素送入堆顶,然后自上而下地调整树使之满足堆的性质,再输出堆顶元素,如此重复,直到堆中仅剩一个元素为止
- 建堆:
- 因为最后一个节点是第|_n/2_|个节点的孩子,对以第|_n/2_|个节点为根的字数进行筛选,看该节点是否小于其左右子节点的值,若不是,将左右子节点中较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该节点为根的子树构成堆为止。
- 归并排序:将待排序列分治递归至每个子表长度为1或2,然后两两归并,直到合并成一个长度为n的有序表为止
- 空间复杂度:合并时需要辅助空间占用n个单元
- 基数排序:d–元组数(关键字位数),r–基数(关键字一位的取值范围)
- 对关键字做d轮分配和回收
- 分配:初始化r个空队列,依次考察n个关键字的第i位,添加到第i个队列Qi中
- 回收:把Q0,Q1,…Qn-1各个队列中的节点依次首尾相连,得到新的节点序列,从而组成新的线性表
- 时间复杂度:O(d(n+r)),d轮,分配:O(n);回收:O(r)
- 空间复杂度:O(r)
- 对关键字做d轮分配和回收
- 桶排序:根据关键字范围创建k个大小相同的子区间,称为bucket,将n个关键字划分到所在区间对应的bucket中,然后对每个bucket中的关键字进行排序,然后依次将k个bucket中的元素列出来即可
- 冒泡排序:重复走访待排序列,比较相邻元素,若顺序错误则交换,每走访一轮,可让该轮的最大值放置到正确位置
-
堆和栈的数据结构
- 堆:见上一题
- 栈:后进先出
-
红黑树与AVL二叉平衡树的区别
- 二叉查找树(BST):
- 左子树所有节点值均小于或等于根节点的值+右子树所有节点值均大于或等于根节点的值+左右子树也是BST
- 查找,插入,删除的时间复杂度等于树的高度O(logN)
- 缺点:会退化成为倾斜的二叉查找树,复杂度为O(N)
- 平衡二叉树(AVL):平衡二叉树就是为了解决二叉查找树退化成一颗链表而诞生了
- 具有二叉查找树的全部特性。
- 每个节点的左子树和右子树的高度差至多等于1。
- 对于有 n 个节点的平衡树,最坏的查找时间复杂度也为 O(logn)。
- 缺点:平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。
- 红黑树:在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树
- 节点是红色或黑色。
- 根节点是黑色。
- 每个叶子节点都是黑色的空节点(NIL节点)。
- 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
当插入或删除节点使得红黑树的规则被打破时,需要对其进行调整:
1.变色:为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色(会发生连锁效应)
2.左旋:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。
3.右旋:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。
与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因。
-
最长子序列问题,说说思路
-
7升桶和3升桶,怎么打到2升水?
-
n*m的矩阵,每行每列都有序,查找某个数
-
有序整数数组,给定一个数,从数组中找出2个数相加等于它。要求O(n)时间复杂度。
4.操作系统
-
说说用户态和内核态的特点?不同之处?
- 内核态:运行操作系统的内核程序(与硬件关联紧密的程序:时钟管理,中断处理,运行频率较高的程序:进程管理,内存管理)
- 用户态:运行用户自编写的应用程序
- 内核程序是用户程序的管理者,作为管理程序,内核程序能够执行一些用户程序不能执行的特权指令,如,IO指令,置中断指令
-
进程与线程的区别
- 进程是系统资源分配的基本单位,线程是CPU调度的基本单位
- 一个进程的所有线程共享该进程的资源
- 进程切换需要保存和设置进程的CPU环境;而线程切换只需要保存和设置少量寄存器内容
-
进程之间的通信方式
- 共享存储区:在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行读/写操作实现进程之间的信息交换
- 消息传递:利用操作系统提供的消息传递方法实现进程通信
- 直接通信方式:发送进程直接把消息发送给接收进程,把它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中区的消息
- 间接通信方式:发送进程把消息发送到某个中间实体(一般称为信箱)中,接收进程从中间实体中取得消息
- 管道通信:管道是一个固定大小的缓冲区,半双工通信,某一时刻只能单向传输
- 写进程:以字符流形式将大量的数据送入写管道,写满则阻塞
- 读进程:从管道中读取数据,数据一旦被读取,就从管道中被抛弃
-
说说协程,协程的优势
- 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个维度。
- 一个进程可以包含多个线程,一个线程可以包含多个协程。
- 一个线程内的多个协程虽然可以切换,但是这多个协程是串行执行的,只能在这一个线程内运行,没法利用CPU多核能力。
- 协程与进程一样,它们的切换都存在上下文切换问题。
表面上,进程、线程、协程都存在上下文切换的问题,但是三者上下文切换又有明显不同,见下表:
协程适合I/O 阻塞型。
I/O本身就是阻塞型的(相较于CPU的时间世界而言)。就目前而言,无论I/O的速度多快,也比不上CPU的速度,所以一个I/O相关的程序,当其在进行I/O操作时候,CPU实际上是空闲的。
我们假设这样的场景,如下图:1个线程有5个I/O的事情(子程序)要处理。如果我们绝对的串行化,那么当其中一个I/O阻塞时,其他4个I/O并不能得到执行,因为程序是绝对串行的,5个I/O必须一个一个排队等待处理,当一个I/O阻塞时,其它4个也得等着。
而协程能比较好地处理这个问题,当一个协程(特殊子进程)阻塞时,它可以切换到其他没有阻塞的协程上去继续执行,这样就能得到比较高的效率,如下图所示:
-
CPU的调度策略
- 方式
- 非抢占式调度:CPU上当前运行的进程不会因为有更为重要或紧迫的进程进入就绪队列而暂停,还会继续执行直到进程完成或进入阻塞状态
- 抢占式调度:CPU上当前运行的进程可以因为有更为重要或紧迫的进程进入就绪队列而暂停,将CPU分配给这个更为重要或紧迫的进程
- 调度算法
- 先来先服务(FCFS):属于不可抢占算法,对长作业比较有利,对短作业不利
- 短作业优先(SJF):
- 每次从就绪队列中选择估计运行时间最短的进程,将CPU分配给它,使之立即执行
- 对长作业不利,容易产生饥饿现象(长作业长期不被调度)
- 优先级调度:每次从就绪队列中选择优先级最高的进程,将CPU分配给它,使之立即执行
- 非抢占式优先级调度和抢占式优先级调度
- 静态优先级(优先级在进程创建时确定)和动态优先级(优先级在进程运行期间动态调整)
- 高响应比优先调度:每次进行作业调度时,先计算后备队列中每个作业的响应比,从中选出响应比最高的作业投入运行
- 响应比Rp=(等待时间+要求服务时间)/要求服务时间
- 对于长作业,作业的响应比可以随着等待时间的增加而提高,当其等待时间足够长时,响应比便可以升到很高,从而也可获得CPU。克服了饥饿状态,兼顾了长作业。
- 时间片轮转调度:系统将所有的进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中的第一个进程执行,即先来先服务,但仅能运行一个时间片。在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出(被抢占)CPU给下一个就绪的进程,而被抢占的进程返回到就绪队列的末尾重新排队,等候再次运行。
- 多级反馈队列调度:综合了时间片轮转算法和优先级调度算法
- 设置多个就绪队列,优先级从高到低
- 优先级越高的就绪队列分配的时间片越小
- 方式
-
会不会存在有些进程一直得不会得到CPU的使用权
在短进程优先的调度算法下,长进程可能会长时间得不到CPU的调度,即产生饥饿现象
某个进程进入死循环,cpu还上下文切换吗?
不会影响上下文切换
什么是死锁,形成死锁须具备哪些条件,怎么预防死锁,如果死锁已经产生怎么解决,讲一下银行家算法。
- 死锁产生的必要条件
- 互斥条件:在一段时间内某资源仅为一个进程所占有
- 不可剥夺条件:进程所获得的资源在使用完毕之前,不能被其他进程强行夺走
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求
- 循环等待条件:进程的资源等待关系形成了闭合的环
- 死锁预防:破坏必要条件
- 破坏互斥,不太可行
- 强行剥夺进程占有的资源
- 资源静态分配
- 资源按序分配:只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri的资源
- 死锁避免:银行家算法
- 允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态(如果系统无法找到一个安全序列,则称系统处于不安全状态),以避免发生死锁。
- 可利用资源向量(Available):Available[j]=K–表示系统中现有Rj类资源的数目为K
- 最大需求矩阵(Max):Max[i][j]=K–表示进程i需要Rj类资源的最大数目为K
- 分配矩阵(Allocation):Allocation[i][j]=K–表示进程i当前已分到的Rj类资源的数目为K
- 需求矩阵(Need):Need[i][j]=K–表示进程i还需要Rj类资源的数目为K
- 死锁解除
- 资源剥夺
- 撤销进程
- 进程回退:回退到足以回避死锁的地步
- 死锁产生的必要条件
页面置换算法
- 最佳置换算法(OPT):淘汰以后不再被访问的页面
- 先进先出置换算法(FIFO):淘汰最早进入内存的页面,会出现Belady异常
- 最近最久未使用算法(LRU):淘汰最近最长时间未访问过的页面
- 时钟置换算法(CLOCK):使用位u+修改位m
LRU算法知道吗?自己设计怎么设计?
为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰
5.计算机网络
TCP和UDP的区别
- UDP:在IP服务的基础上,仅增加复用、分用,差错检验,无连接不可靠;面向报文(依次交付一个完整报文);首部长度:8B
- TCP:面向连接,点对点,全双工通信;可靠(差错检验,保证有序到达,无丢失);面向字节流;首部长度:20B
TCP三次握手和四次挥手过程
协议层有哪些?说说五层协议
- OSI参考模型:物理层,数据链路层,网络层,传输层,会话层,表示层,应用层
- TCP/IP模型(Internet上使用的协议):网络接口层,网际层,传输层,应用层
- 网络五层模型:物理层,数据链路层,网络层,传输层,应用层
三次握手,建立连接
- 客户端向服务器发送同步请求报文段(不携带数据):SYN=1,seq=x(选择1个初始序列号);客户端进入SYN-SENT状态
- 服务器接收到请求报文段之后,向客户端发送同步确认报文段(不携带数据):SYN=1,ACK=1,seq=y(选择1个初始序列号),ack=x+1(确认收到x序列号);服务器端进入SYN-RCVD状态
- 客户端收到同步确认报文段之后,向服务器端发送确认报文段(可以携带数据):ACK=1,seq=x+1,ack=y+1(确认收到y序列号);客户端和服务器进入ESTABLISHED状态,完成3次握手。
-
四次挥手,释放连接
- 客户端向服务器发送请求终止报文段(停止发送数据):FIN=1,seq=u(之前最后传输字节序号+1);客户端进入FIN-WAIT-1状态
- 服务器接收到连接释放报文段之后,向客户端发送确认报文段(不携带数据):ACK=1,seq=v,ack=u+1(确认收到u序列号);服务器端进入CLOSE-WAIT状态(半关闭),通知高层的应用进程:客户端要释放与服务器端的TCP连接了。客户端收到确认报文段之后,进入FIN-WAIT-2状态:等待服务器发送确认终止报文。
- 服务器将数据发送完毕后,向客户端发送确认终止报文:FIN=1,ACK=1,seq=w(半关闭状态,服务器可能发送了一些数据,假定此时序号为w),ack=u+1;服务器端进入LAST-ACK状态,等待客户端的最终确认。
- 客户端接收到确认终止报文之后,向服务器发送确认报文段:ACK=1,seq=u+1,ack=w+1;客户端进入TIME-WAIT状态(此时客户端的TCP连接还没有释放),等待2MSL(最长报文段寿命)时间之后,连接才真正的释放CLOSED。
- 服务器端只要收到客户端的确认,立即CLOSED状态。
-
TCP为什么是三次握手,不是两次
防止两次握手的情况下,客户端发出的已失效的连接请求报文段突然又传送到了服务器端,产生了错误,因为三次握手时,客户端收到服务器端对失效连接报文段的确认时,可以不予理睬,则连接不会建立。
为什么四次挥手不是三次
- 保证客户端发送的最后一个确认报文段能够到达服务器端,不等待2MSL,则可能服务器端没能进入正常关闭状态,而客户端已经关闭,无法重传。
- 防止出现“已失效的连接请求报文段”,等待2MSL可保证本连接持续的时间内所产生的所有报文段从网络中消失。
TCP最后一次ACK包没有送到就开始传输数据包,会发生什么?
题目的意思应该是说,客户端只经过两次握手就开始发送数据会发生什么,见上上题。
TCP的滑动窗口
- 接受窗口(rwnd):接收方根据接受目前缓存大小所允许的最新窗口值;反映了接收方的容量。
- 拥塞窗口(cwnd):发送方根据自己估算的网络拥塞程度而设置的窗口值,反映了网络当前的容量。
发送窗口的上限值=Min(rwnd,cwnd)
-
ping命令,Traceroute是哪一层
- ping:
- 测试两个主机之间的连通性
- 工作在应用层,直接使用网络的ICMP协议的回送请求和回答报文,没有使用传输层的TCP或UDP协议
- traceroute:
- 跟踪分组经过的路由
- 工作在网络层,使用ICMP协议的时间超时报文
-
说说HTTP协议的特点
- 客户/服务器模式:
- 无连接:HTTP协议本身是无连接的,使用TCP连接
- 无状态:协议对于事务处理没有记忆能力,缺少状态意味着如果后续处理需要前面的信息,则必须重传;通常在用户主机上存储Cookie文本文件,用于Web服务识别用户。
-
说说HTTP的过程
- 浏览器分析链接指向的页面的URL
- 浏览器向DNS请求解析URL的IP地址
- 域名系统DNS解析出IP地址
- 浏览器与服务器建立TCP连接(默认端口号:80)
- 浏览器向服务器发出HTTP请求报文
- 服务器向浏览器发送HTTP响应报文
- TCP连接终止
- 浏览器将响应报文进行解释,并将Web页面显示给用户
-
HTTP常见的状态码
-
GET请求和Post请求的区别
- 报文层面:
- get将请求信息(账号,密码)放在URL中,不安全;
- post将请求信息放在报文体中,安全
- 数据库层面:
- get是查询数据的,符合幂等性(多次操作对数据库的结果是相同的)和安全性(对数据库中的操作没有改变数据库中的数据)
- post是向数据库中提交数据,因此会改变数据库中的数据,post是作用在上一级的URL上的,每一次请求都会添加一份新资源,不符合幂等性
- 其他层面:
- get请求可以被缓存,可以保存在浏览器的浏览记录中,能够保存为书签
- post则不行
-
Cookie和Session的区别
HTTP协议无状态的解决方案
- 客户端解决方案:Cookie是服务器发送给客户端的特殊信息(例如:个人信息),以文本的形式存储在客户端,客户端每次向服务器发送请求时,都会带上这些特殊信息,服务器收到后,会解析Cookie生成与客户端相对应的内容
- 服务器端解决方案:Session是服务器使用的类似于散列表的结构保存信息,当程序需要为某个客户端的请求创建session的时候,服务器首先检查这个请求是否已包含了一个session id,如果已经包含,说明之前已经为该客户端创建过session,服务器就按session id将这个session检索出来使用,不包含session id则为此客户端创建session,并且生成与此session相关的session id,响应时会发给客户端进行保存
- 区别
- Cookie数据存放在本地的浏览器上,Session数据存放在服务器上
- Cookie不是很安全,别人可以分析存放在本地的Cookie,并进行Cookie欺骗
- Session会在一定时间内保存在服务器上,当访问增多会影响服务器的性能
-
HTTP1.0, HTTP1.1和 HTTP2.0的区别
1.1相较1.0最明显的区别是引入了长连接技术
SSL(安全套接层):
为网络通信提供安全及数据完整性的一种安全协议
-
位于TCP与各个应用层协议之间,是操作系统对外的API
-
采用身份认证和数据加密保证网络通信的安全及数据完整性
-
HTTP安全吗?说说HTTPS的改变
-
HTTP:客户端和服务器没有任何身份确认的过程,数据全部明文传输,容易被黑客截获,黑客可冒充服务器,返回任意信息给客户端,而不被客户端察觉
-
HTTPS(超文本传输安全协议):以计算机网络安全通信为目的的传输协议,在HTTP下加入了SSL层,从而具有了保护数据隐私、完整性和提供对网站服务器身份认证的功能。
数据加密(给数据裹上一层外套)
-
数字签名:在信息的后面加上一段内容(经过哈希后的值),可以证明信息没有被修改过
-
哈希算法:将任意长度的信息转化为固定长度的值,算法不可逆,常见:MD5算法
-
非对称加密:加密时用的秘钥(公钥)和解密使用的秘钥(私钥)是不相同的,私钥是保密的
-
对称加密:加密和解密使用同一个秘钥
-
HTTPS数据传输流程(数据传输之前,web浏览器会与服务器进行一次握手,已确定双方的加密密码信息)
- web浏览器将生成的随机密码和支持的加密算法信息发松给服务器
- 服务器选择一套浏览器支持的加密算法,以证书的形式发送给web浏览器
- web浏览器验证证书的合法性,并使用证书中的公钥将信息加密后发送给服务器
- 服务器使用网站本身的私钥将web浏览器发送的信息解密确定密码,验证哈希是否一致,然后服务器会使用密码加密握手信息发送给浏览器
- web浏览器解密响应信息,并计算经过哈希算法加密的握手消息,如果与服务器发送过来的哈希值一致,则此过程结束后,浏览器会使用之前生成的随机密码和对称加密算法进行加密,交互数据。
-
HTTP和HTTPS之间的区别
- HTTPS需要到CA申请证书,HTTP不需要
- HTTPS是密文传输的,HTTP是明文传输的
- 连接方式不同,HTTPS默认使用443端口,HTTP使用80端口
- HTTPS=HTTP+加密+认证+完整性保护,较HTTP安全
-
DNS域名解析原理
- DNS系统:将主机名转换为对应的IP地址,运行在UDP协议之上
- 层次:根域名服务器(知道所有顶级域名服务器的IP地址)==>顶级域名服务器==>权限域名服务器(总是能够将其管辖的主机名转换为IP地址)==>本地域名服务器(主机发出DNS查询请求时,请求报文首先发给本地域名服务器)
- 查询顺序:本地域名服务器==>根域名服务器==>顶级域名服务器==>权限域名服务器
- 递归查询:我帮你去下一级域名服务器上查找
- 迭代查询:我告诉你下一级域名服务器在哪,你自己去找
-
每次DNS域名解析都要请求DNS服务器,是不是很耗时?怎么解决
-
在域名服务器中广泛使用高速缓存:DNS服务器可以将自己查询到的结果信息缓存到高速缓存中。当下次有另一个星通的域名查询到达该服务器时,该服务器能够直接提供所需要的IP地址,而不需要去其他的DNS服务器上查询了。
-
浏览器缓存==>系统缓存==>路由器缓存==>本地域名服务器缓存==>根域名服务器缓存==>顶级域名服务器缓存
-
-
ARP协议原理
- 无论网络层使用什么协议,在输数据链路层传输数据帧都需要使用硬件地址(MAC地址),所以需要一种方法完成IP地址到MAC地址的映射,即地址解析协议ARP
- ARP工作在网络层,因为他能够看到IP地址
- 每个主机都设有一个ARP高速缓存,存放本局域网上各主机和路由器的IP地址到MAC地址的映射表,称为ARP表
- 原理:当主机A欲向本局域网上的某个主机B发送IP数据报时,就先在其ARP高速缓存中查看有无主机B的IP地址。如果有,就可查出其对应的MAC地址,再将此MAC地址写入MAC帧,然后通过局域网将该MAC帧发送到此MAC地址;如果没有,就通过使用目的地址为FF-FF-FF-FF-FF-FF的帧来封装并广播ARP请求分组,可以使得同一个局域网内的所有主机都收到ARP请求。当主机B收到该ARP请求后,就会向主机A发出响应ARP分组,分组中包含主机B的IP地址与MAC地址的映射关系,主机A在收到后就将此映射写入ARP缓存中,然后按查询到的硬件地址发送MAC帧。
-
使用Socket实现一个WebServer(BIO==>伪异步)
public class BIOPlainEchoServer { //=====服务器端=====问题:每收到一个请求就创建一个线程开销比较大 public void serve(int port) throws IOException { //1.将ServerSocket绑定到指定的端口里 final ServerSocket socket = new ServerSocket(port); while (true) { //2.阻塞直到收到新的客户端连接 final Socket clientSocket = socket.accept(); System.out.println("Accepted connection from " + clientSocket); //3.创建一个子线程去处理客户端的请求 new Thread(new Runnable() { @Override public void run() { //4.从客户端socket的输入流中读取数据 try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) { //5.将客户端的请求原封不动回写回去 PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true); while (true) { writer.println(reader.readLine()); writer.flush(); } } catch (IOException e) { e.printStackTrace(); } } }).start(); } } //=====服务器端(改进版:伪异步):引入线程池,将客户端请求提交给线程池去执行===== public void improvedServe(int port) throws IOException { //1.将ServerSocket绑定到指定的端口里 final ServerSocket socket = new ServerSocket(port); //2.=======创建一个固定的线程池======= ExecutorService executorService = Executors.newFixedThreadPool(6); while (true) { //3.阻塞直到收到新的客户端连接 final Socket clientSocket = socket.accept(); System.out.println("Accepted connection from " + clientSocket); //4.将请求提交给线程池去执行 executorService.execute(() -> { //4.从客户端socket的输入流中读取数据 try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) { PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true); //5.将客户端的请求原封不动回写回去 while (true) { writer.println(reader.readLine()); writer.flush(); } } catch (IOException e) { e.printStackTrace(); } }); } } }
-
BIO、NIO、AIO各自的含义、特点,差异体现在哪里
- BIO(Blocking IO):同步阻塞(服务器端需要阻塞等待来自客户端的连接请求)
-
- 每接收到一个来自客户端的连接就创建一个子线程去处理客户端的请求
- 缺点:客户端并发访问量急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务
- 改进(引入线程池):后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N.通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
- NIO(Non-Blocking IO):同步非阻塞(服务器端在从Channel读取数据到Buffer期间可以执行其他操作,在将Buffer中的数据写入Channel期间也可以执行其他操作)
- 它支持面向缓冲(Buffer)的,基于通道(Channel)的I/O操作方法。数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中.
- Buffer:在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。
- Channel:通道是双向的,可读也可写。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
-
Selectors(选择器):NIO的底层使用了操作系统的IO多路复用技术:单线程可以同时处理多个网络IO,调用系统级别的select/poll/epoll模型,由系统进行监控IO状态,例如select轮训可以监控许多个io请求,当有一个socket数据被准备好的时候,就可以返回了。
- 当监测到一个新连接,不需要创建一个线程,而是直接注册到clientSelector
- AIO(Asynchronous IO):异步非阻塞(一旦一个新的客户端请求被接受时,会调用回调函数,通知服务器端执行响应操作)
- 异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
- 基于回调:实现CompletionHandle接口,调用时触发回调函数
- BIO(Blocking IO):同步阻塞(服务器端需要阻塞等待来自客户端的连接请求)
-
用过网络编程吧?用过,说说select和epoll的原理和区别
- 原理:IO多路复用机制。轮训文件描述符(fd),根据文件描述符关联的内核读缓冲区的状态,触发相应的可读或者可写事件,发出消息通知程序进行读写操作。
- 区别:
6.数据库
-
数据库的三大范式
-
第一范式:当关系模式R的所有属性都不能在分解为更基本的数据单位时,称R是满足第一范式的,简记为1NF。满足第一范式是关系模式规范化的最低要求,否则,将有很多基本操作在这样的关系模式中实现不了。
-
第二范式:如果关系模式R满足第一范式,并且R得所有非主属性都完全依赖于R的每一个候选关键属性,称R满足第二范式,简记为2NF。
-
第三范式:设R是一个满足第一范式条件的关系模式,X是R的任意属性集,如果X非传递依赖于R的任意一个候选关键字,称R满足第三范式,简记为3NF.
-
-
理解三大范式
-
第一范式:
-
每一列属性都是不可再分的属性值,确保每一列的原子性
-
两列的属性相近或相似或一样,尽量合并属性一样的列,确保不产生冗余数据。
-
-
第二范式:
-
要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主键
-
-
第三范式:
-
数据不能存在传递关系,即每个属性都跟主键有直接关系而不是间接关系。像:a–>b–>c 属性之间含有这样的关系,是不符合第三范式的。
-
-
-
数据库中数据记录查找?
- 首先Mysql的基本存储结构是页(记录都存在页里面)
- 各个数据页可以组成一个双向链表
- 而每个数据页中的记录又可以组成一个单向链表
- 每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
- 以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。
-
所以说,如果我们写
select * from user where username = 'Java3y'
这样没有进行任何优化的sql语句,默认会这样做:- 定位到记录所在的页:需要遍历双向链表,找到所在的页
- 从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了,主键可使用二分查找
-
为什么索引能够提高查询速度?
- 将无序数据变成有序数据(相对)
-
要找到id为8的记录简要步骤:
-
很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过**“目录”**就可以很快地定位到对应的页上了!其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
-
什么情况下索引不能提高查询速度?
- 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
- 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
- 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
-
mysql查询语句怎么做性能分析
-
数据库索引有哪些优点和缺点,mysql数据库索引是通过什么实现的,相比B+树索引数据结构,有没有更快的索引数据结构,为什么不用更快的数据结构,红黑树的时间复杂度是多少。
-
索引:是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
-
-
在哪些情况下不适合创建索引
- 数据量小的表不需要建立索引,建立会增加额外的索引开销
- 频繁更新的字段不适合建立索引,因为索引的维护需要开销
- where条件中用不到的字段不适合建立索引
- 取值范围较少的字段不适合建立索引
-
最左匹配原则及其成因
-
最左匹配原则:
假设我们有两列a,b,a和b是联合索引,他的顺序是a,b,我们在where语句中调用a=? and b=?的时候就会走联合索引,如果调用where a = ?的时候也会走索引,但是当我们使用where b = ?的时候就不会走这个联合索引 -
成因:
mysql创建复合索引的规则是首先会对复合索引的最左边,也就是索引中的第一个字段进行排序,在第一个字段排序的基础上,在对索引上第二个字段进行排序,其实就像是实现类似order by 字段1,字段2这样的排序规则,那么第一个字段是绝对有序的,而第二个字段就是无序的了,因此一般情况下直接只用第二个字段判断是用不到索引的,这就是为什么mysql要强调联合索引最左匹配原则的原因。
-
-
聚集索引和非聚集索引
- 聚集索引:叶子节点就是数据节点
- 聚簇索引的顺序,就是数据在硬盘上的物理顺序。
- 一张表只允许存在一个聚簇索引,因为真实数据的物理顺序只能有一种。
- 聚簇索引默认是主键,如果表中没有定义主键,InnoDB会选择一个唯一的非空索引代替(“唯一的非空索引”是指列不能出现null值的唯一索引,跟主键性质一样)。如果没有这样的索引,InnoDB会隐式地定义一个主键来作为聚簇索引。
- 非聚集索引:叶子节点仍然是索引节点,只不过有指向对应数据块的指针。查询到达叶子结点后还要进行一次回表查询
- 聚集索引:叶子节点就是数据节点
-
MyISAM和InnoDB在索引上的区别
- MyISAM:非聚集索引
- InnoDB:聚集索引
-
Mysql中delete,drop和truncate的异同
- 相同点:
- truncate和不带where子句的delete、以及drop都会删除表内的数据
- drop、truncate都是DDL语句(数据定义语言),执行后会自动提交
- 不同点:
- truncate和delete只删除数据不删除表的结构(定义),drop会删除依赖于该表结构的约束(constrain)、触发器(trigger)、索引(index);依赖于该表的存储过程/函数将保留,但是变为invalid状态。
- delete语句是数据库操作语言(dml),事务提交之后才生效;如果有相应的trigger,执行的时候将被触发。truncate、drop是数据库定义语言(ddl),操作立即生效,操作不触发trigger。
- 速度:drop>truncate>delete
- truncate table 在功能上与不带 where 子句的 delete 语句相同:二者均删除表中的全部行。但 truncate table 比 delete 速度快,且使用的系统和事务日志资源少。delete 语句每次删除一行,并在事务日志中为所删除的每行记录一项。truncate 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放。
- 相同点:
-
为什么用B+树不用B树
- B树:B树中每个节点包含了键值和键值对于的数据对象存放地址指针,所以成功搜索一个对象可以不用到达树的叶节点。
- 关键字最左边节点中的关键字都小于本关键字;关键字最右边节点中的关键字都大于本关键字;其他孩子节点中的关键都介于该节点两侧关键字大小之间。
- B+树:B+树非叶节点中存放的关键码并不指示数据对象的地址指针,非叶节点只是索引部分。所有的叶节点在同一层上,包含了全部关键码和相应数据对象的存放地址指针,且叶节点按关键码从小到大顺序链接。
- 哈希索引:哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。
- 缺点
- 不支持范围查询,因为经过Hash算法得到的Hash值顺序并不能保证和之前的顺序完全一样
-
数据库无法进行排序
-
不能利用部分索引键进行查询,因为对于组合键,Hash索引先拼接组合键之后计算Hash值
-
不能避免表扫描:对于Hash值冲突的数据,还是要扫描bucket
-
当有大量Hash值发生冲突时,查询效率比较低
- 关系型数据库一般使用B+树作为存储引擎原因
- B+树的磁盘读写代价更低:B+树的键一定会出现在叶结点中,并且有可能在非叶结点中也有可能重复出现,以维持B+树的平衡
- B+树的查询效率更加稳定:B+树的检索无论成功与否,一定要走完树的所有层,时间复杂度是固定的
- B+树更有利于对数据库的扫描:支持对叶节点链表的顺序搜索
- B树:B树中每个节点包含了键值和键值对于的数据对象存放地址指针,所以成功搜索一个对象可以不用到达树的叶节点。
-
Mysql数据库的锁机制
- 共享锁(读锁/S锁):上了共享锁之后,依然支持上共享锁,但不支持上排它锁
- 排他锁(写锁/X锁):上了排它锁之后,就不支持再上其他的锁
- 增删改查时的默认加锁行为
- MyISAM(表级别锁):select操作默认加上表级别的共享锁,update,insert,delete操作默认加上表级别的排他锁
- InnoDB(行级别锁,也支持表级锁):select操作默认不加锁,update,insert,delete操作默认加上行级别的排他锁
- 表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
- 行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
- InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB将使用表锁
- 悲观锁:对系统被外界(本系统当前其他事务+来自外部系统事务)修改持悲观态度,因此在数据处理过程中将数据处于锁定状态,依赖于数据库层提供的锁机制。
- 乐观锁:认为数据一般情况下不会造成冲突,所以在数据进行提交更新时,才会对数据冲突与否进行检测,如果发生冲突,则返回错误信息,让用户决定如何去做。不会使用数据库提供的锁机制。一般就是记录数据版本,使用版本号或者时间戳。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,反之拒绝
-
update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version}
- GAP锁(间隙锁):当我们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
Select * from emp where empid > 100 for update;
上面是一个范围查询,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
-
数据库引擎有哪些?说说MYISAM和InnoDB的区别
-
mysql的事务管理
-
ACID特性
-
Automic(原子性):事务包含的一组操作要么全执行,要么全不执行
-
Consistency(一致性):数据从一个一致性状态到达另一个一致性状态,数据满足完整性约束
-
Isolation(隔离性):事务完成前数据的中间状态对其他事务是不可见的
-
Durabliity(持久性):保证提交的事务对数据库影响的永久性,需要REDO日志重启时恢复
-
-
事务并发访问引起的问题以及使用哪种事务隔离级别避免
- LU(Lost Update丢失更新):一个事物的更新覆盖了另一个事物的更新—-现在主流数据库都为为我们自动加锁,mysql所有事务隔离级别在数据库上均可避免此类问题。
- DR(Dirty Read读取脏数据):一个事务读取了另一个事务更新却没有提交的数据—-在RC(Read Commmited)隔离级别以上均可避免
set session transaction isolation level read commited
原理:把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作
- NRR(Non-Repeatable Reads不可重复读):一个事务对同一个数据项的多次读取操作可能得到不同的结果,事务A多次读取同一数据,事务B在事务A读取数据的过程中,对数据进行了更新和提交,使得事务A多次读取时,得到的数据不一致—-在RR(Repeatable Read)隔离级别以上均可避免,确保在同一事务中,对同一个数据的先后读取的结果是一样的。
set session transaction isolation level repeatable read
原理:事务级别的快照!每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据。
- PR(Phantom Reads幻读):事务A读取与搜索条件相匹配的若干行,事务B以插入或删除行等方式来修改事务A的结果集,导致事务A看起来像出现幻觉一样-—S(Serializable串行化)隔离级别可以避免
set session transaction isolation level serializable
原理:事务的执行是串行的
-
InnoDB可重复读隔离级别下如何避免幻读
- 表象:快照读
- 实质:行锁+GAP锁
-
数据库设计的三范式,可以违反三范式吗
- 第一范式条件(1NF):必须不包含重复组的关系,即每一列都是不可拆分的原子项。
- 第二范式条件(2NF):关系模式必须满足1NF,并且所有非主属性都完全依赖于主键。
- 解决:拆分表格
- 第三范式的条件(3NF):关系模型满足2NF,所有非主属性对任何候选关键字都不存在传递依赖。即每个属性都跟主键有直接关系而不是间接关系,像:a–>b–>c。
-
Redis(分布式缓存,多个实例共用一份缓存数据)
简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
-
Redis使用哪些数据结构?
- string(字符串)
- string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
- string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
- string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
- hash(哈希)–map
- Redis hash 是一个键值(key=>value)对集合。
- Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
- 每个 hash 可以存储 232 -1 键值对(40多亿)
- list(双向列表)
- Redis list是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
- 支持范围查询:lrange
- 列表最多可存储 232 – 1 元素 (4294967295, 每个列表可存储40多亿)
- set(集合)
- Redis 的 Set 是 string 类型的无序集合。
- 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
- 集合中最大的成员数为 232 – 1(4294967295, 每个集合可存储40多亿个成员)。
- zset(sorted set:有序集合)
- Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
- 不同的是每个元素都会关联一个double类型的score。redis正是通过分数来为集合中的成员进行从小到大的排序。
- zset的成员是唯一的,但分数(score)却可以重复。
-
Redis是多线程还是单线程的
- 单线程
-
Redis常用命令
-
redis 和 memcached 的区别
- redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
- 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
- Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
-
redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
- volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key
-
redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)
- 快照(snapshotting)持久化(RDB):将内存中某一时刻的数据状态进行备份
- AOF(append-only file)持久化:每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件
7.Linux相关
-
Linux中远程传输文件有什么方式
-
linux查看负载的命令:ps,top
-
Linux ps命令,以及看内存当前使用状态的命令
1.ps:探查进程,只能显示某个特定时间点的信息
- 默认,ps命令只会显示运行在当前控制台下的属于当前用户的进程,PID,TTY(启动进程的终端),TIME(进程已用CPU时间)
- 可选参数:
- -e:显示所有进程
- -f:显示完整格式的输出
- -l:显示长列表
C列:进程生命周期中的CPU利用率
2.top:实时监测进程,实时显示进程信息
第1行:当前时间,系统运行时间,登录用户数量,系统的平均负载(最近1分钟,5分钟,15分钟)
第2行:进程总数,running状态的进程数,sleeping状态下的进程数,stopped状态下的进程数,zombie(僵化:完成了,但父进程没有响应)
第3行:CPU的利用率(详细:计算linux服务器CPU利用率,性能分析Linux服务器CPU利用率)
CPU时间包含User time、System time、Nice time、Idle time、Waiting time、Hardirq time、Softirq time、Steal time
空闲态时间==idle time
用户态时间==user time+ Nice time。
内核态时间==system time+ Hardirq time+ Softirq time。
user time。指CPU在用户态执行进程的时间。
system time。指CPU在内核运行的时间。
nice time。指系统花费在调整进程优先级上的时间。
idle time。系统处于空闲期,等待进程运行。
waiting time。指CPU花费在等待I/O操作上的总时间,与ed相似。
steal time。指当前CPU被强制(involuntary wait )等待另外虚拟的CPU处理完毕时花费的时间,此时 hypervisor 在为另一个虚拟处理器服务。
Softirq time 、Hardirq time。分别对应系统在处理软硬中断时候所花费的CPU时间。
第4行:系统的物理内存:内存总容量,已使用大小,空闲大小,缓冲区大小
第5行:系统的交换空间:总容量,已使用大小,空闲大小,缓存大小
PR列:进程的优先级
VIRT列:进程占用的虚拟内存总量
RES列:进程占用的物理内存总量
%CPU:进程是用的CPU时间比例
%MEM:进程是用的内存占可用内存的比例
键入f:选择对输出进行排序的字段
键入d:修改轮训间隔
减去q:退出
-
swap了解吗?真正内存包括缓存和cache吗
Swap分区在系统的物理内存(这里应该是运行内存)不够用的时候,把物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap分区中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。
-
top命令你注意哪个指标?(见上上一题)
8.框架
-
MyBatis的底层实现
-
Spring底层实现
-
Spring中@autoware和@resource的区别
-
SpringMVC底层实现
9.分布式
-
CAP原理
- Consistency(一致性):更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致
- Availability(可用性):系统在单台机器发生故障的情况下,仍然能够正常对外提供读写服务
- Partition tolerence(分区可容忍性):分布式系统在遇到某节点或网络分区故障的时候,仍然能够满足一致性和可用性
- CP without A: 舍弃了可用性的分布式系统,一旦网络发生故障或消息丢失的情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统
- AP without C:保证高可用性的分布式系统,需要在用户访问时可以马上得到返回,一旦网络发生问题,则每个节点只能使用本地数据提供服务,而这样会导致全局数据的不一致性
-
Base理论
- Basically Available(基本可用)
- Soft State(软状态)
- Eventual Consistency(最终一致性)
-
分布式一致性协议
-
paxos算法
-
分布式事务
-
2PC
- 请求阶段:
- 协调者请求参与者进行决策(决策是否可以进行事务的提交)
- 参与者执行所有事务操作,并将Undo信息和Redo信息写入日志
- 参与者事务执行成功则向协调者返回“同意”,执行失败则返回“取消”
- 提交阶段
- 当且仅当所有参与者都返回“同意”时,协调者向所有参与者发送“提交(commit)”的请求,参与者正式完成所有操作,并释放在整个事务期间占用的资源,并向协调者发送“完成”消息,协调者接收到所有参与者反馈的“完成”消息后,完成事务。
- 只要有一个参与者返回“取消”,协调者向所有参与者发送“回滚(rollback)”的请求,参与者利用Undo日志执行回滚,并释放在整个事务期间占用的资源,并向协调者发送“回滚完成”消息,协调者接收到所有参与者反馈的“回滚完成”消息后,取消事务。
-
2PC的缺点
- 同步阻塞问题:2PC的执行过程中,所有参与者节点都是事务阻塞型的
- 单点故障:一旦协调者发生故障,参与者会一直阻塞下去
- 数据不一致:部分参与者接收到提交请求提交了事务,而部分参与者未接收到提交请求没有提交事务,系统出现了数据不一致的问题
-
3pc
- CanCommit阶段(请求阶段):
- 协调者请求参与者进行决策(决策是否可以进行事务的提交)
- 参与者执行所有事务操作,并将Undo信息和Redo信息写入日志
- 参与者事务执行成功则向协调者返回“同意”,执行失败则返回“取消”
- PreCommit阶段(预提交)
- 当且仅当所有参与者都返回“同意”时,协调者向所有参与者发送“预提交(PreCommit)”的请求,参与者正式完成所有操作,并向协调者发送ACK响应,同时等待正式提交指令。
- 只要有一个参与者返回“取消”或者协调者等待超时,协调者向所有参与者发送“事务中断(abort)”请求,参与者利用Undo日志执行回滚,并释放在整个事务期间占用的资源,并向协调者发送“回滚完成”消息,协调者接收到所有参与者反馈的“回滚完成”消息后,取消事务。
- DoCommit阶段(正式提交)
- 正式提交:协调者接收到参与者发送的ACK响应后,向所有参与者发送正式提交(DoCommit)指令,参与者正式提交事务,并释放在整个事务期间占用的资源,并向协调者发送“完成”消息,协调者接收到所有参与者反馈的“完成”消息后,完成事务。
- 事务中断:
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/125653.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...