本文旨在收集Java面试过程中出现的问题,力求全面,仅作学习交流,欢迎补充,持续更新中…,部分段落选取自网上,部分引用文章已标注,部分已记不清了,如侵权,联系本人
Java基础
1、面向对象的概述
面向对象是模型化的,只需抽象出一个类,需要什么功能直接使用就可以了,不必去一步一步的实现,至于是如何实现的,不用去管。
面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。
2、 面向对象的特征有哪些方面?
1)抽象:将一类对象的共同特征总结出来构造类的过程
抽象包括两个方面,一是过程抽象,二是数据抽象。
2)继承:使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。
继承是一种联结类的层次模型,它提供了一种明确表述共性的方法。
3)封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
4)多态:
多态是面向对象编程中的一个核心概念,它指的是对象(方法或属性)可以在不同的情况下展现出不同的形态。在Java中,多态主要通过继承和接口实现,并且体现为两个主要形式:编译时多态和运行时多态。
编译时多态(静态多态)
方法重载:在同一个类中,方法名相同但参数列表不同的多个方法。这种多态是在编译时决定的,因为编译器根据方法签名(方法名和参数类型)来确定要调用哪个方法。
public class Example {
void demo(int a) {
System.out.println("a: " + a);
}
void demo(int a, int b) {
System.out.println("a and b: " + a + ", " + b);
}
double demo(double a) {
return a*a;
}
}
运行时多态(动态多态)
方法覆盖(重写):子类重写父类的方法。当子类对象调用该方法时,执行的是子类重写的版本。
向上转型:子类对象可以被当作父类对象使用,但是在调用重写的方法时,实际执行的是子类的版本,这是多态的典型表现。
class Animal {
void sound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Bark");
}
}
public class TestPolymorphism {
public static void main(String args[]) {
Animal obj = new Dog();
obj.sound(); // 输出 Bark,而不是 Some sound
}
}
多态的优势
- 提高代码的可维护性:允许你在不修改现有代码的情况下,添加新的对象类型。
- 提高代码的可扩展性:可以编写通用的代码来处理更广泛的对象类型。
- 接口与实现分离:用户只需要知道对象的接口,而不需要了解具体的实现细节。
使用场景
设计时,通常使用父类类型作为方法的参数或返回类型,这样的方法可以接受父类以及任何子类的对象。
在运行时,JVM 确定实际要调用的方法版本,这取决于对象的实际类型。
3、重写(Override)和重载(Overload)区别
重载:相同的函数名,不同的参数类型或个数,一个类中 (编译时多态)
重写:子类继承父类,重写父类的方法,函数名相同,参数类型和个数均一致。(运行时多态)
构造器(constructor)是否可被重写(override)
构造器不能被继承,因此不能被重写,但可以被重载。
Object 类的常见方法有哪些?
/**
* native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
*/
public final native Class<?> getClass()
/**
* native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
*/
public boolean equals(Object obj)
/**
* native 方法,用于创建并返回当前对象的一份拷贝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
*/
public String toString()
/**
* native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
*/
public final native void notify()
/**
* native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念
*/
public final void wait() throws InterruptedException
/**
* 实例被垃圾回收器回收的时候触发的操作
*/
protected void finalize() throws Throwable { }
hashCode() 有什么用?
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
为什么要有 hashCode?
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashCode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashCode 值作比较,如果没有相符的 hashCode,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashCode 值的对象,这时会调用 equals() 方法来检查 hashCode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
其实, hashCode() 和 equals()都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HashSet的过程)!
我们在前面也提到了添加元素进HashSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode 值相等并不代表两个对象就相等。
那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
总结下来就是:
如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode() 和 equals() 的介绍之后,下面这个问题已经难不倒你们了。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
思考:重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。
总结:
equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。
更多关于 hashCode() 和 equals() 的内容可以查看:Java hashCode() 和 equals()的若干问题解答
4、String 是最基本的数据类型吗
不是,String类不能被继承,是用final修饰的
String和StringBuffer、StringBuilder的区别是什么?String为什么是不可变的
如果要操作少量的数据用 = String 字符串常量
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder 字符串变量
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer 字符串变量
String和StringBuffer主要区别是性能
String是不可变对象,每次对String类型进行操作都等同于产生了一个新的String对象,然后指向新的String对象. StringBuffer是对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,我们建议使用StringBuffer(线程安全)
在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
5、final有哪些特点?
被final修饰的类不可以被继承
被final修饰的方法不可以被重写
被final修饰的变量不可以被改变.如果修饰引用,那么表示引用不可变,引用指向的内容可变.
被final修饰的方法,JVM会尝试将其内联,以提高运行效率
被final修饰的常量,在编译阶段会存入常量池中
6、抽象类和接口的对比
在Java中,抽象类(Abstract Class)、抽象方法(Abstract Method)和接口(Interface)都是用来实现抽象和多态的机制,但它们在使用和功能上有着一些关键区别。
抽象类 (Abstract Class)
定义:一个不能被实例化的类。它可以包含抽象方法和具体方法。
使用场景:当几个类有共同的属性和方法时,且需要提供一个有部分实现的基类时。
特点:
可以包含构造方法。
可以包含具体方法和抽象方法。
可以包含成员变量。
子类继承抽象类必须实现所有抽象方法,除非子类也是抽象类。
一个类只能继承一个抽象类(因为Java不支持多重继承)。
抽象方法 (Abstract Method)
定义:一个没有具体实现的方法,只有方法签名。
使用场景:当你想强制子类实现该方法时。
特点:
必须在抽象类或接口中声明。
没有方法体,以分号结束。
接口 (Interface)
定义:一个完全抽象的类,只能包含抽象方法和静态常量(在Java 8之后,接口可以包含默认方法和静态方法)。
使用场景:当你想指定一个类必须实现哪些方法而不关心其他功能时。
特点:
所有方法默认是公开和抽象的(Java 8之前)。
可以包含默认方法(带有实现的方法,Java 8及之后)。
可以包含静态方法(Java 8及之后)。
接口不能包含成员变量,但可以有公共静态最终(public static final)常量。
一个类可以实现多个接口。
Java 8 之后的变化
在Java 8之后,接口的功能得到了扩展,它们可以包含具有实现的默认方法和静态方法。这样的改变部分地模糊了抽象类和接口之间的界限。默认方法允许在不影响实现类的情况下向接口添加新功能,而静态方法则提供了一种在接口上定义工具方法的方式。
总结
使用抽象类当不同的类有很多共同的方法和属性,需要一个部分实现的基类。
使用接口来定义一个标准或协议,类可以实现一个或多个接口。
Java 8之后,接口通过默认方法和静态方法提供了更多的灵活性。
抽象类和抽象方法在Java中用于定义一个基本的框架,让子类去具体实现。这些特性在面向对象编程中非常有用,特别是在以下场景中:
抽象类的使用场景
**共享代码基础:**当多个类有共同的方法和属性时,可以把这些共通部分放在抽象类中。这样不仅减少了代码重复,还提高了代码的维护性。
**设计模板方法模式:**抽象类可以定义一个操作中的算法骨架,将某些步骤延迟到子类中实现。这种模板方法设计模式在框架设计中非常常见。
**强制子类实现特定方法:**通过抽象方法,抽象类可以强制子类实现这些方法,确保子类遵循特定的接口。
增加灵活性和可扩展性:抽象类提供了一个更灵活的方式来创建类的集合,它们之间共享某些特性,但又各有不同。
抽象方法的使用场景
定义接口规范:抽象方法可以定义一组子类必须实现的接口规范,确保所有子类都有一致的行为。
**设计模式的实现:**在很多设计模式中,如工厂模式、策略模式等,抽象方法被用来定义一组算法或操作的协议。
**控制继承结构:**通过抽象方法,可以精确控制子类的行为和结构,使得继承更加严格和结构化。
7、普通类和抽象类有哪些区别?
• 普通类不能包含抽象方法,抽象类可以包含抽象方法。
• 抽象类不能直接实例化,普通类可以直接实例化。
抽象类能使用 final 修饰吗?
不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类
8、= = 和 equals 的区别是什么
= =: 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals(): 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
9、Java数据类型
分类
• 基本数据类型
o 数值型
整数类型(byte,short,int,long)
浮点类型(float,double)
o 字符型(char)
o 布尔型(boolean)
• 引用数据类型
o 类(class)
o 接口(interface)
o 数组([])
10、&和&&的区别
&是一个位运算符。
&&是一个逻辑运算符。
11、final finally finalize区别
• final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表
示该变量是一个常量不能被重新赋值。
• finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块
中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
• finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调
用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
12、集合
12.1 Set的去重原理
HashSet是依赖本身的数据结构进行去重的。
过程:
1.当把一个对象添加到HashSet中时,得到对象的哈希值(hashCode),用这个值跟集合中的所有元素进行比较
2.如果hashCode值不同,就认为这两个对象是不同的,就可以把这个元素添加到集合中去
3.如果hashCode值和集合中的某个元素相同,这时不能直接判断该对象就和那个元素一致,而是要接着判断该对象的地址值和调用该对象的 equals() 方法
二者中有一个返回值为true,就认为该对象和集合中的对象是重复的,因此不添加
二者返回值均为false,就认为该对象和集合中的对象不相同,因此可以添加
12.2 ArrayList与LinkedList区别
ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
12.3 HashMap和HashTable的区别
相同点:都是存储key-value键值对的
不同点:
• HashMap允许Key-value为null,hashTable不允许;
• hashMap没有考虑同步,是线程不安全的。hashTable是线程安全的,给api套上了一层synchronized修饰;
• HashMap继承于AbstractMap类,hashTable继承与Dictionary类。
• 迭代器(Iterator)不同。HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException。
• 容量的初始值和增加方式都不一样:HashMap默认的容量大小是16;增加容量时,每次将容量变为"原始容量x2"。Hashtable默认的容量大小是11;增加容量时,每次将容量变为"原始容量x2 + 1";
添加key-value时的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法。Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
12.4 TreeMap使用场景 (红黑树 时间复杂度都为 O(logn))
• 需要基于排序的统计功能
• 需要快速增删改查的存储功能
• 需要快速增删改查而且需要保证遍历和插入顺序一致的存储功能
13、HashMap
13.1 HashMap的底层原理是什么?
jdk8后采用数组+链表+红黑树的数据结构
通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象
当hash表的单一链表长度超过 8 个的时候,链表结构就会转为红黑树结构。
• 红黑树查询:其访问性能近似于折半查找,时间复杂度 O(logn);
• 链表查询:这种情况下,需要遍历全部元素才行,时间复杂度 O(n);
GPT
HashMap 是 Java 中广泛使用的数据结构之一,它基于哈希表的原理实现。理解 HashMap 的底层原理对于编写高效的 Java 程序非常重要。以下是 HashMap 的主要特点和工作原理:
基本特点
键值对:HashMap 存储键值对,每个键映射到一个值。
无序:它不保证映射的顺序,顺序可能随时间变化。
键的唯一性:每个键在 HashMap 中必须是唯一的。
允许null值:HashMap 允许将 null 作为键和值。
内部结构
数组和链表(或红黑树):HashMap 内部使用一个数组来存储数据。每个数组元素是一个链表的头节点。当链表长度超过一定阈值时,链表转换为红黑树,以提高搜索效率。
哈希函数:HashMap 通过哈希函数来决定键的存储位置。当两个不同的键产生相同的哈希值时,会发生哈希冲突。
工作原理
存储元素(put操作):
首先,计算键的哈希码。
然后,使用哈希码的某些位作为数组的索引。
如果该索引位置没有元素,直接存储键值对。
如果有元素(即发生哈希冲突),则添加到链表或红黑树的末尾。
检索元素(get操作):
计算键的哈希码,找到对应的数组索引。
在该索引处的链表或红黑树中搜索具有指定键的节点。
扩容:
当 HashMap 中的元素数量达到容量和负载因子的乘积时,会发生扩容。
扩容会创建一个新的更大的数组,并重新计算每个元素在新数组中的位置。
13.2 HashMap的特性?
1.HashMap存储键值对实现快速存取,允许为null。key值不可重复,若key值重复则覆盖。
2.非同步,线程不安全。
3.底层是hash表,不保证有序(比如插入的顺序)
13.3 HashMap为什么比数组查询快
hashmap使用数组加链表的数据结构,数组存放键的信息,但并不保存键本身,而是通过键生成一个数字作为数组的下标,这个数字就是散列码,由hashCode()方法生成,由于数组是固达大小的,所以在固定数组容量的情况下,可能会出现不同的键生成相同的下标,也就可能会产生hash冲突,通常,冲突由外部链表处理,数组不保存值,而是保存值的list,然后对list中的值使用equals()方法进行线性的查询, 如果散列函数好的话,数组的每个位置就会有较少的值, 这样就不是查询整个list,而是快速跳转到数组的某个位置,只对较少的元素进行比较。这便是HashMap如此快的原因。
13.4 Hash冲突解决方法
- 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
2.再哈希法
3.链地址法(Java hashmap就是这么做的)
4.建立一个公共溢出区
13.5 在使用 HashMap 的时候,用 String 做 key 有什么好处?
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
13.6 并发安全map(CouncurrentHashMap)
JDK在1.7和1.8中的区别
1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。
Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。
3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
参考 https://blog.csdn.net/qq_22343483/article/details/98510619
13.7、map的扩容机制
参考 https://blog.csdn.net/qq_39736597/article/details/113726067
14、进程,线程
14.1 进程,线程之间的区别?
进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是cpu调度和分派的基本单位.
多个线程共享内存资源,减少切换次数,从而效率更高.
一个程序至少有一个进程,一个进程至少有一个线程.进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高.线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位.同一进程中的多个线程之间可以并发执行.
1)一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
*优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
14.2 什么是线程安全?
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在调用代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
14.3 实现线程安全
• 线程封闭
• 无状态的类
• 让类不可变
• volatile
• 加锁和CAS
• 安全的发布
• ThreadLocal
14.4 进程之间是如何通讯的(管道,消息队列,共享内存,信号量,Socket)
1. 管道
这种通信方式是单向的,只能把第一个命令的输出作为第二个命令的输入,如果进程之间想要互相通信的话,那么需要创建两个管道。
2.消息队列
例如 a 进程要给 b 进程发送消息,只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的
消息队列里取出来。同理,b 进程要个 a 进程发送消息也是一样。
缺点:如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味发送消息(拷贝)这个过程需要花很多时间来读内存。
3.共享内存
系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间。那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制了。
4.信号量
信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。
5.Socket
例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。
14.5 死锁与活锁的区别,死锁与饥饿的区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成
的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件:
1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,
失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而
处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执
行的状态。
Java 中导致饥饿的原因:
1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前
持续地对该同步块进行访问。
3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方
法),因为其他线程总是被持续地获得唤醒。
14.6 Java 中用到的线程调度算法是什么?
采用时间片轮转的方式。可以设置线程的优先级,会映射到下层的系统上面的优
先级上,如非特别需要,尽量不要用,防止线程饥饿
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占
用的 CPU 的时间片这个也比较好理解。
java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用
CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用
CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。
14.7 什么是阻塞队列?阻塞队列的实现原理是什么?如何使用
阻塞队列来实现生产者-消费者模型?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。
这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当
队列满时,存储元素的线程会等待队列可用。
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消
费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者
也只从容器里拿元素。
阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线
程不断将数据放入队列,然后解析线程不断从队列取数据解析。
14.8 多线程同步和互斥有几种实现方法,都是什么?
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程
的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若
干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它
要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成
是一种特殊的线程同步。
线程间的同步方法大体可分为两类:用户模式和内核模式。顾名思义,内核模式
就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,
而用户模式就是不需要切换到内核态,只在用户态完成操作。
用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。内核模
式下的方法有:事件,信号量,互斥量。
14.9 为什么我们调用 start()方法时会执行 run()方法,为什么
我们不能直接调用 run()方法?
当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。
但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,
只会把 run 方法当作普通方法去执行。
14.10 什么是不可变对象,它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即
对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可
变类,如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。
不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然
它们的状态无法修改,这些常量永远不会变。
14.11 java 中有几种方法可以实现一个线程?
继承 Thread 类
实现 Runnable 接口
实现 Callable 接口,需要实现的是 call() 方法
14.12 如何停止一个正在运行的线程?
使用共享变量的方式
在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的
线程用来作为是否中断的信号,通知中断线程的执行。
使用 interrupt 方法终止线程
14.13 notify()和 notifyAll()有什么区别?
当一个线程进入 wait 之后,就必须等其他线程 notify/notifyall,使用 notifyall,可
以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能
唤醒一个。
14.14 java 如何实现多线程之间的通讯和协作?
中断 和 共享变量
Object 类中wait()\notify()\notifyAll()方法可以用于线程间通信关于资源的锁的状态。
14.15 什么叫线程安全?servlet 是线程安全吗?
线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够
正确地处理多个线程之间的共享变量,使程序功能正确完成。
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个
方法,是不能保证共享变量的线程安全性的。
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一
个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?不是的,
14.16 volatile 有什么用?能否用一句话说明下 volatile 的应用
场景?
volatile 保证内存可见性和禁止指令重排。
volatile 用于多线程环境下的单次操作(单次读或者单次写)。
14.17 在 java 中 wait 和 sleep 方法的不同?
最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线
程间交互,sleep 通常被用于暂停执行。
14.18 Java 中的同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发
集合的可扩展性更高。
14.19 什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变
长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候
就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。
14.20 Java 线程池中 submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在
Executor 接口中。
而 submit()方法可以返回持有计算结果的 Future 对象,
14.21 为什么 wait(), notify()和 notifyAll ()必须在同步方法或
者同步块中被调用?
当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接
着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()
方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的
锁,以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要
线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者
同步块中被调用
14.22 并发编程三要素?
1、原子性
原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操
作打断,要么就全部都不执行。
2、可见性
可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他
线程可以立即看到修改的结果。
3、有序性
有序性,即程序的执行顺序按照代码的先后顺序来执行。
14.23 多线程的价值?
1、发挥多核 CPU 的优势
多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多
线程的方式去同时完成几件事情而不互相干扰。
2、防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因
为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但
是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使
用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未
返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。
多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数
据阻塞,也不会影响其它任务的执行。
3、便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么
就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成
几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运
行这几个任务,那就简单很多了。
14.24 创建线程的有哪些方式?
1、继承 Thread 类创建线程类
2、通过 Runnable 接口创建线程类
3、通过 Callable 和 Future 创建线程
4、通过线程池创建
14.25 线程池的优点?
1、重用存在的线程,减少对象创建销毁的开销。
2、可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞
争,避免堵塞。
3、提供定时执行、定期执行、单线程、并发数控制等功能。
14.26 volatile 关键字的作用
对于可见性,Java 提供了 volatile 关键字来保证可见性。
当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当
有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详
细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
14.27什么是 CAS
CAS 是 compare and swap 的缩写,即我们所说的比较交换。
cas 是一种基于锁的操作,而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲
观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访
问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通
过给记录加 version 来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如
果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。
14.28 CAS 的问题
1、CAS 容易造成 ABA 问题
一个线程 a 将数值改成了 b,接着又改成了 a,此时 CAS 认为是没有变化,其实
是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次
version 加 1。在 java5 中,已经提供了 AtomicStampedReference 来解决问题。
2、不能保证代码块的原子性
CAS 机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。
比如需要保证 3 个变量共同进行原子性的更新,就不得不使用 synchronized 了。
3、CAS 造成 CPU 利用率增加
之前说过了 CAS 里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu
资源会一直被占用。
14.29 什么是 AQS
AQS 是 AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步
工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管
理这个同步状态。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广
泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如
ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于
AQS 的。
AQS 支持两种同步方式:
1、独占式
2、共享式
14.30 线程 B 怎么知道线程 A 修改了变量
1、volatile 修饰变量
2、synchronized 修饰修改变量的方法
3、wait/notify
4、while 轮询
25、synchronized、volatile、CAS 比较
1、synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
2、volatile 提供多线程共享变量可见性和禁止指令重排序优化。
3、CAS 是基于冲突检测的乐观锁(非阻塞)
14.31 多线程同步有哪几种方法?
Synchronized 关键字,Lock 锁实现,分布式锁等。
14.32 怎么唤醒一个阻塞的线程
如果线程是因为调用了 wait()、sleep()或者 join()方法而导致的阻塞,可以中断线
程,并且通过抛出 InterruptedException 来唤醒它;如果线程遇到了 IO 阻塞,
无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接接触到操作系统。
14.33 什么是多线程的上下文切换
多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个
就绪并等待获取 CPU 执行权的线程的过程。
14.34 什么是自旋
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等
待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核
态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线
程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多
次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
14.35 单例模式的线程安全性
1、饿汉式单例模式的写法:线程安全
2、懒汉式单例模式的写法:非线程安全
3、双检锁单例模式的写法:线程安全
14.36 同步方法和同步块,哪个是更好的选择?
同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代
码的效率。请知道一条原则:同步的范围越小越好。
14.37 Java 线程数过多会造成什么异常?
1、线程的生命周期开销非常高
2、消耗过多的 CPU 资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空
闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU
资源时还将产生其他性能的开销。
3、降低稳定性
JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,
并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的
大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出
OutOfMemoryError 异常。
14.38 创建线程的方式
1、继承于Thread类
创建一个继承于Thread类的子类
重写Thread类的run方法
创建thread子类的对象
通过此对象调用start方法
2、实现Runnable接口
创建一个实现了Runnable接口的类
实现类去实现Runnable中的抽象方法:run
创建实现类的对象
将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
通过Thread类的对象调用start
3、实现callable接口
与使用runnable方式相比,callable功能更强大些: runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值 方法可以抛出异常 支持泛型的返回值 需要借助FutureTask类,比如获取返回结果
4、通过线程池
线程的Api
15、类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤。所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
1、加载 load
就是指将类型的class字节码数据读入内存。
2、连接 link
1)验证:校验合法性等
2)准备:准备对应的内存(方法区),创建Class对象,为类变量赋默认值
3)解析:把字节码中的符号引用替换为对应的直接地址引用
3、初始化
initalize(类初始化)即执行类初始化方法,大多数情况下,类的加载就完成了类的初始化,有些情况下,会延迟类的初始化。
16、哪些操作会导致类初始化
1、运行主方法所在的类,要先完成初始化,再执行main方法
2、第一次使用某个类型就是在new他的对象,此时这个类没有初始化的话,先完成 类初始化再做实例初始话
3、调用某个类的静态成员(类变量和类方法),此时这个类没有初始化的话,先完成初始化 4、子类初始化时,发现他的父类还没有初始化的话,那么先初始化父类
5、通过反射操作某个类时,如果这个类没有初始化,也会导致该类先初始化
Redis
1、为什么使用redis
性能和并发 主要起缓存作用
大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库。
Redis
Redis使用场景
1、 会话缓存(Session Cache)
优势在于:Redis提供持久化。场景:购物车信息
2、 全页缓存(FPC)
除基本的会话token之外,Redis还提供很简便的FPC平台。回到一致性问题,即使重启了Redis实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,
3、 队列
优点是Redis提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用
4、 排行榜/计数器
Redis提供集合(Set)和有序集合(Sorted Set)也使得我们在执行递增或递减这些操作的时候变的非常简单
5、 发布/订阅
针对数据类型
String:一般做一些复杂的计数功能的缓存;
Hash:存储二维数据或对象;
List:可实现队列,栈及有序的数据存储;
Set:常用于黑名单,微信抽奖等功能,应用场景多变;
SortedSet:做排行榜应用,取TOPN(排序)操作;延时任务;做范围查找。
3、Redis优缺点
优点:
1、 支持多种数据类型 string , list,hash,set,zset
2、 持久化存储 rdb和aof
3、 丰富的特性 pub/sub,key过期策略,事务,支持多个DB等
4、 性能高 全内存操作,所以读写性能很好 10W+/s
缺点:
1、单台机器,存储的数据量小,取决于单机内存
2、如果进行完整重同步,由于需要生成rdb文件,并进行传输,会占用主机的CPU,并会消耗现网的带宽
3、重启Redis,将硬盘中的数据加载进内存,时间比较久
• 缓存和数据库双写一致性问题
• 缓存雪崩问题
• 缓存击穿问题
• 缓存的并发竞争问题
4、Redis 内存 和 key 过期策略
内存过期策略:
1、 配置文件中我们使用 maxmemory-policy 来配置策略
策略实行过程:Redis 会检查内存的使用情况,如果已经超过的最大限制,就是根据配置的内存淘汰策略去淘汰相应的 key,从而保证新数据正常添加
基于近似的LRU算法
LRU算法:
LRU(Least Recently Used)是一种常见的页面置换算法,在计算中,所有的文件操作都要放在内存中进行,然而计算机内存大小是固定的,所以我们不可能把所有的文件都加载到内存,因此我们需要制定一种策略对加入到内存中的文件进项选择。
LRU的设计原理就:当数据在最近一段时间经常被访问,那么它在以后也会经常被访问。这就意味着,如果经常访问的数据,我们需要然其能够快速命中,而不常访问的数据,我们在容量超出限制内,要将其淘汰。
LRU所使用数据结构: 链表+Hash表
2、 key过期策略
设置带有过期时间的 key
Redis 如何清除带有过期时间的 key
采用定时加惰性策略
定时策略:给每个 key 加一个定时器,这样当时间到达过期时间的时候就自动删除 key(对内存友好,对cpu不友好,占用很多cpu资源)
惰性策略:在每次访问一个 key 的时候再去判断这个 key 是否到达过期时间了,过期了就删除掉(对cpu友好,有一个缺点,如果对某个key不在访问,那么这个key永远不会被删除)
5、Redis 缓存和数据库双写一致性问题,择优解决方案
数据库和缓存双写,必然会存在不一致的问题
一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。
强一致性:不能放缓存。
最终一致性:保证结果即可,有几种更新策略如下。
- 先删缓存,再更新数据库(定时策略会在定时区间产生脏数据)
- 先更新数据库,再删缓存(可能会产生脏数据,可利用消息队列优化,最优方案)
- 先更新数据库,再更新缓存(线程不安全,脏数据,基本不考虑)
6、单线程的Redis为什么这么快?
1.纯内存操作
2.单线程操作,避免了频繁的上下文切换
3.采用了非阻塞I/O多路复用机制
7、Redis RDB AOF 持久化策略
Rdb: rdb相当于保存数据的快照
Aof: 相当于日志记录操作命令,日志数据全保存
redis有两种持久化方式,aof和rdb,aof相当于日志记录操作命令,rdb相当于数据的快照。安全性来讲由于aof的记录能够精确到秒级追加甚至逐条追加,而rdb只能是全量复制,aof明显高于rdb。但是从性能来讲rdb就略胜一筹,rdb是redis性能最大化的体现,它不用每秒监控是否有数据写入,当达到触发条件后就自动fork一个子进程进行全量更新,速度也很快。容灾回复方面rdb更是能够快速的恢复数据,而aof需要读取再写入,相对慢了很多。
选择AOF更好,数据安全最重要
Rdb全量复制会影响性能吗?如果每隔30秒执行一次,会怎样?
8、如何解决redis缓存穿透,缓存击穿,缓存雪崩问题
缓存穿透:key对应的数据在数据源并不存在
缓存击穿:key对应的数据存在,但在redis中过期
缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效
解决方案:
缓存穿透:
利用布隆过滤器,或者如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟
缓存击穿:
1.利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试
2.采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做缓存预热(项目启动前,先加载缓存)操作。
3.提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。如果不合法,则直接返回。
缓存雪崩:
1.给缓存的失效时间,加上一个随机值,避免集体失效。
2.使用互斥锁,但是该方案吞吐量明显下降了。
3.双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点
注:互斥锁:某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。
布隆过滤器:利用散列表原理的一种算法
9、如何解决redis的并发竞争key问题
redis事务(鸡肋,因为redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上),
加分布式锁,队列
10、redis常见性能问题和解决方案:
(1) Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
(2) 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…
11、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
答:使用 keys 指令可以扫出指定模式的 key 列表。
对方接着追问:如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会
有什么问题?
这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线
程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时
候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但
是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间
会比直接用 keys 指令长
12、MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如
何保证 redis 中的数据都是热点数据?
答:Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
相关知识:Redis 提供 6 种数据淘汰策略
从已设置过期时间的数据集挑选最近最少使用的数据淘汰;从已设置过期时间的数据集挑选将要过期的数据淘汰;从已设置过期时间的数据集任意选择数据淘汰;从数据集中挑选最近最少使用的数据淘汰;从数据集中任意选择数据淘汰;禁止驱逐数据
13、使用过 Redis 分布式锁么,它是什么回事?
先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了
释放
使用Spring redisTemplate的实现
使用Redis来实现分布式锁,其方法是采用Redis String 的 SET进行实现,SET 命令的行为可以通过一系列参数来修改:
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
Redis集群
* 主从模式
在主从复制中,数据库分为两类:主数据库(master)和从数据库(slave)。
当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照文件和命令后加载快照文件和缓存的执行命令。
复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据一致性。
缺点:
从上面可以看出,master节点在主从模式中唯一,若master挂掉,则redis无法对外提供写服务。
* Sentinel(哨兵)模式
作用就是监控redis集群的运行状况,高可用
特点:
sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义
当master挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master
当master重新启动后,它将不再是master而是做为slave接收新的master的同步数据
sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群
多sentinel配置的时候,sentinel之间也会自动监控
当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不需要担心
一个sentinel或sentinel集群可以管理多个主从Redis,多个sentinel也可以监控同一个redis
sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了
工作机制:
每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令
如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被sentinel标记为主观下线。
如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master的确进入了主观下线状态
当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态, 则master会被标记为客观下线
在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;
若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除
* Cluster模式
当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中。cluster模式的出现就是为了解决单机Redis容量有限的问题,将Redis的数据根据一定的规则分配到多台机器
cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容。
使用集群,只需要将redis配置文件中的cluster-enable配置打开即可。每个集群中至少需要三个主数据库才能正常运行,新增节点非常方便。
特点:
多个redis节点网络互联,数据共享
所有的节点都是一主一从(也可以是一主多从),其中从不提供服务,仅作为备用
不支持同时处理多个key(如MSET/MGET),因为redis需要把key均匀分布在各个节点上,
并发量很高的情况下同时创建key-value会降低性能并导致不可预测的行为
支持在线增加、删除节点
客户端可以连接任何一个主节点进行读写
数据库
1、索引的缺点/使用
缺点:Mysql在更新时候增加了更新所带来的 IO 量和调整索引所致的计算量,导致存储空间资源消耗的增加 ,进而事务时候影响效率
使用:创建,添加,删除
2、索引失效的情况
1、没有查询条件;
2、在查询条件上没有使用引导列;
3、索引本身失效;
4、查询条件使用函数在索引列上;
5、查询条件中索引列有运算;
6、查询条件中有or或者like以%开头;
7、需要类型转换
8、查询条件中索引列有运算
9、如果mysql觉得全表扫描更快时(数据少)
10、提示不使用索引。
3、索引的优缺点
优点:
1、所有的MySql列类型(字段类型)都可以被索引,也就是可以给任意字段设置索引。
2、大大加快数据的查询速度。
缺点:
1、创建索引和维护索引要耗费时间,并且随着数据量的增加所耗费的时间也会增加。
2、索引也需要占空间,我们知道数据表中的数据也会有最大上线设置的,如果我们有大量的索引,索引文件可能会比数据文件更快达到上线值。
3、当对表中的数据进行增加、删除、修改时,索引也需要动态的维护,降低了数据的维护速度。
对大数据量存储的解决:
索引表与原表分开储存,一张索引表,一张数据表,查询时先根据索引表对数据表只做主键查询
4、mysql两种引擎区别
MyISAM:
- 不支持事务,但是每次查询都是原子的;
- 支持表级锁,即每次操作是对整个表加锁;
- 存储表的总行数;
- 一个MYISAM表有三个文件:索引文件、表结构文件、数据文件;
- 采用菲聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。
InnoDb: - 支持ACID的事务,支持事务的四种隔离级别;
- 支持行级锁及外键约束:因此可以支持写并发;
- 不存储总行数;
- 一个InnoDb引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;
- 主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。
5、mysql普通索引,组合索引
单列索引,即一个索引只包含单个列,一个表可以有多个单列索引,但这不是组合索引;(当一个表中查询大的情况下,where条件中有多个,如果使用多个单列索引,根据mysql优化器策略,造成可能只使用一个索引,其他索引会失效,导致会全盘扫描表,具体看下面链接)
组合索引,即一个索包含多个列。(当一个表中查询大的情况下,where条件中有多个,那么可以使用组合查询,不会扫描表,直接从索引中获取,查询效率高)
注:如果where条件第一个参数取范围值,会导致索引失效(>或者<等相关范围查询),后面的索引也会失效。
6、Mysql使用的数据结构
二叉树:二叉树就是将你要存的数据做一个比较,先存一个数据作为根节点,然后在做插入操作的话是先去跟这个根节点做比较,比根节点小的放在左边,大的放在右边。
优点:解决了我们没有数据结构时,大量引用对象占用大量堆内存空间导致频繁fullGc导致我们程序无法正常使用,用户体验不良好。
缺点:这棵树他是固定根节点的,所以我们在找一个有序数据的时候,比如1,2,3,4,5,6我找5的时候要经过的io操作比较多。因为他找的时候,实际上是找到你内存地址指针,然后在去你的内存中拿数据。
红黑树:是在二叉树的基础上增加了一个平衡的功能,你插入1,2,3的时候他默认选择最中间的元素作为根节点,达到一个平衡的效果。查找数据时只需要拿出我需要找的数据跟根节点做一个对比,如果比根节点大我就往右边找,如果比根节点小我就往左边找,提高了查找的效率。
优点:提高了查找效率。
缺点:树的高度不可控,如果我有大量数据的时候,我需要一直往下找,才能找到我想要的数据,是很费时间的。
红黑树的时间复杂度 O(lgn)
B-Tree:就是在红黑树的基础上优化了树的一个高度,因为红黑树他插入数据是往下面加的,B-Tree他在插入的时候,他会根据你树的高度来平衡你的数,一般控制在3-4行。比如说你达到了3-4行的时候,他做一次平衡操作。平衡时,他将你平衡过来的数据往横向增加,不会再往下面增加了。
优点:B-Tree做了一个高度的平衡,可以解决数据量较大的情况。
缺点:B-Tree在存数据的时候是每个节点都有自己的数据,因为我们MySQL的索引是控制在16kb,如果你用B-Tree作为索引底层的话,其实算下来一棵树存的数据并不是很大。所以他达到一定的大小时,他会从新开辟一个索引树给你去使用,那么我大数据的时候,我要找的数据比较靠后,我是不是得扫描几个索引树?不利于查找效率。并且不适合做范围查询。
B+Tree是在B-Tree上的数据存储节点上做了一个优化,优化成为,不是子节点不存数据,并且为了兼容范围查询,他把节点改成了冗余的,其实不单单是从为了兼容范围查询来说,因为他需要让我这一棵索引树能存储的数据更多,所以他设置成子节点存储数据,子节点冗余来说的话只是MySQL对于B+Tree做的一个优化,这里我们称为变种B+Tree。为甚需要子节点冗余呢?如果我设置的是子节点才存数据,那么我子节点不冗余,我的数据是不是存储就丢失了呢?所以MySQL大叔考虑的非常周到。让我们的子节点冗余
优点:相对于B-Tree来说,我这颗索引树能存储的数据跟多,更加良好的支持范围查询。
B+Tree的时间复杂度log n
聚集索引、非聚集索引和辅助索引
聚集索引存在于Innodb存储引擎,原理:在你创建表的时候选择innodb存储引擎,其实在底层是将表结构数据存放在一起的,另外还有一个文件是存储你的索引数据结构的。
非聚集索引,就是将表结构、数据,索引全部分离开来。非聚集索引典型的代表就是MySAM
辅助索引,其实就是我们在已经创建好的表上面建立的索引。一般这类索引创建于Where条件使用比较多的字段上,不宜多建,因为如果你建立的索引太多的话,你做增删改查的时候,MySQL不仅仅是将你的数据插入到表中,他还需要去维护多颗索引树。
覆盖索引,何为覆盖索引?覆盖索引其实就是在你的查询语句中,你要查的字段都存在于索引中。比如我又id , name ,age,time 这四个字段。id列有一个主键索引,name,age两个字段是联合索引(联合索引下面有讲)那么我SQL语句是Select id , name ,age from demo的时候,这三个字段都存在于我们的索引树中,所以他走的是联合索引。执行原理其实就是id列放弃走主键索引,默认执行联合索引,直接在联合索引中拿数据。
联合索引就是一条创建索引的SQL语句上面有多个字段存在于索引中。
7、explan执行计划
重点:
type:访问类型,查看SQL到底是以何种类型访问数据的。
key:使用的索引,MySQL用了哪个索引,有时候MySQL用的索引不是最好的,需要force index()。
rows:最大扫描的列数。
extra:重要的额外信息,特别注意损耗性能的两个情况,using filesort和using temporary。
8、MySQL 如何优化 DISTINCT?
DISTINCT 在所有列上转换为 GROUP BY,并与 ORDER BY 子句结合使用。
9、实践中如何优化 MySQL
最好是按照以下顺序优化:
1、SQL 语句及索引的优化
2、数据库表结构的优化
3、系统配置的优化
4、硬件的优化
10、什么叫视图?游标是什么?
答:视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,
查,操作,视图通常是有一个表或者多个表的行或列的子集。对视图的修改不影
响基本表。它使得我们获取数据更容易,相比多表查询。
游标:是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元
中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。
一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。
11、 mysql事务隔离级别
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted) 是 是 是
不可重复读(read-committed) 否 是 是
可重复读(repeatable-read) 否 否 是
串行化(serializable) 否 否 否
事务控制语句:
• BEGIN或START TRANSACTION:显式的开启一个事物。
• COMMIT:也可以使用COMMIT WORK,不过二者是等价的。COMMIT会提交事务,并使已对数据库进行的所有修改成为永久性的。
• Rollback:也可以使用Rollback work,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
• SAVEPOINT identifier:SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有很多个SAVEPOINT;
• RELEASE SAVEPOINT identifier:删除一个事物的保存点,当没有指定的保存点时,执行该语句会抛出一个异常。
• ROLLBACK TO inditifier:把事务回滚到标记点。
• SET TRANSACTION:用来设置事务的隔离级别。InnoDB存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERLALIZABLE。
12、事务的并发问题(脏读,幻读,不可重复读)
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据就是脏数据
2、不可重复读:事务A多次读取同一事物,事务B在事务A多次读取的过程中,对数据做了更新并提交,导致事务A多次读取同一数据时,结果不一致。
3、幻读:系统管理员A将数据库中的所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
13、什么是事务
一般来说,事务必须满足四个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability).
• 原子性:一个事物(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始的状态,就像这个事务从来没有执行过一样。
• 一致性:在事务开始之前和事务结束之后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
• 隔离性:数据库允许多个事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同的级别,包括读未提交(Read uncommitted)、读已提交(Read committed)、可重复读(repeateable read)和串行化(Serializable).
• 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
14、主键索引和普通索引区别
主键索引和普通索引的区别
1, 主键索引索引着数据,然后普通索引索引着主键ID值(这是在innodb中,但是如果是myisam中,主键索引和普通索引是没有区别的都是直接索引着数据)
2,当你查询用的是where id=x 时,那只需要扫描一遍主键索引,然后拿到相应数据
但是如果是查询的普通索引的话,那么会先扫描一次普通索引,拿到主键值,然后再去扫主键索引,拿到所需要的数据,这个过程叫做回表
15、mysql有哪些索引类型
按照逻辑分类,索引可分为:
• 主键索引:一张表只能有一个主键索引,不允许重复、不允许为 NULL;
• 唯一索引:数据列不允许重复,允许为 NULL 值,一张表可有多个唯一索引,但是一个唯一索引只能包含一列,比如身份证号码、卡号等都可以作为唯一索引;
• 普通索引:一张表可以创建多个普通索引,一个普通索引可以包含多个字段,允许数据重复,允许 NULL 值插入;
• 全文索引:让搜索关键词更高效的一种索引。
按照物理分类,索引可分为:
• 聚集索引:一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为 NULL 的唯一索引,如果还是没有的话,就采用 Innodb 存储引擎为每行数据内置的 6 字节 ROWID 作为聚集索引。每张表只有一个聚集索引,因为聚集索引的键值的逻辑顺序决定了表中相应行的物理顺序。聚集索引在精确查找和范围查找方面有良好的性能表现(相比于普通索引和全表扫描),聚集索引就显得弥足珍贵,聚集索引选择还是要慎重的(一般不会让没有语义的自增 id 充当聚集索引);
• 非聚集索引:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同(非主键的那一列),一个表中可以拥有多个非聚集索引。
Mybatis
1.mybatis执行原理
MyBatis 的工作原理:读取 MyBatis 配置文件、加载映射文件、构造会话工厂、创建会话对象、Executor 执行器、输入参数映射、输出结果映射。
mybatis原理具体介绍如下:
1、读取 MyBatis 配置文件:
mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
2、加载映射文件:
映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
3、构造会话工厂:
通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
4、创建会话对象:
由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
5、Executor 执行器:
MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的.参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
2、Mybatis 动态sql 有什么用?执行原理?有哪些动态 sql?
Mybatis 动态 sql 可以在 Xml 映射文件内,以标签的形式编写动态 sql,执行原理
是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能。
Mybatis 提供了 9 种动态 sql 标签: trim | where | set | foreach | if | choose
| when | otherwise | bind 。
3、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,几乎所有的包括 Hibernate,支持延迟加载的原理都
是一样的。
4、Mybatis 的一级、二级缓存:
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为
Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就
将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap
存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,
如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要
实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置
;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存
Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将
被 clear。
Spring
1、Springbean的作用域?
- singleton:单例模式,Spring IoC容器中只会存在一个共享的Bean实例,无论有多少个Bean引用它,始终指向同一对象。Singleton作用域是Spring中的缺省作用域,也可以显示的将Bean定义为singleton模式,配置为:
o - prototype:原型模式,每次通过Spring容器获取prototype定义的bean时,容器都将创建一个新的Bean实例,每个Bean实例都有自己的属性和状态,而singleton全局只有一个对象。根据经验,对有状态的bean使用prototype作用域,而对无状态的bean使用singleton作用域。
- request:在一次Http请求中,容器会返回该Bean的同一实例。而对不同的Http请求则会产生新的Bean,而且该bean仅在当前Http Request内有效。
o ,针对每一次Http请求,Spring容器根据该bean的定义创建一个全新的实例,且该实例仅在当前Http请求内有效,而其它请求无法看到当前请求中状态的变化,当当前Http请求结束,该bean实例也将会被销毁。 - session:在一次Http Session中,容器会返回该Bean的同一实例。而对不同的Session请求则会创建新的实例,该bean实例仅在当前Session内有效。
o ,同Http请求相同,每一次session请求创建新的实例,而不同的实例之间不共享属性,且实例仅在自己的session请求内有效,请求结束,则实例将被销毁。 - global Session:在一个全局的Http Session中,容器会返回该Bean的同一个实例,仅在使用portlet context时有效。
2、springbean生命周期
- 实例化Bean: Ioc容器通过获取BeanDefinition对象中的信息进行实例化,实例化对象被包装在BeanWrapper对象中
- 设置对象属性(DI):通过BeanWrapper提供的设置属性的接口完成属性依赖注入;
- 注入Aware接口(BeanFactoryAware, 可以用这个方式来获取其它 Bean,ApplicationContextAware):Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给bean
- BeanPostProcessor:自定义的处理(分前置处理和后置处理)
- InitializingBean和init-method:执行我们自己定义的初始化方法
- 使用
- destroy:bean的销毁
3、Spring boot自动装配原理?
Spring Boot启动类上都有一个@SpringBootApplication注解。@SpringBootApplication是一个复合注解,它包含的其中一个注解为@EnableAutoConfiguration,而@EnableAutoConfiguration会导入AutoConfigurationImportSelector这个类,这个类会加载jar包里面META-INF/spring.factories配置文件里面填写的配置类。
4、spring中实现事务有几种方式:
编程式事务
声明式事务
5、Spring Boot的优缺点
Springboot优点
解耦,极大的提高了开发、部署效率
1.简化依赖
只需要在 pom 文件中添加如下一个 starter-web 依赖即可
6. 简化配置
Spring Boot更多的是采用 Java Config 的方式,对 Spring 进行配置
7. 简化部署
Spring Boot 内嵌了 tomcat,我们只需要将项目打成 jar 包,使用 java -jar xxx.jar一键式启动项目。
8. 简化监控
我们可以引入 spring-boot-start-actuator 依赖,直接使用 REST 方式来获取进程的运行期性能参数,从而达到监控的目的。
5、SpringAop两种代理模式区别
动态代理(JDK 代理、接口代理):在程序运行时运用反射机制动态创建而成,动态就是在程序运行时生成的,而不是编译时。
cglib 代理(可以在内存动态的创建对象,而不是实现接口,属于动态代理的范畴)
6、spring的循环依赖,及解决
A依赖B,B依赖A
解决:
微服务/Rpc框架
1、用过哪些微服务框架
SpringCloud Alibaba
分布式配置 Nacos
服务注册发现 Nacos
服务熔断 Sentinel
服务调用 Dubbo PROXY OpenFeign RestTemplate
服务路由 Dubbo PROXY SpringCloud Gateway
分布式消息 SCS RocketMQ
负载均衡 Ribbon
分布式事务 Seata
Springcloud 注册中心 原理
ZooKeeper注册中心集群搭建后,集群中各节点呈现主从关系,集群中只有主节点对外提供服务的注册和发现功能,从节点相当于备份节点,只有主节点宕机时,从节点会选举出一个新的主节点,继续提供服务的注册和发现功能。
而Eureka Server注册中心集群中每个节点都是平等的,集群中的所有节点同时对外提供服务的发现和注册等功能。同时集群中每个Eureka Server节点又是一个微服务,也就是说,每个节点都可以在集群中的其他节点上注册当前服务。又因为每个节点都是注册中心,所以节点之间又可以相互注册当前节点中已注册的服务,并发现其他节点中已注册的服务。
Eureka:各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
Feign:基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
Hystrix:发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务
2、微服务雪崩解决方案
雪崩效应产生的几种场景
• 流量激增:比如异常流量、用户重试导致系统负载升高;
• 缓存刷新:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃;
• 程序有Bug:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;
• 硬件故障:比如宕机,机房断电,光纤被挖断等。
• 线程同步等待:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的,最终这条线程将断掉,也可能引发雪崩;
雪崩效应的常见解决方案
针对上述雪崩情景,有很多应对方案,但没有一个万能的模式能够应对所有场景。
针对流量激增,采用自动扩缩容以应对突发流量,或在负载均衡器上安装限流模块。
针对缓存刷新,参考Cache应用中的服务过载案例研究
针对硬件故障,多机房容灾,跨机房路由,异地多活等。
针对同步等待,使用Hystrix做故障隔离,熔断器机制等可以解决依赖服务不可用的问题。
3、 服务降级、服务熔断、服务限流
服务降级:因为某些原因,服务调用出现故障,本次操作以失败告终,但是会有备份的解决方案,比如向友好返回一个友好的提示告诉用户等待再试,此时服务端问题情况还不大,还能提供服务,只是当前可能流量比较大,处理不来
发生的情况:程序运行异常;超时;服务熔断触发服务降级;线程池/信号量打满
服务熔断:是当服务调用或服务器等出现问题,及时中断本次操作,相当于服务一时半会用不了了
服务限流:在同一瞬间,上千万个请求,不可能同一时间处理,需要有限制措施。比如一秒N个,排队有序进行。
网络编程
1、浏览器点击一个请求过程是?
DNS 解析:将域名解析成 IP 地址
TCP 连接:TCP 三次握手
发送 HTTP 请求
服务器处理请求并返回 HTTP 报文
浏览器解析渲染页面
断开连接:UDP 四次挥手
2、如何防止网站攻击
1.XSS攻击 跨站脚本攻击
就是对用户输入的字符串进行处理,防止js脚本执行,
过滤脚本攻击的思路:就是对用户输入的字段进行处理,屏蔽掉其中的敏感词或者特殊字符,防止JS执行。
2. CSRF(Cross-site request forgery) 跨站请求伪造
Token 验证
隐藏令牌
3、cookie/session,token机制
cookie和session的区别
1、cookie数据存放在客户的浏览器上,session数据放在服务器上.
简单的说,当你登录一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上面,客户端每次请求服务器的时候会发送 当前会话的session_id,服务器根据当前session_id判断相应的用户数据标志,以确定用户是否登录,或具有某种权限。由于数据是存储在服务器 上面,所以你不能伪造,但是如果你能够获取某个登录用户的session_id,用特殊的浏览器伪造该用户的请求也是能够成功的。
session_id是服务器和客户端链接时候随机分配的,一般来说是不会有重复,但如果有大量的并发请求,也不是没有重复的可能性。
Session是由应用服务器维持的一个服务器端的存储空间,用户在连接服务器时,会由服务器生成一个唯一的SessionID,用该SessionID 为标识符来存取服务器端的Session存储空间。而SessionID这一数据则是保存到客户端,用Cookie保存的,用户提交页面时,会将这一 SessionID提交到服务器端,来存取Session数据。这一过程,是不用开发人员干预的。所以一旦客户端禁用Cookie,那么Session也会失效。
2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。
3、设置cookie时间可以使cookie过期。但是使用session-destory(),我们将会销毁会话。
4、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
5、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。(Session对象没有对存储的数据量的限制,其中可以保存更为复杂的数据类型)
注意:
session很容易失效,用户体验很差;
虽然cookie不安全,但是可以加密 ;
cookie也分为永久和暂时存在的;
浏览器 有禁止cookie功能 ,但一般用户都不会设置;
一定要设置失效时间,要不然浏览器关闭就消失了;
例如:
记住密码功能就是使用永久cookie写在客户端电脑,下次登录时,自动将cookie信息附加发送给服务端。
application是全局性信息,是所有用户共享的信息,如可以记录有多少用户现在登录过本网站,并把该信息展示个所有用户。
两者最大的区别在于生存周期,一个是IE启动到IE关闭.(浏览器页面一关 ,session就消失了),一个是预先设置的生存周期,或永久的保存于本地的文件。(cookie)
Session信息是存放在server端,但session id是存放在client cookie的,当然php的session存放方法是多样化的,这样就算禁用cookie一样可以跟踪
Cookie是完全保持在客户端的如:IE firefox 当客户端禁止cookie时将不能再使用
https://www.cnblogs.com/l199616j/p/11195667.html
token机制
什么是token
token的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。
当用户第一次登录后,服务器生成一个token并将此token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
简单token的组成;uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token的前几位以哈希算法压缩成的一定长度的十六进制字符串。为防止token泄露)。
身份认证概述
由于HTTP是一种没有状态的协议,它并不知道是谁访问了我们的应用。这里把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下次这个客户端再发送请求时候,还得再验证一下。
通用的解决方法就是,当用户请求登录的时候,如果没有问题,在服务端生成一条记录,在这个记录里可以说明登录的用户是谁,然后把这条记录的id发送给客户端,客户端收到以后把这个id存储在cookie里,下次该用户再次向服务端发送请求的时候,可以带上这个cookie,这样服务端会验证一下cookie里的信息,看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。
以上所描述的过程就是利用session,那个id值就是sessionid。我们需要在服务端存储为用户生成的session,这些session会存储在内存,磁盘,或者数据库。
基于token机制的身份认证
使用token机制的身份验证方法,在服务器端不需要存储用户的登录记录。大概的流程:
客户端使用用户名和密码请求登录。服务端收到请求,验证用户名和密码。验证成功后,服务端会生成一个token,然后把这个token发送给客户端。客户端收到token后把它存储起来,可以放在cookie或者Local Storage(本地存储)里。客户端每次向服务端发送请求的时候都需要带上服务端发给的token。服务端收到请求,然后去验证客户端请求里面带着token,如果验证成功,就向客户端返回请求的数据。
利用token机制进行登录认证,可以有以下方式:
a.用设备mac地址作为token
客户端:客户端在登录时获取设备的mac地址,将其作为参数传递到服务端
服务端:服务端接收到该参数后,便用一个变量来接收,同时将其作为token保存在数据库,并将该token设置到session中。客户端每次请求的时候都要统一拦截,将客户端传递的token和服务器端session中的token进行对比,相同则登录成功,不同则拒绝。
此方式客户端和服务端统一了唯一的标识,并且保证每一个设备拥有唯一的标识。缺点是服务器端需要保存mac地址;优点是客户端无需重新登录,只要登录一次以后一直可以使用,对于超时的问题由服务端进行处理。
b.用sessionid作为token
客户端:客户端携带用户名和密码登录
服务端:接收到用户名和密码后进行校验,正确就将本地获取的sessionid作为token返回给客户端,客户端以后只需带上请求的数据即可。
此方式的优点是方便,不用存储数据,缺点就是当session过期时,客户端必须重新登录才能请求数据。
当然,对于一些保密性较高的应用,可以采取两种方式结合的方式,将设备mac地址与用户名密码同时作为token进行认证。
APP利用token机制进行身份认证
用户在登录APP时,APP端会发送加密的用户名和密码到服务器,服务器验证用户名和密码,如果验证成功,就会生成相应位数的字符产作为token存储到服务器中,并且将该token返回给APP端。
以后APP再次请求时,凡是需要验证的地方都要带上该token,然后服务器端验证token,成功返回所需要的结果,失败返回错误信息,让用户重新登录。其中,服务器上会给token设置一个有效期,每次APP请求的时候都验证token和有效期。
token的存储
token可以存到数据库中,但是有可能查询token的时间会过长导致token丢失(其实token丢失了再重新认证一个就好,但是别丢太频繁,别让用户没事儿就去认证)。
为了避免查询时间过长,可以将token放到内存中。这样查询速度绝对就不是问题了,也不用太担心占据内存,就算token是一个32位的字符串,应用的用户量在百万级或者千万级,也是占不了多少内存的。
token的加密
token是很容易泄露的,如果不进行加密处理,很容易被恶意拷贝并用来登录。加密的方式一般有:
在存储的时候把token进行对称加密存储,用到的时候再解密。文章最开始提到的签名sign:将请求URL、时间戳、token三者合并,通过算法进行加密处理。
最好是两种方式结合使用。
还有一点,在网络层面上token使用明文传输的话是非常危险的,所以一定要使用HTTPS协议。
https://blog.csdn.net/daimengs/article/details/81088172
4、TCP粘包
1.Q:什么是TCP粘包问题?
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
2.Q:造成TCP粘包的原因
(1)发送方原因
TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:
只有上一个分组得到确认,才会发送下一个分组
收集多个小分组,在一个确认到来时一起发送
Nagle算法造成了发送方可能会出现粘包问题
(2)接收方原因
TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
3.Q:什么时候需要处理粘包现象?
如果发送方发送的多组数据本来就是同一块数据的不同部分,比如说一个文件被分成多个部分发送,这时当然不需要处理粘包现象
如果多个分组毫不相干,甚至是并列关系,那么这个时候就一定要处理粘包现象了
4.Q:如何处理粘包现象?
(1)发送方
对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。
(2)接收方
接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。
(2)应用层
应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。
解决办法:循环处理,应用程序从接收缓存中读取分组时,读完一条数据,就应该循环读取下一条数据,直到所有数据都被处理完成,但是如何判断每条数据的长度呢?
格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。
发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。
5.Q:UDP会不会产生粘包问题呢?
TCP为了保证可靠传输并减少额外的开销(每次发包都要验证),采用了基于流的传输,基于流的传输不认为消息是一条一条的,是无保护消息边界的(保护消息边界:指传输协议把数据当做一条独立的消息在网上传输,接收端一次只能接受一条独立的消息)。
UDP则是面向消息传输的,是有保护消息边界的,接收方一次只接受一条独立的信息,所以不存在粘包问题。
举个例子:有三个数据包,大小分别为2k、4k、6k,如果采用UDP发送的话,不管接受方的接收缓存有多大,我们必须要进行至少三次以上的发送才能把数据包发送完,但是使用TCP协议发送的话,我们只需要接受方的接收缓存有12k的大小,就可以一次把这3个数据包全部发送完毕。
参考原文链接:https://blog.csdn.net/weixin_41047704/article/details/85340311
高并发
1、怎么提高并发量?
1.系统操作层
1.1.修改用户进程可打开文件数限制
1.2.修改网络内核对TCP连接的有关限制
1.3.使用支持高并发网络I/O的编程技术
2. 业务处理层
2.1.数据库操作优化
数据库索引,分库,分表,访问查询使用乐观锁,或者读写分库分表
2.2尽量使用缓存redis/memcached
2.3.使用服务器集群来解决单台的瓶颈问题
Nginx反向代理,负载均衡到集群处理
压力测试软件
2、高并发 压力测试
Apache JMeter 压力测试软件
jmeter 可以对测试静态资源(例如 js、html 等)以及动态资源(例如 php、jsp、ajax 等等)进行性能测试
jmeter 可以挖掘出系统最大能处理的并发用户数
jmeter 提供了一系列各种形式的性能分析报告
负载测试:通过测试系统在资源超负荷情况下的表现,以发现设计上的错误或验证系统的负载能力。
压力测试:测试系统能承受的最大负载能力。目的在于发挖掘出目标服务系统可以处理的最大负载。
操作步骤:
1新增线程组
创建测试线程组,并设置线程数量及线程初始化启动方式。
2新增 JMeter 元组
创建各种默认元组及测试元组,填入目标测试静态资源请求和动态资源请求参数及数据。
3新增监听器
创建各种形式的结果搜集元组,以便在运行过程及运行结束后搜集监控指标数据。
4运行&查看结果
调试运行,分析指标数据,挖掘性能瓶颈,评估系统性能状态
3 进程间如何通讯
1.管道(pipe)
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系
有名管道(namedpipe)
有名管道也是半双工的通信方式,但是它云溪无亲缘关系进程间的通信。
2.信号量(semaphore)
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
3.消息队列(messagequeue)
消息队列里有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递消息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点
4.信号(signal)
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
5.共享内存(shared memory)
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
6.套接字(socket)
套接口也是一种进程间通信机制,以其他通信机制不同的是,它可用于不同进程间的通信
4. 线程间如何通讯
锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法
读写锁允许多个线程同时读共享数据,而对写操作是互斥的
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
信号量机制:包括无名线程信号量和命名线程信号量
信号机制:类似进程间的信号处理
线程间的通信目的只要是用于新城同步,所以线程没有像进程通信中的用于数据交换的通信机制。
5. 同步和异步有何不同,在什么情况下分别使用它们?举例说明
如果数据将在线程间共享。例如:正在写的数据以后可能会被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效。
同步交互:指发送一个请求,需要等待返回,然后才能发送下一个请求,有个等待的过程
异步交互:指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待。
区别:一个需要等待,一个不需要等待
JVM
1、什么情况下会发生栈内存溢出。
思路: 描述栈定义,再描述为什么会溢出,再说明一下相关配置参数,OK的话可以给面试官手写是一个栈溢出的demo。
栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,对象引用类型
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。
如果Java虚拟机栈可以动态扩展,并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多)
参数 -Xss 去调整JVM栈的大小
2、Jvm新生代老生代算法
老生代:标记整理算法 Full GC
新生代: 复制算法 Major GC
标记整理算法标记过程仍与”标记-清除”过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存
复制算法将堆中可用的新生代内存按容量划分成大小相等的两块内存区域,每次只使用其中的一块区域。当其中一块内存区域需要进行垃圾回收时,会将此区域内还存活着的对象复制到另一块上面,然后再把此内存区域一次性清理掉。
这样做的好处是每次都是对整个新生代一半的内存区域进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。此算法实现简单,运行高效。
3、什么时候会发生对象回收
当堆中已使用内存达到一定的阈值时触发垃圾回收。
当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。
我们可以通过 引用计数器 和 可达性算法 来判断一个对象是否“已死”。引用计数器很难解决对象之间互相循环引用的问题,所以在主流的商用程序语言(如Java)的主流实现中,都是称通过可达性分析来判断对象是否存活的。
引用计数算法: 这个算法的判断依据是通过给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加 1;当引用失效时,计数器值就减 1;任何时刻计数器为 0 的对象就不可能再被使用的。
可达性算法:基本思路就是通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的
4、在JVM中,堆区中对象的几种状态(可触及,不可触及,可复活)
可触及状态:当一个对象被创建后,只要程序中还有引用变量引用该对象,那么它就始终处于可触及状态。
可复活状态:当程序不再有任何引用变量引用对象时,它就进入可复活状态。该状态的对象,垃圾回收器会准备释放它占用的内存,在释放前,会调用它的finalize()方法,这些finalize()方法有可能使对象重新转到可触及状态。
不可触及状态:当JVM执行完所有的可复活状态的finalize()方法后,假如这些方法都没有使对象转到可触及状态。那么该对象就进入不可触及状态。只有当对象处于不可触及状态时,垃圾回收器才会真正回收它占用的内存。
5、垃圾回收的时间
当一个对象处于可复活状态时,垃圾回收线程执行它的finalize()方法,任何使它转到不可触及状态,任何回收它占用的内存,这对于程序来说都是透明的。程序只能决定一个对象任何不再被任何引用变量引用,使得它成为可以被回收的垃圾。
类比:居民把无用物品放在指定的地方,清洁工人会把它收拾走。但垃圾被收走的时间,居民是不知道的,也无需了解。
垃圾回收器作为低优先级线程独立运行。在任何时候,程序都无法迫使垃圾回收器立即执行垃圾回收操作。
程序中可调用System.gc()或Runtime.gc()方法提示垃圾回收器尽快执行垃圾回收操作,但是不能保证调用后垃圾回收器会立即执行垃圾回收。
6、什么样的对象需要回收
不再被外部引用的对象需要被回收
7、虚拟机是如何判断对象对象不再被引用的呢?
通过可达性分析算法来判定对象是否不再被引用。
双亲委派机制指的是当一个类加载器收到一个class字节码文件请求时,该类加载器首先会把请求委派给父类加载器,一直递归这个操作,当在父类加载器内找不到指定类时,子类加载器才会尝试自己去加载这个class文件。
Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。 ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。 AppClassLoader:主要负责加载应用程序的主函数类。
8、JVM的运行时数据区域有哪些?
方法区(),堆(堆外NIO),程序计数器(pc),虚拟机栈,本地方法栈
9、JVM的主要组成部分有哪些?简单说下各自职责?
1.类加载器(Class Loader):加载类文件到内存。Class loader只管加载,只要符合文件结构就加载,至于能否运行,它不负责,那是有Exectution Engine 负责的。
2.执行引擎(Execution Engine):也叫解释器,负责解释命令,交由操作系统执行。
3.本地库接口(Native Interface):本地接口的作用是融合不同的语言为java所用
4.运行时数据区(Runtime Data Area):
(1)堆。堆是java对象的存储区域,任何用new字段分配的java对象实例和数组,都被分配在堆上,java堆可用-Xms和-Xmx进行内存控制,jdk1.7以后,运行时常量池从方法区移到了堆上。
(2)方法区:用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
(3)虚拟机栈:虚拟机栈中执行每个方法的时候,都会创建一个栈桢用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
(4)本地方法栈:与虚拟机发挥的作用相似,相比于虚拟机栈为Java方法服务,本地方法栈为虚拟机使用的Native方法服务,执行每个本地方法的时候,都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。
(5)程序计数器。指示Java虚拟机下一条需要执行的字节码指令。
什么是软引用,弱引用,虚引用?
强引用(StrongReference):一般指的是对像被new出来,强引用一般不会被jvm收回,但会报OutOfMemory(内存不足)。
软引用(SoftReference):软引用相对来说弱于强引用,当内存足够的时候不会被GC回收,但内存不足时,再试图回收软引用,通过软引用可以做临时缓存。
弱引用(WeakReference):区别于软件引用是生命周期更短,当GC回收启动发现弱引用不管内存满不满,都会被直接回收。
虚引用(PhantomReference):这个引用也有人叫幻引用,也很明显,引用一个不存在,随时会被干掉,算是所有引用中最容易被干掉的。
Object obj = new Object(); /引用
SoftReference ref = new SoftReference(“hong”); //软引用
/引用
Obejct oj = new Object();
WeakReference wf = new WeakReference(oj);
oj = null;
System.gc(); //下面会发现有时候直接返回null;wf.get();
// 虚引用
Object oj= new Object();
ReferenceQueue req= new ReferenceQueue();
PhantomReference pr= new PhantomReference(oj, req);
// 每次返回NullSystem.out.println(pr.get());//返回是否被删除System.out.println(pr.isEnqueued());
11、四种引用类型
强引用:垃圾回收器将永远不会回收被引用的对象。哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了
软引用:是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
弱引用:引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
虚引用:最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
12、类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JVM将会连续完成这三个步骤。所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
1、加载 load
就是指将类型的class字节码数据读入内存。
2、连接 link
1)验证:校验合法性等
2)准备:准备对应的内存(方法区),创建Class对象,为类变量赋默认值
3)解析:把字节码中的符号引用替换为对应的直接地址引用
3、初始化
initalize(类初始化)即执行类初始化方法,大多数情况下,类的加载就完成了类的初始化,有些情况下,会延迟类的初始化。
13、哪些操作会导致类初始化
1、运行主方法所在的类,要先完成初始化,再执行main方法
2、第一次使用某个类型就是在new他的对象,此时这个类没有初始化的话,先完成 类初始化再做实例初始话
3、调用某个类的静态成员(类变量和类方法),此时这个类没有初始化的话,先完成初始化 4、子类初始化时,发现他的父类还没有初始化的话,那么先初始化父类
5、通过反射操作某个类时,如果这个类没有初始化,也会导致该类先初始化
MQ
数据库缓存双写一致性问题
三种更新策略:
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
(1)先更新数据库,再更新缓存
这套方案,大家是普遍反对的。为什么呢?有如下两点原因。
原因一(线程安全角度)
同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了数据库
(2)线程B更新了数据库
(3)线程B更新了缓存
(4)线程A更新了缓存
这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
原因二(业务场景角度)
有如下两点:
(1)如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
(2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。
接下来讨论的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。
(2)先删缓存,再更新数据库
该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
那么,如何解决呢?采用延时双删策略
转化为中文描述就是
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
如果你用了mysql的读写分离架构怎么办?
ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值
上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
采用这种同步淘汰策略,吞吐量降低怎么办?
ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
第二次删除,如果删除失败怎么办?
这是个非常好的问题,因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
(6)请求A试图去删除请求B写入对缓存值,结果失败了。
ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。
如何解决呢?
具体解决方案,且看博主对第(3)种更新策略的解析。
(3)先更新数据库,再删缓存
首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出
• 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
• 命中:应用程序从cache中取数据,取到后返回。
• 更新:先把数据存到数据库中,成功后,再让缓存失效。
另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。
这种情况不存在并发问题么?
不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
假设,有人非要抬杠,有强迫症,一定要解决怎么办?
如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。
还有其他造成不一致的原因么?
有的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的一个问题,如果删缓存失败了怎么办,那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。这也是缓存更新策略(2)里留下的最后一个疑问。
如何解决?
提供一个保障的重试机制即可,这里给出两套方案。
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作
备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。至于oracle中,博主目前不知道有没有现成中间件可以使用。另外,重试机制,博主是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。
算法
二叉树的遍历
前序遍历:根节点->左子树->右子树(根->左->右)
中序遍历:左子树->根节点->右子树(左->根->右)
后序遍历:左子树->右子树->根节点(左->右->根)
几种排序算法
1、冒泡排序
2、选择排序
3、插入排序
4、希尔排序
5、快速排序
6、归并排序
7、堆排序
引用参考https://blog.csdn.net/liang_gu/article/details/80627548
几种查询算法
1、顺序查找
2、折半查找(二分查找)
3、 插值查找