编程有个说法,能用组合就不用继承。
说说继承的好处。
htttp1.1 和2.0 的关系,区别
json rpc
kafka rabbit mq
Lambda表达式使用详解教程 - 知乎
Lambda 规定接口中只能有一个需要被实现的方法(即只能有一个抽象方法),不是规定接口中只能有一个方法,这也称之为“函数式接口”,比如:java.lang.Runnable
就是一个函数式接口
jdk 8 中有另一个新特性:default, 被 default 修饰的方法会有默认实现,不是必须被实现的方法,所以不影响 Lambda 表达式的使用。
JAVA 8 函数式接口--Consumer - 简书
看代码就懂了
andThen 实现:
default Consumer andThen(Consumer super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
示例代码:
class Solution {
public static void main(String[] args) {
Consumer consumer = (o) -> System.out.println(0);
Consumer consumer1 = (o) -> System.out.println(1);
Consumer consumer2 = (o) -> System.out.println(2);
Consumer consumer3 = System.out::println;
consumer
.andThen(consumer1)
.andThen(consumer2)
.andThen(consumer3)
.accept("abc");
}
}
输出:
1
2
3
abc
Java8新特性学习-函数式编程(Stream/Function/Optional/Consumer)
consumer 中:
有个accept 方法,无返回值
有个default 的andThen
Function 中:
有个apply() 方法,有输入,有输出
还有andThen
compose
identity
/**
* Function测试
*/
public static void functionTest() {
Function f = s -> s++;
Function g = s -> s * 2;
/**
* 下面表示在执行F时,先执行G,并且执行F时使用G的输出当作输入。
* 相当于以下代码:
* Integer a = g.apply(1);
* System.out.println(f.apply(a));
* compose: 组合
*/
System.out.println(f.compose(g).apply(1));
/**
* 表示执行F的Apply后使用其返回的值当作输入再执行G的Apply;
* 相当于以下代码
* Integer a = f.apply(1);
* System.out.println(g.apply(a));
*/
System.out.println(f.andThen(g).apply(1));
/**
* identity方法会返回一个不进行任何处理的Function,即输出与输入值相等;
* identity:身份、特点、个性
*/
System.out.println(Function.identity().apply("a"));
}
predicate:断言、断定。
predicate 里面有个test 方法,再结合其中的and、or、equal 那些。
由于 String 在 Java 中是不可变的,因此每当我们执行字符串拼接操作时,它都会生成一个新的 String 并丢弃旧的 String 以进行垃圾收集。
这些重复的操作会在堆中产生大量垃圾冗余。所以 Java 提供了 StringBuffer 和 StringBuilder 类,应该用于字符串操作
参考:Java 中 String 与 StringBuffer 和 StringBuilder 的区别-阿里云开发者社区
到底什么是服务治理? - 知乎
Java Hotspot G1 GC的一些关键技术 - 美团技术团队
https://segmentfault.com/a/1190000023846387
订阅与发布 — Redis 设计与实现
spring boot ,一个接口有多个实现类,如何通过一个autowire 进行一次性加载
或者怎么实现我的这个诉求,一次获取这三个实例
spring boot 怎么集成redis
kafka 的exactly once 如何去实现
provider 发出消息后,收到两个相同的确认信息(provider id 一样,offset 一样,这个什么情况)
partition 的确认信息里面内容是什么
ApplicationContext 如何get 到同一个接口的不同的实现
参考:Java中创建对象的5种方式,你都知道几种?【享学Java】 - 云+社区 - 腾讯云
1. 父类静态代码块 / 静态成员变量(并列)
2. 子类静态代码块 / 静态成员变量(并列)
3. 父类普通代码块 / 普通成员变量(并列)
4. 父类构造方法
5. 子类普通代码块 / 子类普通成员变量(并列)
6. 子类构造方法
Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。
Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。
如何实现深拷贝,就自己实现Clonable 接口,重写每个对象的clone 方法。
这种拷贝对象的非对象属性(基本类型属性)、不可变对象属性,但是不拷贝 对象的对象属性(不含不可变对象) ,即为浅拷贝。
参考:Java 中的浅拷贝与深拷贝 - 知乎
clone 方法是浅拷贝,不是深拷贝。
clone 方法的作用就是复制对象,产生一个新的对象。
在 Java 中基本类型和引用类型的区别。
在 Java 中数据类型可以分为两大类:基本类型和引用类型。
基本类型也称为值类型,分别是字符类型 char,布尔类型 boolean以及数值类型 byte、short、int、long、float、double。
引用类型则包括类、接口、数组、枚举等。
Java 将内存空间分为堆和栈。
基本类型直接在栈 stack中存储数值
而引用类型是将引用放在栈中,实际存储的值是放在堆 heap中,通过栈中的引用指向堆中存放的数据。
浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该对象,如果字段类型是值类型(基本类型跟String)的,那么对该字段进行复制;如果字段是引用类型的,则只复制该字段的引用而不复制引用指向的对象(也就是只复制对象的地址)。此时新对象里面的引用类型字段相当于是原始对象里面引用类型字段的一个副本,原始对象与新对象里面的引用字段指向的是同一个对象
。 因此,修改clonePerson里面的address内容时,原person里面的address内容会跟着改变。
只复制值类型的。对于引用类型的,只复制引用,传递引用。
Object 提供的clone 方法只能是实现浅拷贝的。
实现深拷贝的两种方式:
(1) 第一种是给需要拷贝的引用类型也实现Cloneable接口并覆写clone方法
引用类型不能实现深拷贝,那么我们将每个引用类型都拆分为基本类型,分别进行浅拷贝
(2)第二种则是利用序列化
参考:面试官:Java 是深拷贝还是浅拷贝 - 知乎
重载,就是函数或者方法有相同的名称,但是参数列表不相同的情形。和返回值没关系。
比如说写了两个方法都叫test(),然后返回值不同。
class Test {
int test() {return 0;}
void test() {}
public static void main(String[] args) {
test();
}
}
在java 代码中,假设编译可以通过,但是在调用的时候,就不知道调用哪个方法了
java 识别一个方法是根据方法名加参数列表实现的,与返回值无关。
即外壳不变,核心重写!
返回值、函数名、参数列表,都不变。
重写方法不能抛出新的 检查异常或者比被重写方法申明更加宽泛的异常。
打印一个对象的时候,sout(person),方法内部会自动调用Person 类的toString() 方法。
在打印一个对象的时候,返回的是类名+@+hashCode 值,代表对象在内存中的位置。
如:com.yingjun.ssm.entity.Person@6d06d69c
参数化类型。将类类型,作为参数,进行传递。
泛型接口、泛型类、泛型方法
// 泛型接口
public interface Test {
}
// 泛型类
public class Target implements Test {
}
// 泛型方法
public T add(Class clz) {
T instance = clz.getInstance();
return instance;
}
泛型接口、类的泛型形参,放置在接口或类名的后面。
泛型方法的泛型形参,放置在方法返回类型的前面即可。
java的泛型只是在编译时使用,用于检查传递给泛型类、方法的数据类型是否符合泛型定义,泛型类和方法中的泛型会在编译成功后擦除。
编译器并会在泛型数据操作时,自动添加类型转换。由于编译期间保证了泛型类型的正确使用,因此自动添加的类型转换是安全的。
定义泛型形参时,可以使用extends限制泛型的范围。
比如使用
比如使用
注意:extends并不表示类的继承含义,只是表示泛型的范围关系。
注意:extends中可以指定多个范围,实行泛型类型检查约束时,会以最左边的为准。
执行结果是什么
public static void main(String[] args) {
List list1=new ArrayList();
List list2=new ArrayList();
System.out.println(list1.getClass()==list2.getClass());
}
是true
getClass 方法获取的是对象运行时的类(Class,表示对象的类)
上面的问题就转化成了两个对象List
答案是true
对它们的类型进行打印,结果都是
class java.util.ArrayList
虽然ArrayList
泛型的本质就是参数化类型,而类型擦除使得类型参数只存在于编译期,在运行时,jvm是并不知道泛型的存在的。
那么为什么要进行泛型的类型擦除呢?查阅的一些资料中,解释说类型擦除的主要目的是避免过多的创建类而造成的运行时的过度消耗。试想一下,如果用List表示一个类型,再用List表示另一个类型,以此类推,无疑会引起类型的数量爆炸。
不同情况的类型擦除
(1)无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除后,会被直接替换为Object。
如:
// 原始类
public class Car {
private T id;
public T getId() {
return id;
}
}
// 编译后的类(反编译得到的结果)
public class Car {
public Car() {}
private Object Id;
public Object getId() {
return Id;
}
}
类定义中的类型参数没有任何限制,类型擦除之后,会被替换成Object
(2)有限制类型擦除
当类定义中的类型参数存在限制时,在类型擦除中替换为类型参数的上界或者下界。下面的代码中,经过擦除后T被替换成了Integer:
// 原始类
public class Car {
private T id;
public T getId() {
return id;
}
}
// 反编译后
public class Car {
public Car() {}
private Integer Id;
public Integer getId() {
return Id;
}
}
(3)擦除方法中类型的参数
和擦除类定义中的类型参数一致。(无限制直接擦除为Object,有限制的时候被擦除为上界或者下界)
反射装的
比如说Map
sout(Arrays.asList(map.getClass().getTypeParameters()));
最终只能得到[K, V]
可以看到通过getTypeParameters方法只能获取到泛型的参数占位符,而不能获得代码中真正的泛型类型。
我们知道运行时是没有泛型约束的,在运行的时候是可以吧一个类型的对象放进另一个类型的List 里。正常这么做会报出“编译错误”,但是可以用反射,在运行时写入。
public class ReflectTest {
static List list = new ArrayList<>();
public static void main(String[] args) {
list.add("1");
ReflectTest reflectTest =new ReflectTest();
try {
Field field = ReflectTest.class.getDeclaredField("list");
field.setAccessible(true);
List list=(List) field.get(reflectTest);
list.add(new User());
} catch (Exception e) {
e.printStackTrace();
}
}
}
而在get 元素的时候,会报错。
异常提示xx 类型无法被转成xx 类型。
对于list 的get 函数,有两个步骤。首先是rangeCheck,然后是reture elementData(index)
在取出元素的时候,会将这个元素强制转换成泛型中的类型。在这一阶段程序会报错。通过这一过程,可再次证明了泛型可以对类型安全进行检测。
类型擦除的时候,会有一个覆盖的操作。但是这种覆盖(@Override)会发现,子类和父类的方法参数,在类型擦除之后,参数不一致。
针对这种情况,编译器会通过添加一个桥接方法来满足语法上的要求。同时保证了基于泛型的多态能够有效。
举例:
interface Fruit {
T get(T obj);
}
// 如果Apple 类实现了Fruit 接口之后,正常的反编译代码应该如下(文章的部分摘抄)
public Apple implements Fruit {
public Apple() {}
public Integer get(Integer param) {
return param;
}
public Object get(Object obj) {
return get((Integer) obj)
}
}
参考:面试官:说说什么是泛型的类型擦除? - 51CTO.COM
比如使用
比如使用
注意:extends并不表示类的继承含义,只是表示泛型的范围关系。
注意:extends中可以指定多个范围,实行泛型类型检查约束时,会以最左边的为准。
1. 为使用的泛型类,单独生成一份非泛型的具体类。
比如List
在程序编译的时候,生成一份StringArrayList类,并且类里面所有的操作元素都是String类。
2. 采用擦除机制,泛型接口或类中,所有使用泛型形参的地方,全部擦除,替换为Object类型(java中所有类的父类)。所有相同泛型类的实例共享使用泛型类的代码。在泛型类的实例进行数据操作的地方(泛型类外部,自定义的程序部分),由编译器检查操作的参数是否为泛型类实例定义时的类类型,必要时自动添加强制类型转换。
JAVA反射机制是在运行状态中:
对于任意一个实体类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意方法和属性;
这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制
Java的反射机制允许编程人员在对类未知的情况下,
获取类相关信息的方式变得更加多样灵活,调用类中相应方法,
这是Java增加其灵活性与动态性的一种机制。
反射是通过一个类的Class 对象进行操作而取得该类的所有方法和属性的。
反射的源头在于Class 对象。
反射最常用到的操作:
取得属性的修饰符,属性名,属性值,设置属性值,取得所有的属性。
操作方法,取得方法的修饰符,方法名,方法参数,方法参数类型,方法返回值,取得一个类的所有方法。
反射可以动态的调用、修改一个对象的任何属性、方法(包括私有属性和方法)。
对于泛型擦除之后,程序处于运行之中,可以使用反射,为某个String 类型的List 塞入一个Integer 类型的元素。
getMethods()返回的是当前Class对象的所有公有的方法,包含从父类或父接口继承而来的方法。
getDeclaredMethods()返回的是当前Class对象的所有(包括:public,protected,default,private)方法,但是并不包括继承自父类或父接口的方法。
declared:宣告、断言、表态
常用api 参考如下链接:
参考:Java反射常用API_wenpan的博客-CSDN博客_java反射api是什么
通过类的对象(Class 对象)枚举出该类的所有方法。
可以通过Method.Accessible 绕过Java 语言的访问权限。在私有方法所在类之外的地方调用它。
spring 的IOC 底层原理使用的就是反射机制。
Web开发中,我们经常使用的各种通用框架为了保证框架的可扩展性,往往都使用Java反射功能,根据配置文件中的信息来动态的加载不同的类,还可以为类中的属性赋值等等。
原理:在JVM的层面,java的对象引用不仅要可以直接或间接的接触到对象类型,更应该可以根据索引能得到这个对象的类型数据(对象的Class对象)。这样的JVM设计使得JAVA可以拥有反射功能。
(举例:你怎么知道我电话的?我查的通讯录)
参考:面试官问我反射的实现原理是什么??
JAVA反射机制是在运行状态中,
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个属性和方法;
这种动态获取的信息以及动态调用对象的方法的功能称为 java语言的反射机制。
原理:Java在编译之后会生成一个class文件,反射通过字节码文件找到其类中的方法和属性等
可参考:java 反射原理(jvm是如何实现反射的) - 简书
有空可以看这个,反射的使用和低效率原因:都说 Java 反射效率低,究竟原因在哪里? - 知乎
在java中Class.forName()和ClassLoader都可以对类进行加载。ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。
Class.forName()方法实际上也是调用的CLassLoader来实现的。
Class.forName加载类时将类进了初始化,而ClassLoader的loadClass并没有对类进行初始化,只是把类加载到了虚拟机中
参考:【003期】Java 中 Class.forName 和 ClassLoader 到底有啥区别?
整体流程:翻译(成机器码,或者字节码) -> 解释执行
编译器是把源程序的每一条语句都编译成机器语言,并保存成二进制文件,翻译与执行是分开的,这样运行时计算机可以直接以机器语言来运行此程序,速度很快;C,C++都是靠编译实现的。
解释器则是只在执行程序时,才一条一条的解释成机器语言给计算机来执行,翻译与执行一次性完成,所以运行速度是不如编译后的程序运行的快的,但是就启动效率而言,解释执行的速度更快,因为它不需要进行编译过程。
总的来说,编译执行只需要最开始编译一次,之后每次运行时都不需要再编译,所以运行效率会很快;解释执行在每一次运行时都需要经过解释的过程,所以执行效率较低。
通俗一点,编译就是饭菜全部做好再吃,解释就相当于火锅,边涮边吃。
从内存使用方面来看,编译执行需要生成编译后的机器码文件,而解释执行时逐句解释执行,所以解释执行对内存占用更少。
Java程序也需要编译,但是没有直接编译称为机器语言,而是编译成为字节码,然后在JVM上用解释方式执行字节码。
Python 的也采用了类似Java的编译模式,先将Python程序编译成Python字节码,然后由一个专门的Python字节码解释器负责解释执行字节码。
Java通过解释器解释执行字节码,这样的执行方式相对较慢,尤其是遇到一些运行频繁的代码块或者方法时。于是后来JVM引入了JIT即时编译器(just in time),当JVM发现某些代码运行频繁时就会认定为热点代码“hot spot code”,为了提高运行效率,就会把这些代码编译成为平台相关的机器码然后进行优化,JIT就是用来完成这项工作的。二者共同造就了java的优势——当程序需要迅速启动时,解释器首先发挥作用,省去编译时间,当程序运行时,编译器会逐渐将更多的代码编译成本地机器码从而获得更高的效率。
所以我的理解就是JAVA先编译(高级语言→字节码),后解释(字节码→机器码),在解释的过程中也可能进行编译(JIT编译热点代码)。
参考:解释执行与编译执行以及JIT的区别
重载,返回值是区分点吗?不是。
@Override
String run(String s)
String run(Object s)
run("abc") -> 第一个
run(null) -> 第一个
分别调用哪个函数
A().B(Integer.valueOf(1))在class A中没有找到类型匹配的方法,Java自动类型提升机制,对Integer进行向上转型后匹配到了了public void B(Number n) 方法,调用后输出字符串"Num"
1)向上转型
父类引用指向子类对象为向上转型,语法格式如下:
fatherClass obj = new sonClass();
其中,fatherClass 是父类名称或接口名称,obj 是创建的对象,sonClass 是子类名称。
向上转型就是把子类对象直接赋给父类引用,不用强制转换。使用向上转型可以调用父类类型中的所有成员,不能调用子类类型中特有成员,最终运行效果看子类的具体实现。
2)向下转型
与向上转型相反,子类对象指向父类引用为向下转型,语法格式如下:
sonClass obj = (sonClass) fatherClass;
其中,fatherClass 是父类名称,obj 是创建的对象,sonClass 是子类名称。
向下转型可以调用子类类型中所有的成员,不过需要注意的是如果父类引用对象指向的是子类对象,那么在向下转型的过程中是安全的,也就是编译是不会出错误。但是如果父类引用对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现我们开始提到的 Java 强制类型转换异常,一般使用 instanceof 运算符来避免出此类错误。
例如,Animal 类表示动物类,该类对应的子类有 Dog 类,使用对象类型表示如下:
Animal animal = new Dog(); // 向上转型,把Dog类型转换为Animal类型
Dog dog = (Dog) animal; // 向下转型,把Animal类型转换为Dog类型
参考:Java对象类型转换:向上转型和向下转型
到底什么是服务治理? - 知乎
Java Hotspot G1 GC的一些关键技术 - 美团技术团队
https://segmentfault.com/a/1190000023846387
订阅与发布 — Redis 设计与实现
spring boot ,一个接口有多个实现类,如何通过一个autowire 进行一次性加载
或者怎么实现我的这个诉求,一次获取这三个实例
spring boot 怎么集成redis
kafka 的exactly once 如何去实现
provider 发出消息后,收到两个相同的确认信息(provider id 一样,offset 一样,这个什么情况)
partition 的确认信息里面内容是什么
ApplicationContext 如何get 到同一个接口的不同的实现
如果某个类,重写了hashcode() 方法,如果这个类对象当做map 的key,对于map 会怎么样,冲突吗,链吗
Key {
@Override
hashcode() {
return 4;
}
Key key = new Key();
Key key2 = new Key();
map.put(key) map.put(key2)
专门的网关服务
对开发的rpc 接口进行配置
以http 接口给出
在获取到请求后
解析HttpServletRequest 的时候,取出来参数
进行dubbo 泛化调用
其中有前置校验
name,id,subject 科目,grade 成绩
数据做插入的时候,如何避免重复
前端拦截
后端业务拦截
db 特性拦截
insert ignore into living (room_id) VALUES("7758521")
replace into living (room_id) VALUES("7758521")
insert into living (room_id) VALUES("7758521") on duplicate key update source_type = "1"
insert into living (room_id) SELECT "room_id" from living where not exists (select room_id from living where room_id = "7758521")
java 8 的stream 原理与使用
list.stream().mapToDouble(Integer::doubleValue).average();
一个大excel,如何load 到db 里面
前端上传excel 到某个cdn,拿到链接,给到后端,后端下载,然后后台执行写入。
提供给前端的api ,数据如何校验,如果有异常,怎么抛出,前端如何拿到
根据不同的规则校验
包装response 返回给前端
有两个db,先写上海学生成绩库,再写教育部学生成绩库
这个场景,两个库的关联事务如何做处理,两个场景,上海库成功了,教育部没成功,怎么两个都回滚,这样如何处理
分布式事务:Spring Boot多数据源事务管理_卓立的博客-CSDN博客_多数据源事务控制
分布式事务,看这篇就够了 - 知乎
两阶段提交:同一方法下两个数据源的回滚_分库分表下事务问题的一些解决思路
大量数据从db 导出到excel,怎么处理,从而避免out of memory
后台处理,返回链接,然后去下载
spring batch 是什么
参考链接:通过例子讲解Spring Batch入门,优秀的批处理框架 - 知乎
hive、hbase
问题:
大家都把流比作水流,从一端流向一端,当向输出流写去数据,这些数据是立即到达目的地还是在始端还是在流里面?如果在流里面,我就想不通流里面还可以放数据?放数据的不是只有这两端吗?例如:网络上主机A建立目的地为主机B的输出流,A向输出流写数据,那么数据不是放到A也不是B,难道是放到网线上?这也太离谱了吧?
回答:
流比喻数据的均匀连续性。
计算机最小单位是字节,所以流数据最小单位也是字节(byte)
流这个比喻的本质就是为了让某个对象可以均匀持续地吐出无间隔的数据,或者你可以向这个对象毫无压力地持续不断塞进数据。。。。至于那些管子。。。恰恰是因为现实世界不够符合这个要求,所以必须有。
管子什么的,那不是流,那是不得不要靠它们来装(装逼的装 不是装放的装)流的枝节。
看片子很卡,你会有“流”的任何联想和感受吗。。怎么办?你先等等,操作系统已经准备了个长管子,帮你缓存一大段你再看,这回就很流很流的感觉了。
参考:https://www.zhihu.com/question/28457447/answer/44911601
流是个抽象的概念,是对输入输出设备的抽象,Java程序中,对于数据的输入/输出操作都是以“流”的方式进行。设备可以是文件,网络,内存等。
Kafka的 Topic 被分成了若干分区,每个分区在同一时间只被一个 consumer 消费。这意味着每个分区被消费的消息在日志中的位置仅仅是一个简单的整数:offset。这样就很容易标记每个分区消费状态就很容易了,仅仅需要一个整数而已。这样消费状态的跟踪就很简单了。
每个消费者(Consumer)对每个Topic都有一个offset用来表示 读取到了第几条数据
Kafka允许topic的分区拥有若干副本,这个数量是可以配置的,你可以为每个topic配置副本的数量。Kafka会自动在每个个副本上备份数据,所以当一个节点down掉时数据依然是可用的。
Kafka的副本功能不是必须的,你可以配置只有一个副本,这样其实就相当于只有一份数据。
Zookeeper 主要用于在集群中不同节点之间进行通信
在 Kafka 中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取除此之外,它还执行其他活动,如: leader 检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。
spring 提供的事务管理机制,主要有编程式和声明式两种。
编程式是在代码中手动管理事务的提交、回滚等操作,代码侵入性比较强。
声明式事务:使用注解,基于aop 切面完成。
声明式事务也有两种实现方式,是基于TX和AOP的xml配置文件方式,二种就是基于@Transactional注解了。
propagation,事务的传播属性,那六种。(required 为默认)
required、(没有则创建一个,仅对b 生效)
support、
mandatory、
requires_new、(重新创建一个,如果A 存在事务,则暂停当前的事务。A 抛异常,不影响B。各回滚各的)
not_supported、(非事务方式运行,如果当前存在事务,则暂停)
never、(非事务方式运行,如果当前存在事务,则抛异常)
nested(嵌套的,如果a 存在,则b 嵌套其中执行,否则同required。父回子必回,子回父不一定回)
isolation 属性,事务的隔离级别
默认为:Isolation.DEFAULT
solation.DEFAULT:使用底层数据库默认的隔离级别。
Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
timeout 属性
timeout :事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly 属性
readOnly :指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。
rollbackFor 属性
rollbackFor :用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。
noRollbackFor属性
noRollbackFor:抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。
1. 应用在非public 修饰的方法上
2. propogation 设置错误
3. 属性rollbackFor 设置错误(spring 默认是runtimeException 或者Error)
4. 同一个类中方法调用,导致失效
5. 异常被catch
6. 数据库引擎不支持事务
参考:https://baijiahao.baidu.com/s?id=1661565712893820457
args[]这个参数主要是为程序使用者在命令行状态下与程序交互提供的一种手段。
在命令行中执行java程序的时候使用的是“java 文件名 args参数”。所以args[]是在命令行运行时输入的参数,由于可以有多个参数,所以定义成数组。之所以要把args[]数组定义为String类型是因为java缺省就认为输入的参数都为字符串,之后自己可以进行类型转换。
分布式id:分布式自增序列id的实现(三) ---分布式序号生成器---基于Zookeeper客户端Curator提供的DistributedAtomicLong自增功能_russle的专栏-CSDN博客
使用reids 实现限流的措施是存了一个
分布式限流中间件,sentinel
单机流控,集群流控。
集群流控角色分为token server / token client,token client 向server 请求token
集群流控支持两种阈值计算方式:集群总体模式、单机均摊模式(按照链接的单机均摊的推算总体的 )
集群流控两种部署方式:独立部署(单独启动一个token server 来处理token client 的请求)、嵌入部署(在多个 sentinel-core 中选择一个实例设置为 token server)
redis 令牌桶的实现注释:
需要保存什么数据在redis中?
保存“最新请求的时间”的原因:如果再来一个请求,那么使用当前来到的请求时间,减去已存的“最新请求时间”,得到一个时间差,除以令牌产生速率,就可以得到这段时间的令牌数量。
参考:https://www.cnblogs.com/Chenjiabing/p/12534346.html
保护高并发服务:限流、缓存、降级
rateLimiter 的使用:https://zhuanlan.zhihu.com/p/60979444
多线程适用于什么场景
适用于io 密集型的任务
cpu 密集型任务的特点就是要进行大量的计算,消耗cpu 资源。如果用多任务完成,但是任务越多,花在任务切换的时间就越多。所以这种应该让进程数量等于cpu 核心数
io 密集型任务特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成,对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务。这种适合使用高效的脚本语言进行开发
https://segmentfault.com/a/1190000039947938敖丙 java famaily:https://github.com/AobingJava/JavaFamily/
敖丙csdn 整理的内容
fail-fast的字面意思是“快速失败”。当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构被改变的话,就会抛出异常,防止继续遍历。这就是所谓的快速失败机制。
意思就是说,当Iterator这个迭代器被创建后,除了迭代器本身的方法(remove)可以改变集合的结构外,其他的因素如若改变了集合的结构,都被抛出ConcurrentModificationException异常。
hashmap 中的modcount 的作用,是在进行迭代的过程中,会把modcount 赋值给迭代器expectedModCount,迭代器在做操作的时候,如果检查和hashmap 中的modcount 不一致,则代表有其他线程修改了。这就是fail-fast 机制
参考:https://zhuanlan.zhihu.com/p/37476508
1.如果非要在遍历的时候修改集合,那么建议用迭代器的remove等方法,而不是用集合的remove等方法。(老实遵守阿里巴巴java开发规范……)
2.如果是并发的环境,那还要对Iterator对象加锁;也可以直接使用Collections.synchronizedList。
3.CopyOnWriteArrayList(采用fail-safe)
copyOnWrite,写时复制,简单理解就是,当我们往一个容器添加元素的时候,先将当前容器复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
参考:一文彻底弄懂fail-fast、fail-safe机制(带你撸源码) - 知乎
使用数组作为map 中的key,使用的是数组的地址引用的hashcode。
如果非要使用,那么就得取值,遍历,得到结果。
hashmap 插入流程
(java 7)
初始化;null key 判断;计算下标;key 是否相等;是否扩容;插入
先扩容,再插入;头插法
(java 8)
初始化;获取下标;key 是否相等;插入;树阈值判断;是否扩容
先插入,再扩容
头插法:
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//遍历表
for (Entry e : table) {//循环遍历表中的每个index
while(null != e) {//开始循环
Entry next = e.next;//保存该节点的next节点,作为下一次循环使用
if (rehash) {//是否重新计算hash值
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);//重新计算下标
e.next = newTable[i];//把next引用指向新下表的头节点
newTable[i] = e;//放入新下标链表的头节点
e = next;//取出保存的next节点,准备开始下一次循环
}
}
}
总结就是,对table 中的每个桶做遍历,同时对桶上的节点(如果有链表)进行遍历
对节点内的链表遍历的时候,使用头插法
while (null != e)
弄个next = e.next
对e 进行重新计算下标
获得此下标的节点
设置e.next = newTable[i]
然后把e 放到这个节点中newTable[i] = e
然后e = next,进行下一次遍历
尾插法:就是遍历,找到最后,插入
看起来尾插法效率不高,这么做的原因是
简化头插法transfer
Entry next = e.next;//保存正在迁移节点的next节点
int i = indexFor(e.hash, newCapacity);//计算迁移节点的新下标
e.next = newTable[i];//把新下表桶中的头节点设为迁移节点的next节点
newTable[i] = e;//
e = next;
比如线程a 执行扩容,然后就被挂起
线程b 完成扩容
此时线程a 继续进行直到新节点的复制操作
b 完成扩容之后,比如说原来是 3 -> 7,扩容完成之后变成 7 -> 3
线程a 如果继续执行,那么就会变成 3 -> 7,形成死循环
hashmap 中红黑树演变的介绍:
链表转为红黑树:节点个数大于8 且数组个数大于64
红黑树转为链表,涉及到两个操作
1. remove 元素的时候,不是小于6 一定变为链表,而是通过根节点和节点的子节点判断的,最大可能是10 个
(在红黑树的root节点为空 或者root的右节点、root的左节点、root左节点的左节点为空时 说明树都比较小了)
2. resize 的时候才会根据6 这个因子做判断,小于等于6 的时候
参考:掘金
另外参考:HashMap真的是大于8就转换成红黑树,小于6就变成链表吗???_日常发呆-CSDN博客_hashmap超过多少转红黑树
这个可以看看:为什么Map桶中个数超过8才转为红黑树_sinat-CSDN博客_为什么超过8转为红黑树
map 相关的问题:21个关于HashMap刁钻的面试题,第四个我就跪了-华为云
对于队列的几个方法,
添加元素
add() 如果队列已满,则抛异常
offer() 如果队列已满,则返回false
移除元素
remove() 如果队列为空,抛异常
poll() 如果队列为空,返回null
其他场景面试题目
https://segmentfault.com/a/1190000039947938
https://segmentfault.com/a/1190000039947938
什么情况需要重写hashcode 和equals 方法
遵循规则:
所以说,重写equals,必须重新hashcode(因为hashCode是对堆内存的对象产生的特殊值,如果没有重写,不同对象产生的哈希值基本是不同的)
集合中判断对象是否相同,也是先判断hashcode,再判断equals
(equal 里面是判断(obj == this),即判断二者的地址是否相同)
(默认的hashcode方法是根据对象的内存地址经哈希算法得来的)
参考:重写equals和hashCode方法 - 云+社区 - 腾讯云
实际应用中,如果需求,只要内容一致,那么就认为一致,那就要重写
比如说string 的equal 的用法
里面首先判断地址是否一致(判断是否为自身)
如果不一致,判断待比较对象是否为String 类型(如果不是返回false)
如果是,判断长度、判断每个字符是否相等
然后String 的hashCode() 方法也做了重写,通过遍历每个字符,得到一个最终的值
map 的插入流程(已看完)
jdk1.7 和1.8 多线程下的区别:JDK1.7和JDK1.8中HashMap为什么是线程不安全的?_张先森的博客-CSDN博客_hashmap为什么是线程不安全的
一些基本点
数组内存空间是连续的,链表内存空间是离散的。
hashmap 底层是由数组 + 链表构成的,寻址容易,插入删除也容易。(存储单元数组Entry[],数组里面包含链表)
jdk 1.7 的hashmap 的基础是一个线型数组Entry[ ],transient 修饰。冲突的时候,使用头插法
数组下标这么算:index=hash%Entry[].length; hash 是通过key 的hashcode() 取得的。
concurrentHashMap get、put、remove 的时候通过锁“段”(默认分为16 个段)来达到并发,只有在计算resize 的时候才锁整个map
jdk 1.8 的hashmap 的基础是一个线型数组Node[ ],transient 修饰。
hashmap 中各个成员变量为什么用transient
Object 的hashCode() 方法是个native,
不同的jvm 可能有不同的实现,对同一个字符串产生的hashcode 可能不一致。
对于hashmap 重写了序列化方法
对于ArrayList,序列化的时候去掉了扩容里面的null 元素
参考:https://blog.csdn.net/u010454030/article/details/79416996
hashmap 和concurrentHashMap 的区别:
40 个 Java 集合面试官常问问题和答案:
40 个 Java 集合面试官常问问题和答案_诗琪芮的博客-CSDN博客
Java集合必会14问(精选面试题整理):
Java集合必会14问(精选面试题整理) - 知乎
40 个 Java 集合面试官常问问题和答案_诗琪芮的博客-CSDN博客
Map 接口及Collection 接口是所有集合框架的父接口。
线程安全:HashTable/StringBuffer
线程不安全:HashMap/TreeMap/HashSet/ArrayList/LinkedList
arrayList 的空间浪费主要体现在list 列表结尾会预留出一定的空间容量。linkedList 的空间花费提现在它的每一个元素都要存放前驱结点、后继节点、和数据。
hashTable 方法是synchronized 的。HashMap 的初始容量为16,HashTable 的初始容量为11。填充因子默认都是0.75。Map 扩容:x2,table 扩容:x 2 + 1
HashTable 和HashSet 的区别,HashSet 就是基于hashMap 实现的,只不过hashset 里面的hashmap 的所有value 都是同一个object 而已。线程不安全。
重点:hashMap 在java 1.7 和1.8 的区别;
首先hashmap 是线程不安全的,体现在三个方面
对于死循环,在1.8 中不存在了。死循环是因为在resize 过程中对链表进行了倒排序处理;在1.8 中不会再有倒排序处理,所以不会死循环。put 操作,是先把数据放入map 中去,再根据元素的个数决定是否做resize。如果线程操作完了A -> B,此时线程B 又来了,就会造成A -> B -> A 的情况。
数据丢失,如果有两个线程同时判断table[i] == null,这时两个线程都会创建Entry,就出出现数据丢失的情况。
数据重复,如果两个线程同时发现自己的key 不存在,而这两个线程的key 实际是相同的。在像链表中写入的时候,第一个线程将e 设置为自己的entry,而第二个线程执行到了e.next,此时拿到的是最后一个节点,已然会把已持有的数据插入到链表中,这样就会出现了数据重复。
HashMap的线程不安全主要体现在下面两个方面:
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
1.7 先扩容再插入,有可能避免哈希冲突。使用头插法
1.8 先插入再扩容,(而jdk8如果要先扩容,由于是尾插法,扩容之后还要再遍历一遍,找到尾部的位置,然后插入到尾部。(也没怎么节约性能))
concurrentHashMap 在1.8 对于1.7 的区别。
去除 Segment + HashEntry + Unsafe
的实现,
改为 Synchronized + CAS + Node + Unsafe
的实现
ConcurrentHashMap 和HashTable 的区别。
hashtable 的方法都加了synchronized
jdk1.7 concurrentHashMap 底层采用分段数组+链表实现。jdk1.8 采用数组+链表/红黑二叉树实现。
1.8 采用了 synchronized + CAS 算法来保证线程安全。
1.7 中,ConcurrentHashMap 使用“分段锁”机制实现线程安全,数据结构可以看成是"Segment数组+HashEntry数组+链表",一个 ConcurrentHashMap 实例中包含若干个 Segment 实例组成的数组,每个 Segment 实例又包含由若干个桶,每个桶中都是由若干个 HashEntry 对象链接起来的链表。因为Segment 继承ReentrantLock,所以能充当锁的角色,通过 segment 段将 ConcurrentHashMap 划分为不同的部分,就可以使用不同的锁来控制对哈希表不同部分的修改,从而允许多个写操作并发进行,默认支持 16 个线程执行并发写操作,及任意数量线程的读操作。
15道常见的 Java集合类 面试题:
15道常见的 Java集合类 面试题 - 知乎
hashmap 的实现、扩容机制、java8 的改变 hashmap 1.7 使用的是“头插法”,hashmap 1.8 使用的是“尾插法” 多线程机制下,hashmap1.7 会导致死循环的问题,扩容的时候导致。在get 的时候就会进入死循环。 java1.8 在put的时候,判断没有冲突,然后赋值,会导致数据丢失
concurent hash map 设计思路,单独锁、分段锁、无锁。 concurrent 1.7 的实现,是segment + hashEntry + ReentrantLock 组成,结构仍然是数组加链表, concurrent 1.8 采用Node + CAS + Synchronized 来保证并发安全 对于concurrent 1.8,Java 提供了volatile 来保证可见性、有序性,但是不保证原子性 get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。 数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
没空看:
❤️集合很简单?开什么玩笑?肝了一周,全是精华,万字讲解!面试再不怕集合问题了!!!❤️:
❤️集合很简单?开什么玩笑?肝了一周,全是精华,万字讲解!面试再不怕集合问题了!!!❤️_程序员springmeng-CSDN博客
arrayList 扩容,默认1.5 倍,没有缩容机制
(后加的)
paxos 和raft 的区别:
Paxos和Raft共识算法(一) - 知乎
并发编程几个特性:
数据库事务正确执行四个要素:
分布式系统中CAP 理论:以下三个只能同时满足两个。
线程不安全的时候,可以使用synchronized 关键字或者Lock 锁。
https://xiaomi-info.github.io/2020/03/24/synchronized/
(Volatile 关键字只能保证可见性 & 有序性,不能保证原子性。简单来说,线程A 读取变量a 之后被阻塞,线程B 操作完之后并刷回内存,这时候根据可见性原则,其他线程对a 就可见了。参考:volatile为什么不能保证原子性 - 简书 最后一段
另外的解释:volatile方式的i++,总共是四个步骤:i++实际为load、Increment、store、Memory Barriers 四个操作。内存屏障是线程安全的,但是内存屏障之前的指令并不是.在某一时刻线程1将i的值load取出来,放置到cpu缓存中,然后再将此值放置到寄存器A中,然后A中的值自增1(寄存器A中保存的是中间值,没有直接修改i,因此其他线程并不会获取到这个自增1的值)。如果在此时线程2也执行同样的操作,获取值i==10,自增1变为11,然后马上刷入主内存。此时由于线程2修改了i的值,实时的线程1中的i==10的值缓存失效,重新从主内存中读取,变为11。接下来线程1恢复。将自增过后的A寄存器值11赋值给cpu缓存i。这样就出现了线程安全问题。
重点:如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的
)
原子操作:不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有线程切换的操作。(整个操作被视为一个整体)
以上参考:
一文彻底搞懂CAS实现原理 - 知乎
可以不用看链接里关于cas 原理的一部分,太底层了。
cas 原理:
一个变量简单的读取和赋值操作是原子性的,将一个变量赋值给另外一个变量不是原子性的。
一个变量赋值给另一个变量,比如说int a = b; 先把b 的值读取到寄存器内存中,然后再把该值从寄存器搬运到内存a 的地址中。
cas 被认为是一种乐观锁,乐观锁其实就是一种思想
compare and swap,其中有几个值:
valueOffset,value 变量的内存偏移地址,也就是那个要操作的变量的实际地址,举例来说,要对int a = 10;做操作,就是a 的地址。
expect,期望更新的值,也就是如果要执行a++,就是在a = 10 的基础上加1,也就是说期望更新到值是10。
update,要更新的最新值,也就是说,更新完成之后,a 的值应该是11,那么update 就应该是11。
其中value 是使用volatile 修饰保证可见性
假设内存中数据的值为V,旧的预期值为A,新的修改值为B。那么CAS操作可以分为三个步骤:
1)将旧的预期值A与内存中的值V比较;
2)如果A与V的值相等,那么就将V的值设置为B;
3)返回操作是否成功。
对于底层,使用的是总线加锁(加锁总线,效率低)或者缓存加锁(加锁某一个共享内存变量的地址)来实现原子操作。
cas 产生的问题:
几个角色:很多用户(用户线程),共享资源(取药窗口)。在用户线程和共享资源之间,是通过中间系统来协调控制的,这里面就会涉及锁的概念。
锁
是用来控制多个线程访问共享资源的方式。一个锁能防止多个线程对共享资源的同时访问,有些锁也允许多个线程并发访问共享资源,比如读写锁。
在 Java 中经常使用的锁是 synchronized,synchronized 会隐式的获得锁,但它必须是先获得锁再释放锁。这种方式简化了同步的管理,但扩展性不如 Lock 显示的获得锁和释放锁更加灵活。
从性能上来讲,当并发量高、竞争激烈的场景下,Lock 锁会较 synchronized 性能上表现的
更稳定些。反之,当并发量不高的情况下,synchronized 有分级锁的优势,因此两者性能差不多,synchronized 相对来说使用上更加简单,不用考虑手工释放锁。
Lock 显示的锁使用,因为使用上更加灵活,这得益于其底层基础同步框架的实现机制,它就是 AQS。
底层使用AQS 设计,抽象出来统一的同步协调处理器,作为并发包构建的基本骨架。
ReentrantLock 基于AQS 的实现(参考:https://zhuanlan.zhihu.com/p/141715040)
final void lock() {
if (compareAndSetState(0, 1)) // compareAndSetState底层其实是调用的unsafe的CAS系列方法
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
lock方法先通过CAS尝试将同步状态(AQS的state属性)从0修改为1。若直接修改成功了,则将占用锁的线程设置为当前线程
CAS操作未能成功,说明state已经不为0,此时继续acquire(1)操作,这个acquire()由AQS实现提供
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
简单解释下:
tryAcquire方法尝试获取锁,如果成功就返回,如果不成功,则把当前线程和等待状态信息构适成一个Node
节点,并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。
通过state 累加实现“重入”的功能
多线程独占式并发工具:
1)ReentrantLock
可重入锁,同一时刻仅允许一个线程访问,所以可以称作 独占锁
,线程可以重复获取同一把锁。
多线程共享式并发工具:
1)ReentrantReadWriteLock
可重入的读写锁,允许多个读线程同时进行读,但不允许写-读、写-写线程同时访问。
适用于读多写少的场景下。
底层使用AQS 实现,也是使用state 来表示锁
如何用一个共享变量来区分锁是写锁还是读锁呢?答案就是按位拆分(前16 位表示读锁,后16 位表示写锁)
由于state是int类型的变量,在内存中占用4个字节,也就是32位
。将其拆分为两部分:高16位和低16位,其中高16位用来表示读锁状态,低16位用来表示写锁状态
。当设置读锁成功时,就将高16位加1,释放读锁时,将高16位减1;当设置写锁成功时,就将低16位加1,释放写锁时,将第16位减1
假设锁当前的状态值为S,将S和16进制数0x0000FFFF
进行与运算
,即S&0x0000FFFF,运算时会将高16位全置为0,将运算结果记为c,那么c表示的就是写锁的数量
将S无符号右移16位
(S>>>16),得到的结果就是读锁的数量
参考:
https://zhuanlan.zhihu.com/p/91408261
2)CountDownLatch(发令枪)
主要用来解决一个线程等待 N 个线程的场景。
就像短跑运动员比赛,等到所有运动员全部都跑完才算竞赛结束。
CountDownLatch countDownLatch = new CountDownLatch(3);
countDownLatch.await();
countDownLatch.countDown();
countDownLatch.countDown();
countDownLatch.countDown();
底层使用AQS,使用state 来表示计数值的大小
比如说主线程,等待所有子线程执行完成,然后继续执行
3)CyclicBarrier(同步屏障)
主要用于 N 个线程之间互相等待。
就像几个驴友约好爬山,要等待所有驴友都到齐后才能统一出发。
参考:深入浅出java CyclicBarrier - 简书
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
cyclicBarrier.await();
cyclicBarrier.await();
cyclicBarrier.await();
CountDownLatch和CyclicBarrier的区别
(01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。
(02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
原文链接:https://blog.csdn.net/BThinker/article/details/104417813
4)Semaphore
限流场景使用,限定最多允许N个线程可以访问某些资源。
就像车辆行驶到路口,必须要看红绿灯指示,要等到绿灯才能通行。
Semaphore semaphore = new Semaphore(2);
semaphore.aquire();
semaphore.release();
基于上述这些并发包工具,我们可以根据多线程的不同使用场景去选择。JDK 提供的这些并发包基本能够满足了大部分的开发者的使用需求。
以上参考:通过一个生活中的案例场景,揭开并发包底层AQS的神秘面纱 - Java爱好者社区 - 博客园
linux 操作系统基础知识:
只要你认真看完一万字☀️Linux操作系统基础知识☀️分分钟钟都吊打面试官《❤️记得收藏❤️》_苏州程序大白的博客-CSDN博客
AQS 简介:通俗理解AQS原理及与Reentrantlock的关系_Lynn_coder的博客-CSDN博客_lock和reentrantlock关系
aqs 的简简单单过一遍,看里面的“过程”:AQS简简单单过一遍 - 知乎
上面两个链接配合着看
重入锁:到底什么是重入锁,拜托,一次搞清楚! - 知乎,可重入锁(good)-阿里云开发者社区
java可重入锁与不可重入锁_慎独-CSDN博客_重入锁
锁类型的总结
阿里面试失败后,一气之下我图解了Java中18把锁_爱笑的架构师-CSDN博客
ReentrantReadWriteLock
互斥锁
是独占锁的一种常规实现)、读写锁(读写锁
是共享锁的一种具体实现)ReadWriteLock可重入锁
又称之为递归锁
,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。(Syn & Reentrant 都是可重入,一定程度避免死锁)自旋锁
是指线程在没有获得锁时不是被直接挂起,而是执行一个忙循环,这个忙循环就是所谓的自旋。为了减少线程被挂起的几率。自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock
参考:自旋锁VS适应性自旋锁 - 简书
分段锁。分段锁
是一种锁的设计,并不是具体的一种锁。目的是将锁的粒度进一步细化,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
锁升级。(无锁 | 偏向锁 | 轻量级锁 | 重量级锁)
JDK1.6 为了提升性能减少获得锁和释放锁所带来的消耗,引入了4种锁的状态:无锁
、偏向锁
、轻量级锁
和重量级锁
,它会随着多线程的竞争情况逐渐升级,但不能降级。
java 中synchronized 内部实现原理就是锁升级的过程。
无锁,如CAS 的形式。乐观锁形式
偏向锁,是指它会偏向于第一个访问锁的线程。通过控制对象的标志位来实现,如果是可偏向的,判断对象头存储的现成id 是否和本线程一致。
轻量级锁,竞争程度变得比较激烈的时候,偏向锁就会升级为轻量级锁
,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式
等待上一个线程释放锁。
重量级锁,如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁
,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
锁优化技术
锁粗化(将多个同步块数量减少,将单个同步块的作用范围加大)
锁消除(锁消除
是指虚拟机编译器在运行时检测到了共享数据没有竞争的锁,从而将这些锁进行消除。)
偏向锁:无竞争场景
轻量级锁:轻微竞争场景。自旋,稍加等待即可,无需切换线程上下文。
重量级锁:竞争大的场景。
由低到高依次升级。开销也依次增加。
无锁、偏向、轻量、重量,一个比较好的解释:无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁 - 简书
cas 和自旋的关系:https://segmentfault.com/q/1010000021946513
Lock比synchronized多了以下功能(Lock的优势):
当然,Lock也不是完美的,否则java就不会保留着synchronized关键字了,显示锁的缺点主要有两个:
由于synchronized是JVM实现的,因此JVM可以对其一些优化,比如Java 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
参考:https://www.zhihu.com/question/429967101/answer/1575770226
关于mesi 还可以参考:缓存一致性协议(MESI) - 一念永恒乐 - 博客园
(这个写的太多了,有时间再看吧:内存屏障(Memory Barrier)究竟是个什么鬼? - 知乎)
volatile 保证可见性:缓存一致性协议。
volatile 保证有序性:通过插入内存屏障来禁止指令重排序。
内存屏障:内存屏障是一种CPU指令,它的作用是对该指令前和指令后的一些操作产生一定的约束,保证一些操作按顺序执行。
参考:面试官:请说下volatile的实现原理 - 知乎,写的清晰。
什么是状态机:什么是状态机? - 知乎
状态机就是一个数学模型,通常体现为状态转换图。四个状态:State, Event, Action, Transition.
线程同步机制
几个场景
参考:Java 如何线程间通信,面试被问哭。。。 - 知乎
start()方法来启动线程,真正实现了多线程运行,这时无需等待。
run()只是一个普通方法,程序还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码。还是只有主线程这一个。
调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。
Java Thread 的 run() 与 start() 的区别 - 灰色飘零 - 博客园
Thread.sleep(0) 的作用:触发一次操作系统的重新竞争
参考:Sleep(0)的妙用_HawkJony的博客-CSDN博客_sleep
线程切换状态,参考:Java多线程--线程各状态如何进行切换_哆啦咪~fo-CSDN博客_线程切换过程
Thread.sleep() 和Objects.wait() 的区别
参考:Java中sleep()和wait()的区别 - 簡書
ExecutorService 可以执行Runnable 和Callable 任务。
任务创建完成,可以使用多种方法提交任务到ExecutorSerivice 中去。
ExecutorService 的停止:
因为提供了两个方法,所以最佳实战就是同时使用这两种方法,并结合awaitTermination() 方法。
executorService.shutdown();
try {
if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
Future 接口
Future 接口的对象允许我们获取任务执行的结果或检查任务的状态(正在运行还是执行完毕)
Future 接口的get() 方法,是阻塞的。如果是Callable 任务,则返回结果;如果是Runnable 任务,则返回null。
因为get() 方法是阻塞的,如果调用get() 方法的时候任务还一直在运行,那么调用将会一直被阻塞,直到任务执行完成并返回结果之后才返回。
get 方法可能会导致程序可用性降低,所以可以设置超时时间,避免长时间阻塞。如果超过,那么抛出TimeoutException 异常。
除了get 方法,还有isDone(),cancel(),isCanceled()
ScheduledExecutorService 接口
用于在预定义的延迟之后运行任务或者定期执行任务
以上关于ExecutorSerivce 来自:
一文秒懂 Java ExecutorService - Java 一文秒懂 - 简单教程,简单编程
ThreadPoolExecutor
是一个可被继承的线程池实现。
对于ThreadPoolExecutor 的实例化,其中有几个主要的参数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler);
调度策略:
需要了解内容:
FixedThredPool: new ThreadExcutor(n, n, 0L, ms, new LinkedBlockingQueue()
SingleThreadExecutor: new ThreadExcutor(1, 1, 0L, ms, new LinkedBlockingQueue())
CachedTheadPool: new ThreadExcutor(0, max_valuem, 60L, s, new SynchronousQueue());
ScheduledThreadPoolExcutor: ScheduledThreadPool, SingleThreadScheduledExecutor.
以上参考:ThreadPoolExecutor详解 - 知乎
一文秒懂 Java 线程池之 ThreadPoolExecutor - Java 一文秒懂 - 简单教程,简单编程
介绍的比较好的文章:
Java线程池面试必备:核心参数、工作流、监控、调优手段
参考:再有人问你分布式锁是什么,就把这个丢给他! - 知乎
使用分布式锁,是分布式系统中互斥访问共享资源的一种方式
分布式锁的问题
非集群的时候:
集群:
实现可以使用jedis 的一个函数:jedis.set(String key, String value, String nxxx, String expx, int time)
第三个参数nxxx:把key、value set到redis中
nx : not exists, 只有key 不存在时才把key value set 到redis
xx : is exists ,只有 key 存在是,才把key value set 到redis
第四个参数expx:参数有两个值可选 :
ex : seconds 秒
px : milliseconds 毫秒
参考:jedis set 的四个重载方法(byte[]的四个自动忽略)
以上参考:分布式锁的实现之 redis 篇 | 小米信息部技术团队
reids 发布订阅模式:订阅与发布 — Redis 设计与实现
加锁:
map
里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间。删除锁步骤:
缺点:master 宕机问题;脑裂问题。
以上参考:Redisson 实现分布式锁原理分析 - 知乎
redis 发布订阅机制:
发布者和订阅者都是Redis客户端,Channel则为Redis服务器端,发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
没有持久化,没有消息保障机制
参考:Redis学习汇总:Redis发布订阅机制,面试必知必会! - 知乎
网上比较流行的做法是,setnx + lua 脚本,而在分布式系统中,使用主从结构,就有可能造成脑裂,对于cap 满足了ap 不能满足c。
redlock 需要多个实例,但都是独自部署,没有主从结构。避免redis 异步复制造成的锁丢失问题。
红锁加锁:
红锁释放锁:
红锁的问题:
假设有A B C D E 五个节点:
这样客户端1 和2 针对统一资源都获取了锁。
针对上述问题,AOF 1s 同步一次,最坏情况下丢失1s 的数据;可以设置fsync 同步数据,性能会下降,但是由于系统问题,仍然可能丢失数据。
提出延迟重启功能,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。
==============================================
大神的争论:关于Redis RedLock算法的争论 | 码农网
还可能存在的问题:client1 获取锁之后,发生了full GC,超过expire time,然后client2 获取了锁,进行写数据。然后client1 苏醒过来,也进行了写数据,导致问题。(不仅FGC,其他导致系统停顿,比如说IO或者网络的堵塞或波动等)
解决:设置一个token,获取锁的时候给到,在提交改数据的时候,判断token 是否小于上一次提交修改的token,小于则拒绝。(这样就不需要分布式锁了)
==============================================
三种实现redis 锁的简介:细说Redis分布式锁:setnx/redisson/redlock?了解一波? - 知乎
红锁介绍:分布式锁之(RedLock)红锁_Forest24的博客-CSDN博客_redlock分布式锁
redis 命令不区分大小写,key 严格区分大小写
最大运输key 大小512M
redis 的五种基本类型
string、hash、list、set、zset
redis 127.0.0.1:6379> SET runoob "菜鸟教程" OK redis 127.0.0.1:6379> GET runoob "菜鸟教程"
redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World" "OK" redis 127.0.0.1:6379> HGET runoob field1 "Hello" redis 127.0.0.1:6379> HGET runoob field2 "World"
list 的基本命令:
lpush 左边插
rpush 右边插
lset 是有index 的,做替换处理
lrem key "hello" 删除key 代表的list 中相同元素"hello"
set 的基本命令:
redis 的set 是无需的string 类型的集合
底层是通过哈希表实现的,添加、删除、查找时间复杂度都是O(1)
集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
redis 127.0.0.1:6379> DEL runoob redis 127.0.0.1:6379> sadd runoob redis (integer) 1 redis 127.0.0.1:6379> sadd runoob mongodb (integer) 1 redis 127.0.0.1:6379> sadd runoob rabbitmq (integer) 1 redis 127.0.0.1:6379> sadd runoob rabbitmq (integer) 0 redis 127.0.0.1:6379> smembers runoob
zset 的基本命令
和 set 一样也是string类型元素的集合,且不允许重复的成员。
每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
zadd key num1 value1 num2 value2 添加元素,其中num数值用于排序,value才是真正的元素,num相同时以元素字典顺序排
zrange key start end [withscores] 返回索引范围内的元素,加上withscores后会连同分数一起返回
zrevrange key start end [withscores] 逆序返回元素,其它同zrange
zrem key member 删除元素
zcard key 返回元素个数
zcount key min max 分数在min和max之间的成员的个数
redis 过期机制
定期删除(随机取一些,判断是否过期。但是如果太多了,也删不过来)
惰性删除(client 在访问key 的时候,判断是否过期,如果过期了就删除,同时不会将结果返回客户端)
redis 内部删除过期key 的机制:「定期删除」+「惰性删除」两者配合的过期策略
问题:假如有些key 既过期也没有客户端访问,还是会导致内存耗尽,这时候就要使用内存淘汰机制了
内存淘汰机制:
内存耗尽,则自动触发该机制,选取key 删除
淘汰策略、阈值可配置
在持久化的过程中,过期的不会进入到持久化文件中;数据恢复过程中,AOF 会对过期的数据增加del 命令。
内存淘汰机制的几种(有六种,分类1 个,2 个,3 个):
volatile-random:在设置了过期时间的key中,随机移除key
volatile-ttl:在设置了过期时间的key中,将最早过期的key移除(ttl:time to live)
以上参考:掘金
题外话:
lru 最近最少未使用算法实现举例:
最近最久未使用算法(LRU)介绍与实现 - 知乎
redis 数据类型底层结构
reids 所有数据结构都是由redisObjects 包装的。
里面包含几个内容
可以参考这个看看看:redis的embstr为什么是39B | 笔记本
如果存储是long 类型的数字,使用int 类型编码
如果小于等于39 字节,使用embstr 类型编码
如果大于39 字节,使用sds 数据结构,使用raw 类型编码(后来版本改为了44 字节)
struct sdshdr {
int len;
int free;
char buf[];
}
list
是有序可重复列表
hash
set
zset
以上参考:https://segmentfault.com/a/1190000040102333
介绍的是不同数据结构的基本介绍
几个重要的数据结构:
sds
ziplist。是个压缩列表,包含几个元素:压缩列表占用字节数;最后一个元素距离压缩列表起始位置偏移量,用于快速定位到最后一个节点;元素个数;元素内容列表;压缩列表结束标识
skiplist。跳表。关键词:多级索引,
比红黑树高效的地方:查找区间内所有元素
以上参考:Redis数据结构底层实现 - 米扑博客
有序集合的实现方式
元素数量小于128 个,同时每个元素长度小于64 字节,使用ziplist
不满足以上两个条件就会使用跳表,具体是组合了map 和skiplist
总结一下跳表原理:
redis中skiplist的MaxLevel设定为32层
skiplist原理中提到skiplist一个元素插入后,会随机分配一个层数,而redis的实现,这个随机的规则是:
为了提高搜索效率,redis会缓存MaxLevel的值,在每次插入/删除节点后都会去更新这个值,这样每次搜索的时候不需要从32层开始搜索,而是从MaxLevel指定的层数开始搜索
插入的过个:
(这个拿不准)
其中map 存的是value -> score score -> value的映射。在zset 做插入操作的时候
(应该是score -> value 的映射,socore 可重复,value 不可以重复。set 结构)
zadd [zset name] [score] [value]:
如果score一样怎么办?根据value再排序,按照顺序插入
参考:掘金
持久化方式,区别
rdb 是快照,把当前数据持久化到硬盘
aof 是将每次执行的写命令保存到硬盘(类似于 MySQL 的 binlog)
Redis 高可用特性之 “持久化” 详解
aof 流程:Redis中的AOF工作流程_姜秀丽的博客-CSDN博客
掘金
redis 集群, 哈希槽是redis 集群的实现方式,官方集群,一个数据分片,
redis 剩下的几种类型,HyperLogLog、Geo、BloomFilter
HLL 提供不精确的去重计数
redis 为什么快
基于内存
单线程
io 多路复用
讲讲 reids 2.5 支持的事务,伪事务,原理
MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行。
通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。
watch 命令,监视一个(或多个)key,如果在事务执行之前这个(或多个)key被其他命令所改动,那么事务将被打断
WATCH命令可以被调用多次。 对键的监视从 WATCH 执行之后开始生效, 直到调用 EXEC 为止。
当 EXEC
被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。
watch 机制的作用,如果没有watch 某个键,有可能造成“不可重复读”,同时最终的结果也不一致。
实现原理:存在一个watched_keys 字典,key 是监视的键,value 是监视这个键的客户端链表。WATCH
命令的作用, 就是将当前客户端和要监视的键在 watched_keys
中进行关联。
unwatch,取消 WATCH 命令对所有 keys 的监视
事务中的错误:
redis 事务不支持回滚。可以保持简单而快速;同时redis 认为redis 命令错误应该开发时被发现。
参考:https://segmentfault.com/a/1190000023951592#item-4
sortset 实现原理 & 优劣势
实现朋友圈功能
最经典的:旁路缓存模式。
或者把读写请求放到一个队列中,完全保证一致性。
redis 相关:大厂面试官喜欢这样问Redis,双写一致性、并发竞争、线程模型,我整理好了_敖丙-CSDN博客_双写一致性面了6家大厂,我把问烂了的Redis常见面试题总结了一下(带答案)_敖丙-CSDN博客
redis 总结
https://thinkwon.blog.csdn.net/article/details/103522351
持久化:数据持久化为了做数据备份
主从复制:则是部署多个副本节点,多个副本节点实时复制主节点的数据,当主节点宕机的时候,有完整的副节点可以使用。也可以实现读写分离,提高访问性能。
哨兵机制:如果主节点宕机,需要自动恢复机制。通过哨兵实现。
集群:如果读请求很大,那么主从节点可能cover 住;但是如果写请求也很大,如果只有一个主节点是无法承受的,这时候就需要集群化。就是多个主从节点构成一个集群,每个节点单存储一部分数据,这样写请求也可以分散到多个主节点上,解决写压力大的问题。同时可以动态新增节点进行扩容,提升性能。
要想实现集群化,就要部署多个主节点。同时每个主节点还会有多个从节点。(承担更大的流量、数据持久化、数据复制、故障自动恢复等功能,保证集群高性能和高可用)
集群方案:
主流reids 集群化方案,可以按照是否中心化来划分。客户端分片、redis cluster 是无中心化集群方案,codis、Twemproxy 是中心化集群方案
是否中心化是指客户端访问多个Redis节点时,是直接访问(去中心化)还是通过一个中间层Proxy(中心化方案)来进行操作。
参考:Redis集群方案对比:Codis、Twemproxy、Redis Cluster - 云+社区 - 腾讯云
Redis 的主从结构可以采用一主多从或者级联结构,Redis主从复制可以根据是否是全量分为全量同步和增量同步。
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
参考:Redis 的主从同步,及两种高可用方式_cute-CSDN博客_redis主从
自动处理的办法,两步:(1)自动监控(2)通知系统处理
有赞提过一个框架TMC,客户端监控热key,通知系统进行处理。
【原创】谈谈redis的热key问题如何解决 - 孤独烟 - 博客园
Redis中大key问题,热key问题的解决方案 - Code2020 - 博客园
哨兵的作用是监控Redis 服务器的状态,可以再master 节点下线之后,将其他slave 节点升级为master 节点。实现高可用。
多个哨兵可以监控同一个redis 节点,哨兵和哨兵之间可以相互监视。
提高redis 可用性的关键是:多副本部署 + 自动故障恢复。多副本部署依赖主从复制。
哨兵是redis 高可用的解决方案,它是一个管理多个redis 实例的服务工具,可以实现对redis 服务的监控、通知、自动故障转移。
哨兵是一个分布式系统,在一个架构中可以运行多个哨兵进程,这些进程使用流言协议(gossipprotocols)来接收关于Master是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个Slave作为新的Master.
以上来自:redis主从复制下哨兵模式---选举原理(转载)_zhaoquanwei2018的博客-CSDN博客_redis哨兵模式投票原理(这个应该看看,但是还没看完)
这个也应该看看:Redis 哨兵模式(Sentinel) 原理 - 知乎
哨兵三个任务:
监控:监控主从节点是否正常运行
选主:选出一个leader 进行主从切换
通知:选主完成后,需要把新主库的连接信息通知给从库和客户端
哨兵发现服务下线:
领导者哨兵选举流程:
服务故障处理:
故障转移选举节点选择:
过滤掉不健康的(下线或断线),没有回复过哨兵 ping 响应的从节点
选择 slave-priority 从节点优先级最高(redis.conf)
选择复制偏移量最大,指复制最完整的从节点
以上参考:面试必问,redis高可用原理,哨兵机制详解 - 知乎
哨兵的工作流程主要分为以下几个阶段:
哨兵机制概览:11.Redis哨兵面试题(高频面试题)_java程序鱼的博客-CSDN博客
(写的看来不错的文章,没时间看
Redis(五):集群:主从复制、CAP、PAXOS、cluster分片集群(一) - 知乎
Redis分布式锁是否是安全的? - 知乎
Redis主从复制、Redis哨兵模式、Redis集群 - 知乎
Redis(二)----哨兵、Twemproxy、集群 - 知乎
Redis学习总结(23)——Redis如何实现故障自动恢复?浅析哨兵的工作原理
深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了! | Kaito's Blog
Redis如何实现故障自动恢复?浅析哨兵的工作原理 | Kaito's Blog
)
根因:参考并发理论基础:并发问题产生的三大根源 - 知乎
cpu 切换导致的原子性问题
高速缓存的产生。高速缓存和cpu 每个核心绑定高速缓存,互相不可见
指令优化导致的重排序问题
列存储优势:自动索引;
四种幂等性解决方案:掘金
reenterlock 和synchronize的区别
synchronized和lock的区别
(如果有时间看,讲讲实现原理)
用来控制线程同步的,控制synchronized 代码段不被多个线程同时执行。
使用synchronized 的时候,能锁代码块就不要锁方法,尽量减少锁粒度。
synchronized 不能继承,也即父类的方法synchronized f() {} 在子类中会变成f() {}。
为什么只有static 方法加上synchronized 之后,才能实现类锁。
因为同步方法加上static
关键字后,那此方法在class创建的时候,就已经初始化好了。类中所有的实例,同步使用这个方法,锁的作用范围是最大的。
对于synchronized 只有在执行完成、抛出异常或者调用wait 方法之后,才会释放锁,其他情况只能等待。
关于“类锁”参考:Java中synchronized实现类锁的两种方式及原理解析_五道口-CSDN博客,写的很清晰。
领域驱动设计:DDD
领域驱动设计(DDD):领域接口化设计
不能声明普通外层类,或者包为静态的。
static 可用于下面四种情况。
内部类 & 静态类
内部类的实例化
- 内部类分为静态内部类、普通内部类
举例:
OuterClass.StaticInnerClass sic = new OuterClass.StaticInnerClass();
OuterClass oc = new OuterClass();
OuterClass.NormalInnerClass nic = oc.new NormalInnerClass();
静态对象 & 非静态对象 区别
1.拥有的区别:静态对象是不同类对象共同拥有的 ,而非静态对象是由类单独拥有;
2.内存空间区别:静态对象内存空间是固定,非静态类对象是附着相关类的分配;
3.分配顺序区别:先分配静态对象的空间,然后才会分配非静态对象也就是初始化。
一个问题:static 方法可以实例化外部类,但是不能实例化非静态内部类;同时实例化之后,可以调用非静态方法。
解:
第二个问题:在构造函数中使用this 关键字,但是往往可以看到在构造函数中使用this 的情况,而此时不是还没有实例化对象吗
参考:java main方法调用类的实例化方法疑问_默默-CSDN博客
static 的“继承”问题
1)子类是不继承父类的static变量和方法的。因为这是属于类本身的。但是子类是可以访问的。
2)子类和父类中同名的static变量和方法都是相互独立的,并不存在任何的重写的关系。
参考:https://blog.csdn.net/weixin_33778778/article/details/94207336
this 和super 的区别:Java中this和super的用法总结 - codersai - 博客园
java中静态方法中为什么不能使用this、super和直接调用非静态方法 - 旧巷里的旧少年 - 博客园
分布式锁的了解:1.zk。2. redis。 3. mysql
zk 的常见使用场景:
服务的注册与订阅(共用节点)。
分布式通知(监听node)。
服务命名(zonde 特性)。
数据订阅、发布(watcher)。
分布式锁(临时节点)。
zk 是个数据库,存了节点。是文件存储系统,并且有监听通知机制(观察者模式)
zk 节点有四大类。持久化、持久化顺序节点(持久化顺序编号目录节点)、临时目录节点、临时编号目录节点。节点名称都是唯一的。
zk 实现分布式锁。
假设锁空间的根节点是/lock
2.客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁
执行业务代码
完成业务流程后,删除对应的节点,释放锁
zk 提供的api 设置监听器的操作和读操作是原子执行的。这样保证在读子节点列表时同时设置监听器,保证不丢失时间。
另外的优化:假如当前有10000 个节点在等在锁,如果获得锁的客户端释放锁时,这10000 个客户端都会被唤醒,这种情况称作“羊群效应”,这种获得锁的客户端释放锁时,zk 需要通知10000 个客户端,这会阻塞其他操作,所以最好的情况应该只唤醒最小节点的客户端。所以在设置监听时,每个客户端应该对刚好在它之前的子节点设置事件监听,即需要1 的监听序号0 的
锁服务分围两类,一个是保持独占,一个是控制时序。
对于第一类,我们将zk 上的znode 看做是一个锁,所有客户端都去创建/distrbute_lock 节点,创建成功了则代表拥有了这把锁。
对于第二类,/distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选mater 一样,编号小的获得锁,用完删除。步骤:(1)在根目录下创建临时有序节点(2)获取根目录下的子目录列表,取得最小i 值(3)判断i 值是否和自己创建的一致,是则获得锁(4)如果不相等,则监听比自己小的一个节点j(4)j 节点是否存在,如果不存在,则重新获取根目录下子节点,取得最小的i 值,重复[2] 步骤。
zk 可以做到:(1)命名服务 (2)配置服务,统一配置到zk 上,然后从zk 上读配置(3)集群管理 是否有机器退出、加入,选举master(4)分布式锁(5)队列管理
并发竞争是怎么控制的。
在一个线程中可以多次获取同一把锁。比如,一个线程在执行一个带锁的方法,该方法又调用了另一个需要相同锁的方法,而该线程可以直接执行调用的方法(即可重入),而无需重新获得锁。
java 线程是基于“每线程”,而不是“每调用”的,也就是说,java 为每个线程分配一个锁,而不是每次调用分配一个锁。
可重入锁的原理:加锁时,需要判断锁是否已经被获取,如果已经被获取,则判断获取锁的线程是否是当前线程。如果是当前线程,则给获取次数加1,。如果不是当前线程,则需要等待。
ReentrantLock 和Synchronized 都是可重入锁。
mvcc 多版本控制。
布隆过滤器概念。
让线程自己独立保存一份自己的变量副本,每个线程都独立使用自己的线程副本。这样就不影响其他线程。
原理:每个Thread 中有个变量,threadLocalMap,其实是一个map 的结构。
key 是个Reference,是个弱引用,key 是ThreadLocal 类的实例对象,value 就是传进去的那个值。(防止gc 回收的时候无法回收)
ThreadLocal为啥要用弱引用?不知道 - 知乎
JAVA并发(3)—线程运行时发生GC,会回收ThreadLocal弱引用的key吗? - 简书
彻底搞清楚ThreadLocal与弱引用 - 知乎
有个疑问,使用弱引用,如果在线程运行的过程中,
有空可以看看这个视频:【真实工作场景】中怎么用ThreadLocal_哔哩哔哩_bilibili,没空这条不用看了。
ThreadLocal 应该实现“对于单个线程,本地化变量”,实现线程隔离。
Thread 对象中有个map,用来保存本地变量。
Thread 对象里面会有个 map,用来保存本地变量。
这个map 是ThreadLocal.ThreadLocalMap,通过开放寻址法来解决冲突。
(找不到往后移动一个位置)
ThreadLocal 为什么要用弱引用
看这个,复习一遍
https://mp.weixin.qq.com/s/76lIW4ia8D90Htoyg2NJpw
threadLocal 导致内存泄漏的原因
因为内部threadLocalMap 的声明周期和thread 一致。如果是线程池,thread 一直没被销毁,则发生泄漏。
参考:https://segmentfault.com/a/1190000022704085
java 中四种引用,强引用、软引用、弱引用、虚引用。 (不是重点)
参考视频:java四种类型引用,强引用,软引用,弱引用,虚引用_哔哩哔哩_bilibili
强引用,最常见的,比如说直接创建一个对象,Object obj = new Object();。obj 就是一个强引用,在当前栈帧有效的作用于中,永远不会被回收。
软引用,SoftReference 包装。在内存有足够的空间的时候,能存活。系统内存不足的时候,垃圾回收动作到来时,它会被回收释放内存。(比如说用来当做缓存)
SoftReference
弱引用,WeakReference 包装。能活到下一次gc 之前,如果进行gc,那么一定会被回收。
WeakReference
可以当做缓存使用。
ThreadLocal 中使用,减少内存泄漏的概率。
虚引用,被phantomReference 类包装。用来跟踪对象引用被加入到队列的时刻。无法通过虚引用获取到一个对象的实例。只能搭配队列使用。
使用场景用来释放资源。用来做消息通知,释放自己。
这个示例写得好:Java中的强引用、软引用、弱引用和虚引用及其实例_爵士陈的博客-CSDN博客_软引用举例
dubbo 内部 内部使用netty、zookeeper
BIO
同步阻塞的通信模式,即典型的请求——应答通信模型。 其服务端,通常由一个独立的Acceptor 线程负责监听客户端的链接,每个client 请求,创建一个线程处理请求,完成后,销毁。
NIO,non-blocking IO,也叫New IO
同步非阻塞的通信模式,客户端和服务器通过channel 通信,NIO 可以在channel 进行读写操作,这些channel 都会被注册到Selector 多路复用器上。Selector 通过一个线程不停地轮询这些Channel,找出已经准备就绪的Channel 进行操作。
即nio 通过轮询,对轮询到的事件进行处理,不需要为每个连接单独开线程处理。
nio 的三大核心为selector(选择器)、buffer(缓冲区)、channel(通道)
缓冲区buffer: 是nio 和bio 的一个重要区别。bio 是将数据直接写入或读取到Stream 对象中,而NIO 的数据操作都是在缓冲区中进行的。 通道channel: 和流不同,channel 是双向的。分为两类:一个是网络读写(SelectableChannel),一个是用于文件操作的(FileChannel)。 选择器Selector: 是NIO 编程的基础,非常重要,提供选择已经就绪的任务的能力。netty 是基于java NIO 的网络应用框架,使用netty 可以快速开发网络应用,例如服务器和客户端的协议。
io 多路复用
NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
{
ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池
ServerSocket serverSocket = new ServerSocket();
serverSocket.bind(8088);
// 如果当前主线程没有被打断
while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来
// 主线程一直被阻塞到这里,等待socket 的accept 事件
Socket socket = serverSocket.accept();
// 如果获取到accept 事件之后,则创建新线程,去执行操作。socket 作为参数
executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程
}
class ConnectIOnHandler extends Thread{
private Socket socket;
public ConnectIOnHandler(Socket socket){
this.socket = socket;
}
public void run(){
// 线程一直在这里进行死循环,处理读写事件
while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件
String someThing = socket.read()....//读取数据
if(someThing!=null){
......//处理数据
socket.write()....//写数据
}
}
}
}
这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。
其实这也是所有使用多线程的本质:
(即I/O 密集型任务,用多线程)
现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很"贵"的资源,主要表现在:
所以,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型。
Java NIO浅析 - 知乎
Socket网络编程 - 知乎
从jvm的角度来看java的多线程 - 叫我家宝 - 博客园
volatile 只能保证可见性、有序性。
保证可见性:jmm 内存模型,通过缓存一致性协议,总线嗅探机制。
volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。
保证有序性:指令重排序是通过分析预习判断指令的依赖关系。而多线程的环境下,比如说方法1 操作两个变量,方法二根据这两个变量做操作,其中有个判断。然后两个方法在不同的线程中执行。对于方法1,指令进行了重排序,就会导致方法2 出问题。
以上参考:并发环境下指令重排带来的问题 - 简书
如何保证有序性。直接参考下面链接。
参考:知道这些,面试时volatile就稳了 - 知乎
使用volatile 的场景
应用于大量并发读,少量并发写的场景。
不应该使用的场景:
1. 对变量的写操作不依赖于当前值。理解就是,当你需要改变这个变量时,要保证你要改变的值跟这个变量原先的值没有任何关系,比如:count = 10; 而不是count++;
2. 该变量没有包含在具有其他变量的不变式中.
可以使用的场景:
参考:volatile适用场景 - 知乎
volatile 和synchronized 的区别:https://segmentfault.com/a/1190000023907308
Java并发编程:volatile关键字解析 - Matrix海子 - 博客园
掘金(这个链接可以的)
单例模式,确保一个类在任何情况下,都绝对只有一个实例,并提供一个全局访问点。隐藏其构造方法,属于创建型模式。
使用场景:ServletContext、ServletConfig、ApplicationContext、DBPool
单例模式的实现:
如何破坏:通过反射,拿到class 类对象,设置setAccessible(true),破坏单例。
解决方案:
答案:通过枚举类。(参考:Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式?【享学Java】 - 云+社区 - 腾讯云)
参考:5分钟学会利用反射破坏单例模式解决方案_猿医ˉ生-CSDN博客
枚举介绍,原理,各种单例介绍,可以看这个:掘金
懒汉式和饿汉式哪个是线程安全?
DCL 设计模式 spring 中用到的,代理、单例、
以上:java——Final修饰成员变量的注意事项_西城风雨楼-CSDN博客_final修饰成员变量
使用例子查看:Java中的final变量、final方法和final类_pan_junbiao的博客-CSDN博客_final变量
字节和字符的区别:字节(byte)是计量单位,表示数据量的多少,是计算机用于计量存储容量的一种计量单位,通常情况一字节等于8 位(bit)。字符(character)是计算机中使用的字母、数字、汉字、符号等,如'A','B','#' 等。一般英文一个字母或字符占用一个字节,一个汉字用两个字节表示。
序列化的作用就是为了不同jvm 之间共享实例对象的一种解决方案。由java 提供此机制。
反序列化是利用反射实现的(jdk1.8源码,readObject-> readObject0 -> readOrdinaryObject -> invokeReadResolve ->readResolveMethod.invoke)
序列化基本概念介绍:掘金
序列化:将对象写入到IO 流中
序列化机制允许将实现了序列化接口的java 对象转换为字节序列,这些字节序列可以保存在磁盘上,或者通过网络传输。序列化机制使得对象可以脱离程序的运行而独立存在。
所有可在网络上传输的对象都必须是可序列化的,比如RMI(remote method interface,远程方法调用),传入的参数和返回的对象都是可序列化的,否则会出错。需要保存到磁盘的java 对象也是。
反序列化的顺序与序列化时的顺序一致。
反序列化不会调用构造方法,反序列化对象是由JVM 自己生成的对象,不通过构造方法。
Java序列化同一对象,并不会将此对象序列化多次得到多个对象。
意思是比如一个Teacher t1 = new Teacher("雷利"); oos.writeObject(t1); 一次,然后再oos.writeObject(t1); 那么序列化此对象只会序列化一次。反序列化得到的也是同一个对象。
原因:所有保存到磁盘的对象都有一个序列化编号,当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。
java 序列化算法:
实现序列化有两个接口
存在的问题:如果序列化一个可变对象,在第一次序列化结束之后,如果改变了对象的值,再次序列化此对象,并不会再将此对象转为字节序列,而只是保存序列化编号。
两种序列化对比
实现Serializable接口 | 实现Externalizable接口 |
---|---|
系统自动存储必要的信息 | 程序员决定存储哪些信息 |
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 同时必须提供无参构造方法,因为在反序列化的时候需要反射创建对象。 |
性能略差 | 性能略好 |
序列化版本号
反序列化必须有class 文件,但是随着项目的升级,class 文件也会升级,序列化咱们保证升级前后的兼容性?
java 序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以被反序列化回来。
如果版本号不一致,反序列化会报InvalidClassException 异常
是否需要修改serialVersionUID 的情况
序列化对象时,如果不显示的设置serialVersionUID
,Java在序列化时会根据对象属性自动的生成一个serialVersionUID
,再进行存储或用作网络传输。
单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
写的时候,先调用writeResolve(),再调用writeObject() 方法
读的时候,先调用readObject() 方法,再调用readResolve() 方法
在readResolve() 中直接返回单例的对象就可以了
以上参考:掘金
几种常用的序列化 & 反序列化协议
关于transient 的使用小结
1)一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
2)transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量(static)是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。
3)被transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化
序列化原理:掘金
为什么重写writeObject() 方法可以重写,因为在源码里个检查的步骤
一些问题:2020 最新Java序列化面试题_duchaochen的博客-CSDN博客_java序列化面试题
流的概念:https://segmentfault.com/a/1190000004103031(有时间了解即可)
参考:websocket和socket的区别是什么-常见问题-PHP中文网
socket 是TCP/IP 网络的API,是为了方便使用TCP 或UDP 而抽象出来的一层,位于应用层和传输控制层之间的一组接口;而webSocket 则是一个典型的应用层协议。
等有空看看这个,介绍socket 的:Socket通信协议解析(文章摘要) - 知乎
参考:HTTP长连接、短连接究竟是什么? - 华为云
http 协议的场长连接和短连接本质上是TCP 长连接和短连接
http 属于应用层协议,在传输层使用TCP 协议,网络层使用IP 协议。
IP 协议解决网络路由和寻址问题,TCP 解决IP 层之上可靠传输的问题。TCP 是可靠的,面向连接的。
HTTP 1.0 默认短连接,1.1 开始默认使用长连接。而http 的长连接。
使用长连接的http 协议,会在响应头加入“Connection:keep-alive”
TCP的保活功能主要为服务器应用提供。
长连接多用于操作频繁,点对点的通讯,且连接数不能太多的情况。例如:数据库的连接使用长连接。像web 网站的http 服务一般使用短连接,长连接对于服务端来说会耗费一定资源,而且用户成千上万,所以并发量大,但每个用户无需频繁操作情况下用短连接好。
[http 1.0] get post head
[http 1.1] options put delete trace connect
听我讲完 GET、POST 原理,面试官给我倒了杯卡布奇诺
七层网络模型
DDD 业务领域模型
“就诊”作为出发点,医生、药师作为过程中遇到的角色,“就诊”有明确的目标就是治病,整个过程需要一些动作才能达到这个目标。
总结:出发点(问题)、遇到的角色、目标(达到的结果)、中间的动作
DDD 有两类数据:基础数据(偏内向)、业务数据(偏外向)。业务数据让基础数据彼此关联起来。建模前,分清基础数据、业务数据
java 切换上下文
jvm 内存模型,垃圾回收机制,几种算法,几种回收机制
java创建对象的过程详解,从内存角度
java创建对象的过程详解(从内存角度分析) - 知乎
类加载的过程
有时间可以看看这个:https://www.zhihu.com/question/296949412/answer/2020805404
这个写的不错:JAVA---枚举类、类的加载 - 简书
在并发标记阶段的时候,因为用户线程与GC线程同时运行,有可能会产生多标或者漏标;
标记的作用是,把对象标记为“不是垃圾”
多标--多标记(浮动垃圾)(应该是垃圾的被标位不是垃圾)
漏标--漏标记(不应该是垃圾的,没有被标出来)
漏标问题大。
漏标发生的两个条件:
1.至少有一个黑色对象指向了白色对象
2.所有灰色对象扫描完整个链时,删除之前所有白色对象。
cms 解决漏标的问题:写屏障 + 增量更新
当黑色对象指向白色对象的时候,记下这个黑色对象。
重新标记阶段,将黑色对象改为灰色对象,重新扫描这条路
G1 解决漏标问题:原始快照的方式
在C断开E的时候,会记录原始快照,在重新标记阶段的时候以白色对象变为灰色为起始点扫描整个链,本次GC是不会被清理。
参考:CMS和G1的漏标问题解决及三色标记算法图解 - 简书
Spring 框架中使用到了大量的设计模式,下面列举了比较有代表性的:
代理模式—在 AOP 和 remoting 中被用的比较多。
单例模式—在 spring 配置文件中定义的 bean 默认为单例模式。
前端控制器—Spring 提供了 DispatcherServlet 来对请求进行分发。
Spring 的 ApplicationContext 创建的 Bean 实例都是单例对象,还有 ServletContext、数据库连接池等也都是单例模式。
Spring Framework 中,是使用@EnableAspectJAutoProxy
注解来开启 Spring AOP 相关功能的。
哪两种
kafka 幂等性原理:Kafka幂等性原理及实现剖析 - 哥不是小萝莉 - 博客园
解决:引入了ProducerID和SequenceNumber。
每个producer 都有个producerId,每个消息都有个sequenceNumber。
二者组合,实现幂等。
比如:原子性的读取消息,立即处理和发送,如果中途出现错误,支持回滚操作。
从三个方面说明
Kafka学习之Kafka选举机制简述 - kosamino - 博客园
kafka 控制器介绍:
Kafka实战宝典:Kafka的控制器controller详解 - WindyQin - 博客园
session cookie socket 的这些东西
幻读 不可重复度 脏读
读未提交 1 1 1
读已提交 1 1 0
可重复读 1 0 0
串行化 0 0 0
mysql 默认可重复读
orcale 默认可重复读
是指一组固定常量组成合法值的类型。
枚举类构造方法私有,不允许创建对象
类和枚举的关系:Java 枚举与类的区别_lishuangling21的博客-CSDN博客_java枚举与类的区别
四次挥手,最后为什么要等待2msl,这个要看一下
2MSL(最大报文段生存时间)。
三次挥手:主动方发送fin + ack,被动方发送fin,主动方发送ack
问题:tcp 是全双工通信(可以双向同时通信)。
(1)主动方发送fin 之后,仅仅代表主动方不会发送数据报文,但是主动方仍可以接受、被动方仍可以发送
(2)被动方可能还有报文要发送,所以先发送ack,代表我收到了,知道你要关了,这样主动方不会因为没有收到应答而继续发送断开的连接请求(即fin)
(3)被动方处理完数据之后,便发送给主动方fin 报文,这样保证数据通信正常可靠地完成。发送完fin 报文之后,被动房进入last_ack 阶段
(4)如果主动方即使发送ack 报文进行连接中断确认,这时被动方就直接释放连接,进入可用状态。
而正常主动方发送完最后的ack 消息之后,就可以进入close 状态了。但是主动方会等待2msl 之后才会进入colse 状态。因为要假设网络是不稳定的,如果主动方发送了ack 消息,被动方没有收到,那么服务器就会一直发送fin 报文。
所以客户端不能立刻关闭,它必须确认服务端收到了ack。
客户端会在发送出ACK之后进入到TIME_WAIT状态,同时设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么客户端会重发ACK并再次等待2MSL。
所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,客户端都没有再次收到FIN,那么客户端推断ACK已经被成功接收,则结束TCP连接。
发送端首先发送一个带SYN 标志的数据包给接收方
接收端收到后,回传一个带有SYN/ACK 标志的数据包给接收方。SYN 是为了告诉发送端,发送方到接收方的通道没问题。ack 是为了验证接收方到发送方的通道。
最后,发送端再传回一个带ack 标志的数据包,代表握手结束
主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
经典场景:客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了
由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。
此时此前滞留的那一次请求连接,网络通畅了到达服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接
服务端关闭了连接,客户端确认为链接已建立。发送数据的时候,服务端收到,会发送RST 包,代表异常关闭链接,此时客户端知道链接建立失败。
当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:
Java SPI思想梳理 - 知乎
spi 对双亲委派模型的破坏介绍:聊聊SPI机制以及为什么说SPI破坏了双亲委派模型_womeiyouzaihuashui的博客-CSDN博客
树的数据结构(参考:[Data Structure] 数据结构中各种树 - Poll的笔记 - 博客园)
参考:数据结构里各种难啃的“树”,一文搞懂它 - 知乎
红黑树和B+ 树的区别和应用场景:红黑树 与 B+树区别和应用场景 - 知乎
算法,全排列, 一个字符串集合,能不能通过单词集合拼成字符串 排序算法
找两个比较好的项目。亮点,用了什么技术。 业务背景,怎么设计的,这种。
分布式id 生成方案:
分布式全局ID生成方案 - JaJian - 博客园
分布式事务:
分布式事务,这一篇就够了 | 小米信息部技术团队
两阶段提交 三阶段提交
为什么需要 Zookeeper - 知乎 为什么需要zookeeper
cap 理论:谈谈分布式系统的CAP理论 - 知乎
其他:
介绍经验:如何在面试中介绍自己的项目经验 - hsm_computer - 博客园
mysql 索引:like百分号加前面一定不走索引吗?一不小心就翻车,关于mysql索引那些容易错的点 - 知乎
面题汇总:腾讯、阿里、滴滴后台面试题汇总总结 — (含答案)_温不了情的博客-CSDN博客_腾讯笔试题库及答案
hbase :HBase经典面试常问问题汇总_石榴姐yyds-CSDN博客_hbase的面试题
新问题:
跳表为什么使用了两种数据结构
java8 的metaspace 零拷贝 伪共享 mesi 哈希一致性
分布式事务算法 有点绕