首页 >面试技巧 > 内容

Java面试知识点概览(持续更新)

2022年12月29日 23:03

Java

基础

说说自定义注解的场景及实现

利用自定义注解,结合SpringAOP可以完成权限控制、日志记录、统一异常处理、数字签名、数据加解密等功能。
实现场景(API接口数据加解密)
1)自定义一个注解,在需要加解密的方法上添加该注解
2)配置SringAOP环绕通知
3)截获方法入参并进行解密
4)截获方法返回值并进行加密

说一下泛型原理,并举例说明

==泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。===Java 泛型是在 Java1.5 以后出现的,为保持对以前版本的兼容,使用了擦除的方法实现泛型。擦除是指在一定程度无视类型参数 T,直接从 T 所在的类开始向上 T 的父类去擦除,如调用泛型方法, 传入类型参数 T 进入方法内部,若没在声明时做类似 public T methodName(T extends Father t){},Java 就进行了向上类型的擦除,直接把参数 t 当做 Object 类来处理,而不是传进去的 T。即在有泛型的任何类和方法内部,它都无法知道自己的泛型参数,擦除和转型都是在边界上发生,即传进去的参在进入类或方法时被擦除掉,但传出来的时候又被转成了我们设置的 T。在泛型类或方法内,任何涉及到具体类型(即擦除后的类型的子类)操作都不能进行,如 new T(),或者 T.play()(play 为某子类的方法而不是擦除后的类的方法)

说说你对 Java 注解的理解

注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@

public @interface TestAnnotation {}

使用时@TestAnnotation 来引用,要使注解能正常工作,还需要使用元注解,它是可以注解到注解上的注解。元标签有@Retention @Documented @Target @Inherited@Repeatable 五种

@Retention 说明注解的存活时间,取值有 RetentionPolicy.SOURCE 注解只在源码阶段保留, 在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

@Documented 注解中的元素包含到 javadoc 中去

@Target 限 定 注 解 的 应 用 场 景 , ElementType.FIELD 给 属 性 进 行 注解 ; ElementType.LOCAL_VARIABLE 可以给局部变量进行注解;ElementType.METHOD可以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解 ElementType.TYPE可以给一个类型进行注解,如类、接口、枚举

@Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应用的话,该子类就可继承超类的注解;

注解的作用:
1)提供信息给编译器:编译器可利用注解来探测错误和警告信息
2)编译阶段:软件工具可以利用注解信息来生成代码、html 文档或做其它相应处理;
3)运行阶段:程序运行时可利用注解提取代码
注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了某个注解,再通过 getAnnotation()方法获取 Annotation 对象

谈谈你对解析与分派的认识?

解析指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。

分派可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在 重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的 静态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。

讲一下常见编码方式?

编码的意义:计算机中存储的最小单元是一个字节即 8bit,所能表示的字符范围是 255个, 而人类要表示的符号太多,无法用一个字节来完全表示,固需要将符号编码,将各种语言翻译成计算机能懂的语言。
1)ASCII 码:总共 128 个,用一个字节的低 7 位表示,0〜31 控制字符如换回车删除等;32~126 是打印字符,可通过键盘输入并显示出来;
2)ISO-8859-1,用来扩展 ASCII 编码,256 个字符,涵盖了大多数西欧语言字符。
3)GB2312:双字节编码,总编码范围是 A1-A7,A1-A9 是符号区,包含 682 个字符,B0-B7 是汉字区,包含 6763 个汉字;
4)GBK 为了扩展 GB2312,加入了更多的汉字,编码范围是 8140~FEFE,有 23940 个码位,能表示 21003 个汉字。
5)UTF-16: ISO 试图想创建一个全新的超语言字典,世界上所有语言都可通过这本字典Unicode 来相互翻译,而 UTF-16 定义了 Unicode 字符在计算机中存取方法,用两个字节来表示 Unicode 转化格式。不论什么字符都可用两字节表示,即 16bit,固叫 UTF-16。
6)UTF-8:UTF-16 统一采用两字节表示一个字符,但有些字符只用一个字节就可表示,浪费存储空间,而 UTF-8 采用一种变长技术,每个编码区域有不同的字码长度。 不同类型的字符可以由1~6个字节组成。

utf-8 编码中的中文占几个字节;int 型几个字节?

utf-8 是一种变长编码技术,utf-8 编码中的中文占用的字节不确定,可能 2 个、3 个、4个, int 型占 4 个字节。

二叉搜索树和平衡二叉树有什么关系,强平衡二叉树(AVL 树)和弱平衡二叉树(红黑树)有什么区别?

二叉搜索树:也称二叉查找树,或二叉排序树。定义也比较简单,要么是一颗空 树,要么就是具有如下性质的二叉树:
(1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)任意节点的左、右子树也分别为二叉查找树;
(4)没有键值相等的节点。

平衡二叉树:在二叉搜索树的基础上多了两个重要的特点
(1)左右两子树的高度差的绝对值不能超过 1;
(2)左右两子树也是一颗平衡二叉树。

红黑书:红黑树是在普通二叉树上,对每个节点添加一个颜色属性形成的,需要同时满足
一下五条性质
(1)节点是红色或者是黑色;
(2)根节点是黑色;
(3)每个叶节点(NIL 或空节点)是黑色;
(4)每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点);
(5)从任一节点到其没个叶节点的所有路径都包含相同数目的黑色节点。
区别:AVL 树需要保持平衡,但它的旋转太耗时,而红黑树就是一个没有 AVL 树那样平衡,因此插入、删除效率会高于 AVL 树,而 AVL 树的查找效率显然高于红黑树。

树和 B+树的区别,为什么 MySQL 要使用 B+树 B 树?

(1)关键字集合分布在整颗树中;
(2)任何一个关键字出现且只出现在一个结点中;
(3)搜索有可能在非叶子结点结束;
(4)其搜索性能等价于在关键字全集内做一次二分查找;

B+树:
(1)有 n 棵子树的非叶子结点中含有 n 个关键字(b 树是 n-1 个),这些关键字不保存数据,只用来索引,所有数据都保存在叶子节点(b 树是每个关键字都保存数据);
(2)所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针, 且叶子结点本身依关键字的大小自小而大顺序链接;
(3)所有的非叶子结点可以看成是索引部分,结点中仅含其子树中的最大(或最小) 关键字;
(4)通常在 b+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点;
(5)同一个数字会在不同节点中重复出现,根节点的最大元素就是 b+树的最大元素。

B+树相比于 B 树的查询优势:
(1)B+树的中间节点不保存数据,所以磁盘页能容纳更多节点元素,更“矮胖”;
(2)B+树查询必须查找到叶子节点,B 树只要匹配到即可不用管元素位置,因此 B+树查找更稳定(并不慢);
(3)对于范围查找来说,B+树只需遍历叶子节点链表即可,B 树却需要重复地中序遍历

说说 B-tree 、 B+tree 的区别和使场景?

B-tree:
B-tree 利用了磁盘块的特性进行构建的树。每个磁盘块⼀个节点,每个节点包含了很关键字。把树的节点关键字增多后树的 层级比原来的⼆叉树少了,减少数据查找的次数和复杂度。
B-tree 巧妙利用了磁盘预读原理,将⼀个节点的大小设为等于⼀个页(每⻚为 4K),这样每个节点只需要⼀次 I/O 就可以完全载入。
B-tree 的数据可以存在任何节点中。

B+tree:
B+tree 是 B-tree 的变种,B+tree 数据只存储在叶⼦节点中。这样在 B 树的基础上每个节点存储的关键字数更多,树的层级更少所以查询数据更快,所有指关键字指针都存在叶子节点,所以每次查找的次数都相同所以查询速度更稳定;

数组在内存中如何分配?

静态初始化∶初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如∶
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为 4
String[] computers = f"Del1",“Lenovo”,“Apple”,“Acer”};//
//只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为 3
String[] names = new String[]{“多啦 A梦”,“大雄”,“静香”}; //

动态初始化∶初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值,如∶
//只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组
元素分配初始值为 nul1
String[] cars = new String[4]; //

静态初始化方式,程序员虽然没有指定数组长度 ,但是系统已经自动帮我们给分配了 ,而动态初始化方式,程序员虽然没有显示的指定初始化值,但是因为 Java 数组是引用类型的变量,所以系统也为每个元素分配了初始化值 nu11,当然不同类型的初始化值也是不一样的,假设是基本类型 int 类型,那么为系统分配的初始化值也是对应的默认值 0。

Cloneable 接口实现原理

Cloneable 接口是 Java 开发中常用的一个接口,它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中,注意,这里所说的"拷贝"拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。

在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要具备拷贝功能的类实现 cloneable接口,并重写 clone()方法,就可以通过调用 clone()方法的方式简洁地实现实例拷贝功能

深拷贝(深复制)和浅拷贝(浅复制)是两个比较通用的概念,尤其在 C++语言中,若不弄懂,则会在 delete 的时候出问题,但是我们在这幸好用的是 Java。虽然 ava 自动管理对象的回收,但对于深拷贝(深复制)和浅拷贝(浅复制),我们还是要给予足够的重视,因为有时这两个概念往往会给我们带来不小的困惑。

浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象。举例来说更加清楚∶对象 A1 中包含对 B1 的引用,B1 中包含对 c1 的引用。浅拷贝A1 得到 A2 ,A2 中依然包含对 B1 的引用,B1 中依然包含对 c1 的引用。深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2,A2 中包含对 B2( B1 的 copy )的引用,B2 中包含对 C2( C1 的 copy )的引用。若不对 clone()方法进行改写,则调用此方法得到的对象即为浅拷贝

Java 反射机制

Java 反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法,对于任意个对象都能够调用它的任意一个属性和方法。这种在运行时动态的获取信息以及动态调用对象的方法的功能称为 Java 的反射机制。

Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field,Method,Constructor 类 (每个类都实现了 Member 接口)。这些类型的对象时由 JVM在运行时创建的,用以表示未知类里对应的成员。
这样你就可以使用 Constructor 创建新的对象,用 get()和 set()方法读取和修改与Field 对象关联的字段,用 invoke()方法调用与 Method 对象关联的方法。另外,还可以调用 getFields() getMethods()和 getConstructors()等很便利的方法,以返回表示字段,方法,以及构造器的对象的数组。这样匿名对象的信息就能在运行时被完全确定下来,而在编译时不需要知道任何事情。

import java.lang.reflect.Constructor; public class ReflectTest {public static void main(String[] args) throws Exception {Class clazz = null;clazz = Class.forName("com.jas.reflect.Fruit");Constructor<Fruit> constructor1 = clazz.getConstructor();Constructor<Fruit> constructor2 =clazz.getConstructor(String.class);Fruit fruit1 = constructor1.newInstance();Fruit fruit2 = constructor2.newInstance("Apple");}}class Fruit{public Fruit(){System.out.println("无参构造器 Run");}public Fruit(String type){System.out.println("有参构造器 Run..........." + type);}}

– 运行结果: 无参构造器 Run………… 有参构造器 Run… Apple

Arrays.sort 和 Collections.sort 实现原理和区别

java.uti1.collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。 java.uti1.collections 是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全等操作。 然后还有混排(Shuffling)、反转(Reverse)、替换所有的元素(fil)、拷贝(copy)、返回 Collections 中最小元素(min)、返回Collections 中最大元素(max)、返回指定源列表中最后一次出现指定目标列表的起始位置( 1astIndexofsubList )、返回指定源列表中第一次出现指定目标列表的起始位置( IndexofSubList )、根据指定的距离循环移动指定列表中的元素(Rotate);事实上 Collections.sort 方法底层就是调用的 array.sort 方法

public static void sort(Object[] a) {if (LegacyMergeSort.userRequested) legacyMergeSort(a);elseComparableTimSort.sort(a, 0, a.length, null, 0, 0);}//void java.util.ComparableTimSort.sort()static void sort(Object[] a, int lo, int hi, Object[] work, intworkBase, int workLen){assert a != null && lo >= 0 && lo <= hi && hi <= a.length; intnRemaining= hi - lo;if (nRemaining < 2)return;// Arrays of size 0 and 1 are always sorted // If array is small, do a "mini-TimSort" with no mergesif (nRemaining < MIN_MERGE) {int initRunLen = countRunAndMakeAscending(a, lo, hi);binarySort(a, lo, hi, lo + initRunLen);return;}}

legacyMergeSort (a)∶归并排序 ComparableTimsort.sortO∶
Timsort 排序:Timsort 排序是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序
算法 Timsort 的核心过程
TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个 run。针对这些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run 合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的run,这时将栈上剩余的 run 合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。
综上述过程,Timsort 算法的过程包括
(0)如何数组长度小于某个值,直接用二分插入排序算法
(1)找到各个 run,并入栈
(2)按规则合并 run

Java 获取反射的三种方法

1.通过 new 对象实现反射机制
2.通过路径实现反射机制
3.通过类名实现反射机制

public class Student {private int id; String name;protected boolean sex;public float score;}public class Get {//获取反射机制三种方式public static void main(String[] args) throws ClassNotFoundException{//方式一(通过建立对象)Student stu = new Student(); Class classobj1 = stu.getClass();System.out.println(classobj1.getName());//方式二(所在通过路径-相对路径)Class classobj2 = Class.forName("fanshe.Student");System.out.println(classobj2.getName());//方式三(通过类名)Class classobj3 = Student.class;System.out.println(classobj3.getName());}}

对象的四种引用

强引用只要引用存在,垃圾回收器永远不会回收
Object obj = new Object();.
User user=new User();
可直接通过 obj 取得对应的对象如 obj.eque1s(new objectO);而这样 obj 对象对后面new object 的一个强引用,只有当 obj 这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。

软引用 非必须引用,内存溢出之前进行回收,可以通过以下代码实现
Object obj = new Object();
SoftReference<0bject> sf = new SoftReference(obj);obj= null;
sf.get();//有时候会返回 nul1
这时候 sf 是对 obj 的一个软引用,通过 sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回 nul;软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

弱引用第二次垃圾回收时回收,可以通过如下代码实现
Object obj = new Object();
weakReference<0bject> wf = new weakReference(obj);obj = nu11;
wf.get();//有时候会返回 nu11
wf.isEnQueued ();//返回是否被垃圾回收器标记为即将回收的垃圾
弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,,可以取到,当执行过第二次垃圾回收时,将返回 null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的 isEnQueued 方法返回对象是否被垃圾回收器标记。

虚引用 垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现
0bject obj = new Object();
PhantomReference<0bject> pf = new
PhantomReference<0bject>(obj);obj=null;
pf.get();//永远返回 nu11
pf.isEnQueuedO);//返回是否从内存中已经删除
虚引用是每次垃圾回收的时候都会被回收,通过虚引用的 get 方法永远获取到的数据为null,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。

final finally finalize

  • final 可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
  • finally 一般作用在 try-catch 代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 finally 代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize 是一个方法,属于 Obiect 类的一个方法,而 Object 类是所有类的父类,该方法—般由垃圾回收器来调用,当我们调用 System.gc(方法的时候,由垃圾回收器调用finalize(),回收垃圾,—个对象是否可回收的最后判断。

transient 修饰的变量是临时变量吗?

对。
一旦变量被 transient 修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。
transient 关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被 transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现 SERIALIZABLE 接口。被 transient 关键字修饰的变量不再能被序列化,一个静态变量不管是否被 transient 修饰,均不能被序列化。
注意点:在 Java 中,对象的序列化可以通过实现两种接口来实现,若实现的是SERIALIZABLE 接口,则所有的序列化将会自动进行,若实现的是 Externalizable 接口,则没有任何东西可以自动序列化,需要在 writeExternal 方法中进行手工指定所要序列化的变量,这与是否被 transient 修饰无关。

高、中、低三级调度。

高级调度:即作业调度,按照一定策略将选择磁盘上的程序装入内存,并建立进程。(存在与多道批处理系统中)
中级调度:即交换调度,按照一定策略在内外存之间进行数据交换。
低级调度:即 CPU 调度(进程调度),按照一定策略选择就绪进程,占用 cpu 执行。
其中低度调度是必须的。
下面那个查看 80 端口是否被占用?
方式一:ps -ef |grep 80
方式二:netstat -anp |grep :80
方式三:lsof -i:80
方式四:netstat -tunlp |grep :80
方式五:netstat -an |grep :80

索引可以将随机 IO 变成顺序 IO 吗?

对。

随机 IO:假设我们所需要的数据是随机分散在磁盘的不同页的不同扇区中的,那么找到相应的数据需要等到磁臂(寻址作用)旋转到指定的页,然后盘片寻找到对应的扇区,才能找到我们所需要的一块数据,依次进行此过程直到找完所有数据,这个就是随机 IO,读取数据速度较慢。

顺序 IO:假设我们已经找到了第一块数据,并且其他所需的数据就在这一块数据后边,那么就不需要重新寻址,可以依次拿到我们所需的数据,这个就叫顺序 IO。

能在 try{}catch(){}finally{}结构的 finally{}中再次抛出异常吗?

能。在 finally 中抛异常或者 return 会掩盖之前的异常

int a=10 是原子操作吗?

是的。
注意:
i++(或++i)是非原子操作,i++是一个多步操作,而且是可以被中断的。i++可以被分割成3 步,第一步读取 i 的值,第二步计算 i+1;第三部将最终值赋值给 i。

int a = b;不是原子操作。从语法的级别来看,这是也是一条语句,是原子的;但是从实际执行的二进制指令来看,由于现代计算机 CPU 架构体系的限制,数据不可以直接从内存搬运到另外一块内存,必须借助寄存器中断,这条语句一般对应两条
计算机指令,即将变量 b 的值搬运到某个寄存器(如 eax)中,再从该寄存器搬运到变量 a 的内存地址:
mov eax, dword ptr [b]
mov dword ptr [a], eax
既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行
完毕后被剥夺 CPU 时间片,切换到另外一个线程而产生不确定的情况。

类与对象的关系?

类是对象的抽象,对象时类的具体,类是对象的模板,对象是类的实例

Super与this表示什么?

Super表示当前类的父类对象
This表示当前类的对象

Collections和Collection有什么区别?

java.utilCollection是一个集合接口(集合类的一个顶级接口),它提供了对集合对象进行基本操作的通用接口方法,Collection接口在java类型中有很多具体的实现,Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接集成接口有List和Set

Collctions则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序,搜索以及线程安全等各种操作

在Queue中poll和remove有什么区别?

相同点:都是返回第一个元素,并在队列中删除返回的对象
不同点:如果没有元素poll会返回null,而remove会直接抛出NoSuchElementException异常

什么是隐式转换,什么是显式转换?

显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据,隐式转换就是大范围的变量能够接收小范围的数据,隐式转换和显式转换其实就是自动类型转换和强制类型转换

java中有没有指针?

有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针

java是值传递还是引用传递?

理论上来说,java都是引用传递,对于基本数据类型,传递是值的副本,而不是值本身,对于对象类型,传递是对象的引用,当在一个方法操作参数的时候,其实操作的是引用所指向的对象

假设吧实例化的数组的变量当成方法参数,当方法执行的时候改变了数组内的元素,那么在方法外,数组元素有发生改变吗?

改变了,因为传递是对象的引用,操作的是引用所指向的对象

throw与throws区别

  1. throws:用来声明一个方法可能产生的所有异常,不做任何处理而是将异常往上传,谁调用我我就抛给睡
  • 用在方法声明后面,跟的是异常类名
  • 可以跟多个异常类名,用逗号隔开
  • 表示抛出异常,由该方法的调用者来处理
  • throws表示出现异常的一种可能性,并不一定会发生这些异常
  1. throw:则是用来抛出一个具体的异常类型
  • 用在方法体内,跟的是异常对象名
  • 只能抛出一个异常对象名
  • 表示抛出异常,由方法体内的语句处理
  • throw则是抛出了异常,执行throw则一定抛出了某种异常

什么是Class文件?Class文件主要的信息结构有哪些?

Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按顺序排列
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这样的伪结构仅仅有两种数据类型:无符号数和表
无符号数:是基本数据类型,以U1,U2,U4,U8分别代表一个字节,两个字节,四个字节,八个字节的无符号数,能够用来描写叙述数字,索引引用,数量值或者依照UTF-8编码构成的字符串值
表:由多个无符号数或者其他表作为数据项构成的符合数据类型,全部表习惯性的以_info结尾

形参与实参

形参:全称为:“形式参数”,是在定义方法名和方法体的时候使用的参数,用于接收调用该方法是传入的实际值
实参:全称为"实际参数",是调用该方法时传递给该方法的实际值

用代码演示三种代理

静态代理:
由程序员创建或工具生成代理类的源码,再编译代理类,所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托的关系在运行前就确定了
缺点:每个需要代理的对象都需要自己重复编写代理,很不舒服
优点:但是可以面相实际对象或者是接口的方式实现代理

动态代理:
也叫做JDK代理,接口代理,动态代理的对象,是利用JDK的API,动态的在内存中构建代理对象(是根据被代理的接口来动态生成代理类的class文件,并加载运行的过程),这就是动态代理
优点:不用关心代理类,只需要在运行阶段才指定代理哪一个对象

Java与语言特点

  1. ⾯向对象(封装,继承,多态)
  2. 平台⽆关性( Java 虚拟机实现平台⽆关性)
  3. 支持多线程
  4. ⽀持⽹络编程并且很⽅便( Java 语⾔诞⽣本身就是为简化⽹络编程设计的,因此 Java 语⾔不仅⽀持⽹络编程⽽且很⽅便)
  5. 编译与解释并存

分代收集算法

当前主流VM垃圾收集都采用分代收集(Fenerational Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代,老年代,永久代,这样就可以根据个年代特点分别采用最适合的GC算法

Java中的编译器常量是什么?使用它有什么风险?

公共静态不可变(public static final)变量也即是我们所说的编译器常量,这里的public是可选的,实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变,这种存在的一个问题是你使用了一个内部的或第三方库中的共有编译时常量,当时这个值后面被其他人改变了,当时你的客户端仍然在使用老的值,甚至你已经部署了一个洗呢jar,为了避免这种情况,当你在更新依赖jar文件时,确保重新编译你的程序

什么是"依赖注入"和"控制反转"?

控制反转(IOC)是Spring框架的核心思想,用我自己的话说,就是你要做一件事,别自己可劲new了,你就说你要干啥,然后外包出去就好
依赖注入(DI)在我浅薄的想法中,就是通过接口的引用和构造方法的表达,将一些事情整好了反过来传给需要用到的地方

面向对象

⾯向对象易维护、易复⽤、易扩展。 因为⾯向对象有封装、继承、多态性的特
性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,⾯向对象性能
⽐⾯向过程低。

String str = “i” 和 String str = new String(“i”)一样吗?

不一样,因为内存的分配方式不一样,String str =“i” 的方式,Java虚拟机会将其分配到常量池中,而String str = new String (“i”)则会被分到堆内存中

如果对象的引用被设置为null,垃圾回收期是否会立即释放对象占用的内存

不会,在下一个垃圾回收周期中,这个对象将是可被回收的

是否了解连接池,使用连接池有什么好处?

数据库连接是非常消耗资源的,影响到程序的性能指标,连接池是用来分配,管理释放数据库连接的,可以使应用重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接连接,通过释放空闲时间较长的数据库连接避免使用数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能

你所了解的数据源技术有哪些?使用数据源有什么好处?

Dbcp,c3p0den,用的最多的还是c3p0,因为更加稳定,安全,通过配置文件的形式来维护数据库信息,而不是通过硬编码,当连接的数据库信息发生改变时,不需要再更改程序代码就实现了数据库信息的更新

抽象类能使用final修饰吗?

不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被基础,这样彼此就会产生矛盾,所以final不能修饰抽象类

Java数据类型

Java中数据类型分两种:
1.基本类型:long,int,byte,float,double,char
2.对象类型:Long,Integer,Byte,Float,Double其它一切java提供的,或者你自己创建的类。其中Long叫 long的包装类。Integer、Byte和Float也类似,一般包装类的名字首写是数值名的大写开头。
ID用long还是Long?
hibernate、el表达式等都是包装类型,用Long类型可以减少装箱/拆箱
在hibernate中的自增的hid在实体中的类型要用Long 来定义而不是long。否则在DWR的匹配过程中会出现Marshallingerror:null的错误提示。
到底是选择Long 还是long这个还得看具体环境,如果你认为这个属性不能为null,那么就用long,因为它默认初值为0,如果这个字段可以为null,那么就应该选择Long。

JVM JDK 和 JRE

什么是JVM?java虚拟机包括什么?

JVM:java虚拟机,运用硬件或软件手段实现的虚拟的计算机
Java虚拟机包括:寄存器,堆栈,处理器

JVM

Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。

什么是字节码?采⽤字节码的好处是什么?
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的⽂件),它不⾯向任何特定的处理器,只⾯向虚拟机。由于字节码并不针对⼀种特定的机器,因此,Java 程序⽆须重新编译便可在多种不同操作系统的计算机上运⾏。

Java程序从源代码到运行一般步骤:
.java文件(源代码)经过JDK中的javac编译,生成.class文件(JVM中可理解的Java字节),JVM生成机器可执行的二进制机器码

为什么java是编译与解释共存的语言?
.class->机器码 这⼀步。有些⽅法和代码块是经常需要被调⽤的(也就是所谓的热点代码),所以后⾯引进了 JIT 编译器,⽽ JIT 属于运行时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使⽤。机器码的运⾏效率肯定是⾼于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔。

Java 既有解释执行,也有编译执行,为了解决解释器的性能瓶颈问题,优化 Java 的性能,引入了即时编译器,大幅度的提高运行效率。

java代码执行过程
在这里插入图片描述

JDK 和 JRE

JDK 是 Java Development Kit,它是功能⻬全的 Java SDK。
JRE 是 Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟(JVM),Java 类库,java 命令和其他的⼀些基础构件。

Java 和 C++的区别?

  • 都是⾯向对象的语⾔,都⽀持封装、继承和多态
  • Java 的类是单继承的,C++ ⽀持多重继承;虽然 Java 的类不可以多继承,但是接⼝可以多
    继承。
  • Java 有⾃动内存管理机制,不需要程序员⼿动释放⽆⽤内存

字符型常量和字符串常量的区别?

  1. 形式上: 字符常量是单引号引起的⼀个字符; 字符串常量是双引号引起的若⼲个字符
  2. 含义上: 字符常量相当于⼀个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表⼀个地
    址值
    (该字符串在内存中存放位置)
  3. 占内存⼤⼩ 字符常量只占 2 个字节; 字符串常量占若⼲个字节 (注意: char 在 Java 中占两
    个字节)

什么是B/S架构?什么是C/S架构?

B/S(Browser/Server),浏览器/服务器程序
C/S(Clent/Server),客户端/服务端,桌面应用程序

Java都有哪些开发平台?

JAVA SE:主要用在客户端开发
JAVA EE:主要用在web应用程序开发
JAVA ME:主要用在嵌入式应用程序开发

四大特性

面向对象思想OOP
抽象
关键词abstract声明的类叫作抽象类,abstract声明的⽅法叫抽象⽅法
⼀个类⾥包含了⼀个或多个抽象⽅法,类就必须指定成抽象类
抽象⽅法属于⼀种特殊⽅法,只含有⼀个声明,没有⽅法体
抽象支付:pay(金额,订单号),默认实现是本地支付,微信支付,支付宝支付,银行卡支付
封装
封装是把过程和数据包围起来,对数据的访问只能通过已定义的接⼝即⽅法
在java中通过关键字private,protected和public实现封装。
封装把对象的所有组成部分组合在⼀起,封装定义程序如何引⽤对象的数据,
封装实际上使⽤⽅法将类的数据隐藏起来,控制⽤户对类的修改和访问数据的程度。 适当的
封装可以让代码更容易理解和维护,也加强了代码的安全性
类封装
⽅法封装
继承
⼦类继承⽗类的特征行为,使得⼦类对象具有⽗类的方法属性(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。⽗类也叫基类,具有公共的⽅法和属性
动物<-猫
动物<-狗

        abstract class AbsPay{        }        WeixinPay extends AbsPay{        }        AliPay extends AbsPay{        }

多态
所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
多态性分为编译时的多态性和运行时的多态性。方法重载实现的是编译时的多态性,而方法重写实现的是运行时的多态性。
优点:减少耦合、灵活可拓展
⼀般是继承类或者重写⽅法实现

Java 中实现多态的机制是什么?

多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用修改源程序,就可以让引用变量绑定到各种不同的类实现上。

Java 实现多态有三个必要条件: 继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能够具备调用父类方法和子类的方法。

构造器 Constructor 是否可被 override?

Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。

重写和重载的区别

重载Overload:表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同,参数个数或类型不同
重写Override:重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法
重写发⽣在运⾏期,是⼦类对⽗类的允许访问的⽅法的实现过程进⾏重新编写。

  1. 返回值类型、⽅法名、参数列表必须相同,抛出的异常范围⼩于等于⽗类,访问修饰符范围⼤于等于⽗类。
  2. 如果⽗类⽅法访问修饰符为 private/final/static 则⼦类就不能重写该⽅法,但是被 static 修饰的⽅法能够被再次声明。
  3. 构造⽅法⽆法被重写
    综上:重写就是⼦类对⽗类⽅法的重新改造,外部样⼦不能改变,内部逻辑可以改变

什么情况下会出现内存溢出,内存泄漏?

内存泄漏的原因很简单:

  1. 对象是可达的(一直被引用)
  2. 当时对象不会被使用

常见的内存泄漏的例子:
解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上述内存泄漏问题了,其他内存内存泄漏得一步一步分析了

内存溢出的原因:

  1. 内存溢出导致堆栈内存不断增大,从而引发内存溢出
  2. 大量的jar,class文件加载,装载类的空间不够,溢出
  3. 操作大量的对象导致对聂村空间已经用满了,溢出
  4. nio直接操作内存,内存过大导致溢出

解决:
查看程序是否存在内存泄漏的问题
设置参数加大空间
代码中是否存在死循环或者循环产生过多重复的对象实
查看是否使用nio直接操作内存

String,StringBuffer 和 StringBuilder 的区别是什么?

String为什么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串, private final char value[] (在 Java 9 之后,String 类的实现改⽤ byte 数组存储字符串 private final byte[] value),所以 String 对象是不可变的。
⽽ StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。

线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是StringBuilder 与 StringBuffer 的公共⽗类,定义了⼀些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共⽅法。StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。

以StringBuffer的apend举例:
在这里插入图片描述
性能
每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。StringBuffer 每次都会对 StringBuffer 对象本身进⾏操作,⽽不是⽣成新的对象并改变对象引⽤。
相同情况下使⽤ StringBuilder 相⽐使⽤ StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的⻛险

总结:

  1. 操作少量的数据: 适⽤ String
  2. 单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder
  3. 多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer

String 为什么要设计成不可变的?

1)字符串常量池需要 String 不可变。因为 String 设计成不可变,当创建一个 String 对象时, 若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。如果字符串变量允许改变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。
2)String 对象可以缓存 hashCode。字符串的不可变性保证了 hash 码的唯一性,因此可以缓存 String 的 hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较 hashCode,提高了比较性能;
3)安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机制所需的 Strign 参数等,若 String 可变,将会引起各种安全隐患。

自动装箱与拆箱

装箱:将基本类型⽤它们对应的引⽤类型包装起来;
拆箱:将包装类型转换为基本数据类型;

在一个静态方法内调用⼀个非静态成员为什么是非法的?

由于静态⽅法可以不通过对象进⾏调⽤,因此在静态⽅法⾥,不能调用其他非静态变量,也不可以访问⾮静态变量成员。
Non-static field ‘a’ cannot be referenced from a static context

无参构造

Java 程序在执⾏⼦类的构造⽅法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造⽅法”。因此,如果⽗类中只定义了有参数的构造⽅法,⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为 Java 程序在⽗类中找不到没有参数的构造⽅法可供执⾏。解决办法是在⽗类⾥加上⼀个不做事且没有参数的构造⽅法。

接口

接口是否可以继承接口?接口是否支持多继承?类是否支持多继承?接口里面是否可以有方法实现?

接⼝⾥可以有静态⽅法和⽅法体
接⼝中所有的⽅法必须是抽象⽅法(JDK8之后就不是)
接⼝不是被类继承了,而是要被类实现
接⼝⽀持多继承, 类不⽀持多个类继承
⼀个类只能继承⼀个类,但是能实现多个接⼝,接⼝能继承另⼀个接⼝,接⼝的继承使⽤extends关键字,和类继承⼀样

JDK8接口新特性
interface中可以有static方法,但必须有⽅法实现体,该⽅法只属于该接⼝,接⼝名直接调⽤该⽅法
接⼝中新增default关键字修饰的方法,default⽅法只能定义在接⼝中,可以在⼦类或⼦接⼝ 中被重写default定义的⽅法必须有⽅法体
⽗接⼝的default⽅法如果在⼦接⼝或⼦类被重写,那么⼦接⼝实现对象、⼦类对象,调⽤该方法,以重写为准
本类、接⼝如果没有重写⽗类(即接⼝)的default⽅法,则在调⽤default⽅法时,使⽤⽗类(接口) 定义的default⽅法逻辑

接口和抽象类

  1. 接⼝的⽅法默认是 public ,所有⽅法在接⼝中不能有实现,即只能有抽象方法(Java 8 开始接⼝⽅法可以有默认实现),而抽象类可以有非抽象的⽅法。
  2. 接⼝中除了 static 、 final 变量,不能有其他变量,⽽抽象类中则不⼀定。
  3. ==⼀个类可以实现多个接⼝,但只能实现⼀个抽象类。==接口自己本身可以通过 extends 关键字扩展多个接⼝。
  4. 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。
  5. 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,而接口是对行为的抽象,是⼀种行为的规范。

成员变量与局部变量的区别有哪些?

  1. 从语法形式上看:成员变量是属于类的,而局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  2. 从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
  3. 从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调用而⾃动消失。
  4. 成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。

构造方法

⼀个类的构造⽅法的作⽤是什么? 若⼀个类没有声明构造⽅法,该程序能正确执⾏吗? 为什么?
主要作用是完成对类对象的初始化⼯作。可以执⾏。因为⼀个类即使没有声明构造⽅法也会有默认的不带参数的构造⽅法。

构造⽅法有哪些特性?

  1. 名字与类名相同。
  2. 没有返回值,但不能⽤ void 声明构造函数。
  3. ⽣成类的对象时⾃动执⾏,⽆需调⽤。

静态方法和实例方法有何不同

  1. 在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式。⽽实例⽅法只有后⾯这种⽅式。也就是说,调⽤静态⽅法可以⽆需创建对象。
  2. 静态⽅法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态⽅法),⽽不允许访问实例成员变量和实例⽅法;实例⽅法则⽆此限制。

== 与 equals(重要)

两个等号,如果是基本数据类型判断的是值,引用数据类型判断的是内存地址
equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况:
情况 1:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

  • String 中的 equals ⽅法是被重写过的,因为 object 的 equals ⽅法是比较的对象的内存地址,而String 的 equals ⽅法比较的是对象的值。
  • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引⽤。如果没有就在常量池中重新创建⼀个 String 对象

hashCode 与 equals (重要)

hashCode

==hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。==这个哈希码的作⽤是确定该对象在哈希表中的索引位置。 hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode ⽅法是本地⽅法,也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。

public native int hashCode();

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就
利⽤到了散列码!(可以快速找到所需要的对象)

为什么重写 equals 时必须重写 hashCode ⽅法?

如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。

final关键字

主要⽤在三个地⽅:变量、⽅法、类。
变量:如果是基本数据类型,那么加上final字段后,其值就不能进行更改,如果是引用数据类型,那么就不能让其指向另一个对象

方法:1.锁定方法,防止继承类修改它的含义;2.是效率。在早期的 Java 实现版本中,会将 final ⽅法转为内嵌调⽤。但是如果⽅法过于庞大,可能看不到内嵌调⽤带来的任何性能提升(现在的 Java 版本已经不需要使⽤final ⽅法进⾏这些优化了)。类中所有的 private ⽅法都隐式地指定为 final。

:加了final字段的类不允许被继承,其中所有成员方法被隐式地在指定为final方法

异常

Java异常类结构层次图

在这里插入图片描述
在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的子类** Exception (异常)**和 Error (错误)。Exception 能被程序本身处理( try catch ),Error 是⽆法处理的(只能尽量避免)。

异常处理总结

  • try 块: ⽤于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟⼀个 finally 块。
  • catch 块: ⽤于处理 try 捕获到的异常。
  • finally 块: ⽆论是否捕获或处理异常, finally 块⾥的语句都会被执⾏。当在 try 块或catch 块中遇到 return 语句时, finally 语句块将在⽅法返回之前被执⾏。

在以下 3 种特殊情况下, finally 块不会被执⾏:

  1. 在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后, finally 还是会被执⾏
  2. 程序所在的线程死亡。
  3. 关闭 CPU。

Java序列化

Java 序列化中如果有些字段不想进⾏序列化,怎么办?
使用transient或者transient注解
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。

键盘输入

⽅法 1:通过 Scanner

Scanner sc = new Scanner(System.in);

方法2:通过BufferedReader

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

JAVA注解

Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。

四种标准元注解

@Target

@Target说明了Annotation所修饰的对象范围: Annotation可被用于packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数。在 Annotation 类型的声明中使用了 target 可更加明晰其修饰的目标

@Retention 定义 被保留的时间长短

Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描
述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:
SOURCE:在源文件中有效(即源文件保留)
CLASS:在 class 文件中有效(即 class 保留)
RUNTIME:在运行时有效(即运行时保留)

@Documented 描述-javadoc

@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。

@Inherited 阐述了某个被标注的类型是被继承的

@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个annotation 将被用于该class 的子类。

注解处理器

 /1*** 定义注解*/@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface FruitProvider { /**供应商编号*/public int id() default -1;/*** 供应商名称*/ public String name() default ""/** * 供应商地址*/ public String address() default "";}//2:注解使用public class Apple { @FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路") private String appleProvider; public void setAppleProvider(String appleProvider) { this.appleProvider = appleProvider; } public String getAppleProvider() { return appleProvider; } }/3*********** 注解处理器 ***************/public class FruitInfoUtil { public static void getFruitInfo(Class<?> clazz) { String strFruitProvicer = "供应商信息:"; Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解 for (Field field : fields) { if (field.isAnnotationPresent(FruitProvider.class)) { FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);//注解信息的处理地方 strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:" + fruitProvider.name() + " 供应商地址:"+ fruitProvider.address(); System.out.println(strFruitProvicer); } } } } public class FruitRun { public static void main(String[] args) { FruitInfoUtil.getFruitInfo(Apple.class);/***********输出结果***************/// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延 } }

反射

反射机制是什么

反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

Person p=new Student();
其中编译时类型为 Person,运行时类型为 Student。

反射能做什么

  • 在运行时判断任意一个对象所属的类;
  • 在运行时构造任意一个类的对象;
  • 在运行时判断任意一个类所具有的成员变量和方法;
  • 在运行时调用任意一个对象的方法;
  • 生成动态代理

I/O

分类

  • 按照流的流向划分:输入流和输出流
  • 按照操作单元划分:字节流和字符流
  • 按照留的角色划分:节点流和处理流

既然有了字节流,为什么还要有字符流?

不管是⽂件读写还是⽹络发送接收,信息的最⼩存储单元都是字节,那为什么I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐好。

BIO,NIO,AIO 有什么区别?

BIO (Blocking I/O): 同步阻塞 I/O 模式
NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。

深拷贝 vs 浅拷贝

浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容

异常

如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
在这里插入图片描述

异常分类

**Throwable **是 Java 语言中所有错误或异常的超类。下一层分为 **Error 和 Exception **
Error

  1. Error 类是指 java 运行时系统的内部错误和资源耗尽错误。应用程序不会抛出该类对象。如果出现了这样的错误,除了告知用户,剩下的就是尽力使程序安全的终止。

Exception(RuntimeException、CheckedException)
2. Exception 又有两个分支,一个是运行时异常 RuntimeException ,一个是CheckedException。
RuntimeException 如 : NullPointerException 、 ClassCastException ;一个是检查异常CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。

JAVA内部类

Java 类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类。根据定义的方式不同,内部类分为静态内部类,成员内部类,局部内部类,匿名内部类四种。
静态内部类:public static class Inner
成员内部类:public class Inner
局部内部类:定义在方法中的类,就是局部类。如果一个类只在某个方法中使用,则可以考虑使用局部类。

 public void test(final int c) { final int d = 1; class Inner { public void print() { System.out.println(c); } } }

匿名内部类:

test.test(new Bird() { public int fly() { return 10000; } public String getName() { return "大雁"; } });

JDBC加载驱动

  1. 加载数据库驱动类
  2. 打开数据库链接
  3. 执行sql语句
  4. 处理返回结果
  5. 关闭资源

在使用jdbc的时候,如何防止出现sql注入

使用CallableStatement

怎么在JDBC内调用一个存储过程

使用PreparedStatement类,而不是使用Statement类

JAVA序列化

保存(持久化)对象及其状态到内存或者磁盘
Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。
Java 对象序列化就能够帮助我们实现该功能。

序列化对象以字节数组保持-静态成员不保存
使用 Java 对象序列化,==在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。==必须注意地是,==对象序列化保存的是对象的”状态”,即它的成员变量。==由此可知,对象序列化不会关注类中的静态变量。

序列化用户远程对象传输
==除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,==都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。

Serializable 实现序列化
在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化
通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。writeObject 和 readObject 自定义序列化策略
在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。

序列化 ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)

Transient 关键字阻止该变量被序列化到文件中

  1. 在变量声明前加上 Transient 关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。
  2. 服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

集合

LinkedHashMap 的应用

基于 LinkedHashMap 的访问顺序的特点,可构造一个 LRU(Least Recently Used)最近最少使用简单缓存。也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在LinkedHashMap 上扩展的。

HashMap 是线程安全的吗,为什么不是线程安全的?

不是线程安全的;
如果有两个线程 A 和 B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位置还有其他的数据。假设一种情况,线程 A 通讨 if 判断 ,该位置没有哈希冲突,进入了 if 语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程 B,线程 A 停在了 if 语句里面,线程 B 判断该位置没有哈希冲突(线程 A 的数据还没插入),也进入了 if 语句 ,线程 B 执行完后,轮到线程 A 执行,现在线程 A 直接在该位置插入而不用再判断。这时候,你会发现线程 A 把线程 B 插入的数据给覆盖了。发生了线程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能就直接给覆盖了。

HashMap 如何解决 Hash 冲突?

通过引入单向链表来解决 Hash 冲突。当出现 Hash 冲突时,比较新老 key 值是否相等,如果相等,新值覆盖旧值。如果不相等,新值会存入新的 Node 结点,指向老节点,形成链式结构,即链表。当 Hash 冲突发生频繁的时候,会导致链表长度过长,以致检索效率低,所以 JDK1.8 之后引入了红黑树,当链表长度大于 8 时,链表会转换成红黑书,以此提高查询性能。

HashSet 是如何保证不重复的?

向 HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较 hash 值,同时还要结合 equles 方法比较。
HashSet 中的 add ()方法会使用 HashMap 的 add ()方法。以下是 HashSet 部分源码:

private static final Object PRESENT = new Object(); private transientHashMap<E,Object> map;public HashSet() {map = new HashMap<>();}public boolean add(E e) {return map.put(e, PRESENT)==null;}

HashMap 的 key 是唯一的,由上面的代码可以看出 HashSet 添加进去的值就是作为
HashMap 的 key。所以不会 重复(HashMa 比较 key 是否相等是先比较 hashcode 在比
较 equals)。

List 和 Set 的区别

List , Set 都是继承自 Collection 接口
List 特点:元素有放入顺序,元素可重复 ,
Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals ()方法,另外 list 支持 for 循环,也就是通过下标来遍历,也可以用迭代器,但是 set 只能用迭代,因为他无序,无法用下标来取得想要的值。)

Set 和 List 对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。List:和数组类似,List 可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。

CopyOnWriteArrayList 是线程安全的吗?

是的
CopyOnWriteArrayList 使用了一种叫写时复制的方法,当有新元素添加到 CopyOnWriteArrayList 时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。创建新数组,并往新数组中加入一个新元素,这个时候,array 这个引用仍然是指向原数组的。当元素在新数组添加成功后,将 array这个引用指向新数组。
CopyOnWriteArrayList 的整个 add 操作都是在锁的保护下进行的。这样做是为了避免在多线程并发 add 的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

public boolean add(E e) {//1、先加锁final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;//2、拷贝数组Object[] newElements = Arrays.copyOf(elements, len + 1);//3、将元素加入到新数组中newElements[len] = e;//4、将 array引用指向到新数组setArray(newElements);return true;} finally {//5、解锁lock.unlock();}}

由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:

  1. 如果写操作未完成,那么直接读取原数组的数据;
  2. 如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
  3. 如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
    可见,CopyOnWriteArrayList 的读操作是可以不用加锁的。

CopyOnWriteArrayList 有几个缺点:

  1. 由于写操作的时候,需要拷贝数组,会消耗内存,
  2. 如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc
  3. 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,
    虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用因为谁也没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

CopyOnWriteArrayList 透露的思想:读写分离,读和写分开 最终一致性 使用另外开辟空间的思路,来解决并发冲突

数组越界问题

一般来讲我们使用时,会用一个线程向容器中添加元素,一个线程来读取元素,而读取的操作往往更加频繁。写操作加锁保证了线程安全,读写分离保证了读操作的效率,简直完美。如果这时候有第三个线程进行删除元素操作,读线程去读取容器中最后一个元素,读之前的时候容器大小为 i,当去读的时候删除线程突然删除了一个元素,这个时候容器大小变为了 i-1,读线程仍然去读取第 i 个元素,这时候就会发生数组越界。

测试一下,首先向 CopyOnWriteArrayList 里面塞 10000 个测试数据,启动两个线程,一个不断的删除元素,一个不断的读取容器中最后一个数据。

public void test(){for(int i = 0; i<10000; i++){list.add("string" + i);}new Thread(new Runnable() {@Overridepublic void run() {while (true) {if (list.size() > 0) {String content = list.get(list.size() - 1);}else {break;}}}}).start();new Thread(new Runnable() {@Overridepublic void run() {while (true) {if(list.size() <= 0){break;}list.remove(0);try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();}

Array 和 ArrayList 有何区别?

Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

Collection 和 Collections 有什么区别?

Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。

迭代器Iterator是什么?

地带起是一种设计模式,它是一个兑现,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层机构,迭代器通常被称为"轻量级"对象,因为创建它的代价小

怎么确保一个集合不能被修改?

可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.langUnsupportedoperationException异常

List

Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:分别是 ArrayList、Vector 和 LinkedList。

ArrayList(数组)

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

Vector(数组,线程同步)

Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。

LinkList(链表)

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。

说一下HashSet的实现原理?

HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT

HashSet(Hash表)

哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。

说说List,Set,Map三者的区别?

List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。
Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
**Map **(⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数y=f(x),“x”代表 key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。

RandomAccess 接⼝

public interface RandomAccess {}

在这里插入图片描述
RandomAccess 接⼝中什么都没有定义。所以,RandomAccess 接⼝不过是⼀个标识罢了。标识什么? 标识实现这个接⼝的类具有随机访问功能。在 binarySearch ⽅法中,它要判断传⼊的 list 是否RamdomAccess 的实例,如果是,调用 indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法

比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排
序。

List

什么是 Fail-Fast、什么是 Fail-Safe?

Fail-Fast:一旦发现遍历的同时其他人来修改,则立刻抛出异常
Fail-Safe:发现遍历的同事其他人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成

  • ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败
  • CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

Vector和ArrayList、LinkedList联系和区别

从线程安全角度:
ArrayList:底层是数组实现,线程不安全,查询和修改非常快根,根据下标就可以进行操作时间复杂度1,但是增加和删除慢,需要移动大量的元素,时间复杂度n
LinkedList: 底层是双向链表,线程不安全,查询和修改速度慢,需要进行遍历操作,时间复杂度为n,但是增加和删除速度快,时间复杂度1
Vector: 底层是数组(Object[] )实现,线程安全的,操作的时候使用synchronized进行加锁
使用场景:
Vector已经很少用了
增加和删除场景多则用LinkedList
查询和修改多则用ArrayList

如果需要保证线程安全,ArrayList应该怎么做,用有几种方式

方式一:自己写个包装类,根据业务一般是add/update/remove加锁
方式二:Collections.synchronizedList(new ArrayList<>()); 使用synchronized加锁
//本质还是加锁
List list2 = Collections.synchronizedList(list1);
方式三:CopyOnWriteArrayList<>() 使用ReentrantLock加锁

CopyOnWriteArrayList和Collections.synchronizedList实现线程安全有什么区别

CopyOnWriteArrayList:执行修改操作时,会拷贝一份新的数组进行操作(add、set、remove等),代价十分昂贵,在执行完修改后将原来集合指向新的集合来完成修改操作,源码里面用ReentrantLock可重入锁来保证不会有多个线程同时拷贝一份数组
以添加元素源码举例:

    public boolean add(E e) {        synchronized (lock) {            Object[] es = getArray();            int len = es.length;            es = Arrays.copyOf(es, len + 1);            es[len] = e;            setArray(es);            return true;        }    }

场景:读高性能,适用读操作远远大于写操作的场景中使用(读的时候是不需要加锁的,直接获取,删除和增加是需要加锁的, 读多写少)
Collections.synchronizedList:线程安全的原因是因为它几乎在每个方法中都使用了synchronized同步锁
场景:CopyOnWriteArrayList适合读多的场景,synchronizedList适合写多的场景
场景:写操作性能比CopyOnWriteArrayList好,读操作性能并不如CopyOnWriteArrayList

CopyOnWriteArrayList的设计思想是怎样的,有什么缺点?

设计思想:读写分离+最终一致
缺点:内存占用问题,写时复制机制,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象,如果对象过大(大对象会直接保存在老生代)则容易发生Yong GC和Full GC

ArrayList的扩容机制

注意:JDK1.7之前ArrayList默认大小是10,JDk1.8开始是未指定集合容量,默认是0,若已经指定的大小,(小于集合大小,小于10),当集合第一次添加元素的时候,集合大小扩容为10
ArrayList的元素个数大于其容量,扩容的大小=原始大小+原始大小/2

tips:关于ArraysList.addAll扩容机制详解
addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)
if(list.size=0&&addAll.size>10) then list.size = addAll.size else if ((addAll+list.size)> 下次扩容容量) then list.size扩容 = addAll+list.size else if((addAll+list.size)< 下次扩容容量) then list.size扩容 = 下次扩容的容量

Map

了解Map吗?用过哪些Map的实现

HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap
HashMap:底层是基于数组+链表,JDK8以后引入了红黑树,当链表大于8的时候,则会转成红黑树,非线程安全的,默认容量是16、允许有空的健和值
Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有null的健和值

HashMap 和 Hashtable 的区别

  1. HashMap 是⾮线程安全的, HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。
  2. 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。
  3. 对 Null key 和 Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。
  4. 初始容量⼤⼩和每次扩充容量⼤⼩的不同 :创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。

hashCode()和equals()

hashcode
顶级类Object里面的方法,所有的类都是继承Object,返回是一个int类型的数
根据一定的hash规则(存储地址,字段,长度等),映射成一个数组,即散列值
equals
顶级类Object里面的方法,所有的类都是继承Object,返回是一个boolean类型
根据自定义的匹配规则,用于匹配两个对象是否一样,一般逻辑如下
//判断地址是否一样
//非空判断和Class类型判断
//强转
//对象里面的字段一一匹配
使用场景:对象比较、或者集合容器里面排重、比较、排序

hashCode() 与 equals() 的相关规定:

  1. 如果两个对象相等,则 hashcode ⼀定也是相同的
  2. 两个对象相等,对两个 equals() ⽅法返回 true
  3. 两个对象有相同的 hashcode 值,它们也不⼀定是相等的
  4. 综上, equals() ⽅法被覆盖过,则 hashCode() ⽅法也必须被覆盖
  5. hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)。

手写Hashcode和equals

public class User {    private int age;    private  String name;    private Date time;    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public Date getTime() {        return time;    }    public void setTime(Date time) {        this.time = time;    }    @Override    public int hashCode() {        //int code = age/name.length()+time.hashCode();        //return code        return Objects.hash(age,name,time);    }    @Override    public boolean equals(Object obj) {        if(this == obj) return true;        if(obj == null || getClass() != obj.getClass()) return false;        User user = (User) obj;        return age == user.age && Objects.equals(name, user.name) && Objects.equals(time, user.time);    }}

HashMap 多线程操作导致死循环问题

主要原因在于并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。

ConcurrentHashMap 如何保证线程安全,jdk1.8 有什么变化?

DK1.7:使用了分段锁机制实现 ConcurrentHashMap,ConcurrentHashMap 在对象中保存了一个 Segment 数组,即将整个 Hash 表划分为多个分段;而每个 Segment 元素,即每个分段则类似于一个 Hashtable;这样,在执行 put 操作时
首先根据 hash 算法定位到元素属于哪个 Segment,然后对该 Segment 加锁即可。因此,ConcurrentHashMap 在多线程并发编程中可是实现多线程 put 操作,不过其最大并发度受 Segment 的个数限制。
JDK1.8: 底层采用数组+链表+红黑树的方式实现,而加锁则采用 CAS 和 synchronized实现

HashMap和TreeMap应该怎么选择,使用场景

hashMap: 散列桶(数组+链表),可以实现快速的存储和检索,但是确实包含无序的元素,适用于在map中插入删除和定位元素
treeMap:使用存储结构是一个平衡二叉树->红黑树,可以自定义排序规则,要实现Comparator接口,能便捷的实现内部元素的各种排序,但是一般性能比HashMap差,适用于安装自然排序或者自定义排序规则(写过微信支付签名工具类就用这个类)

Set和Map的关系

核心就是不保存重复的元素,存储一组唯一的对象
set的每一种实现都是对应Map里面的一种封装,
HashSet对应的就是HashMap,treeSet对应的就是treeMap

Set如何解决线程不安全问题

使用CopyOnWriteSet解决

常见Map的排序规则是怎样的?

按照添加顺序使用LinkedHashMap,按照自然排序使用TreeMap,自定义排序 TreeMap(Comparetor c)

如果需要线程安全,且效率高的Map,应该怎么做?

多线程环境下可以用concurrent包下的ConcurrentHashMap, 或者使用Collections.synchronizedMap(),
ConcurrentHashMap虽然是线程安全,但是他的效率比Hashtable要高很多

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤分段的数组+链表 实现,JDK1.8采⽤的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑⼆叉树。 Hashtable 和JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
实现线程安全的⽅式(重要):
① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable (同⼀把锁) :使⽤** synchronized** 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不
能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。

为什么Collections.synchronizedMap后是线程安全的?

使用Collections.synchronizedMap包装后返回的map是加锁的

介绍下你了解的HashMap

索引计算

索引计算方法

  • 首先,计算对象的 hashCode()
  • 再进行调用 HashMap 的 hash() 方法进行二次哈希
    • 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
  • 最后 & (capacity – 1) 得到索引

数组容量为何是 2 的 n 次幂

  1. 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
  2. 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap

注意

  • 二次 hash 是为了配合 容量是 2 的 n 次幂 这一设计前提,如果 hash 表的容量不是 2 的 n 次幂,则不必二次 hash
  • 容量是 2 的 n 次幂 这一设计计算索引效率更好,但 hash 的分散性就不好,需要二次 hash 来作为补偿,没有采用这一设计的典型例子是 Hashtable

树化与退化

树化意义

  • 红黑树用来避免 DoS 攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略
  • hash 表的查找,更新的时间复杂度是 O ( 1 ) O(1) O(1),而红黑树的查找,更新的时间复杂度是 O ( l o g 2 ⁡ n ) O(log_2⁡n ) O(log2n),TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表
  • hash 值如果足够随机,则在 hash 表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小

树化规则
在这里插入图片描述

  • 当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

退化规则

  • 情况1:在扩容时如果拆分树时,树元素个数 <= 6 则会退化链表
  • 情况2:remove 树节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表

HashMap底层(数组+链表+红黑树 jdk8才有红黑树)
数组中每一项是一个链表,即数组和链表的结合体
Node<K,V>[] table 是数组,数组的元素是Entry(Node继承Entry),Entry元素是一个key-value的键值对,它持有一个指向下个Entry的引用,table数组的每个Entry元素同时也作为当前Entry链表的首节点,也指向了该链表的下个Entry元素
在JDK1.8中,链表的长度大于8,链表会转换成红黑树

能否解释下什么是Hash碰撞?常见的解决办法有哪些,hashmap采用哪种方法

hash碰撞的意思是不同key计算得到的Hash值相同,需要放到同个bucket中
常见的解决办法:链表法、开放地址法、再哈希法,二次寻址法等
HashMap采用的是链表法

HashMap底层是 数组+链表+红黑树,为什么要用这几类结构呢?

数组 Node<K,V>[] table ,根据对象的key的hash值进行在数组里面是哪个节点
链表的作用是解决hash冲突,将hash值一样的对象存在一个链表放在hash值对应的槽位,红黑树 JDK8使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn),通过hash碰撞,让HashMap不断产生碰撞,那么相同的key的位置的链表就会不断增长,当对这个Hashmap的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树

为啥选择红黑树而不用其他树,比如二叉查找树,为啥不一直开始就用红黑树,而是到8的长度后才变换?
二叉查找树在特殊情况下也会变成一条线性结构,和原先的链表存在一样的深度遍历问题,查找性能就会慢,使用红黑树主要是提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据后会通过左旋,右旋、变色等操作来保持平衡,解决单链表查询深度的问题
数据量少的时候操作数据,遍历线性表比红黑树所消耗的资源少,且前期数据少平衡二叉树保持平衡是需要消耗资源的,所以前期采用线性表,等到一定数之后变换到红黑树

说下hashmap的put和get的核心逻辑(JDK8以上版本)

put:

  1. 判断table是否为空或者长度为0,那么则进行扩容操作
  2. 否则hash分析命中那个桶是否有值,没有值的话,直接保存插入
  3. 有值的话,判断key值是否一样,一样的话进行覆盖操作
  4. 不一样的话,判断是否为树节点,如果为树节点,直接插入,此时的话已经是一颗红黑树了
  5. 如果不是树节点,则还是链表,那么需要进行的是遍历插入
  6. 插入进去之后,进行判断,其长度是否大于8,大于8,则转成红黑树,不大于8的话,直接保存插入.
    get:
  7. 判断table是否为空或者长度为0
  8. 根据key算出bucket.然后获取首节点,hash碰撞概率小,通常链表第一个节点就是值,没必要进行遍历
  9. 如果不只一个值,就需要循环遍历,存在多个hash碰撞
  10. 然后查找的时候判断是否是红黑树,是的话则调用树的查找,如果不是则代表是链表结构,循环遍历获取节点

了解ConcurrentHashMap吗?为什么性能比hashtable高?

ConcurrentHashMap线程安全的Map, hashtable类基本上所有的方法都是采用synchronized进行线程安全控制高并发情况下效率就降低ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化

jdk1.7和jdk1.8里面ConcurrentHashMap实现的区别

JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全
技术点:Segment+HashEntry
JKD8的版本:取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率,CAS(读)+Synchronized(写)
技术点:Node+Cas+Synchronized

说下ConcurrentHashMap的put的核心逻辑(JDK8以上版本)

不允许key或者value为空
spread(key.hashCode()) 二次哈希,减少碰撞概率
tabAt(i) 获取table中索引为i的Node元素
casTabAt(i) 利用CAS操作获取table中索引为i的Node元素
put的核心流程
1、key进行重哈希spread(key.hashCode())
2、对当前table进行无条件循环
3、如果没有初始化table,则用initTable进行初始化
4、如果没有hash冲突,则直接用cas插入新节点,成功后则直接判断是否需要扩容,然后结束
5、(fh = f.hash) == MOVED 如果是这个状态则是扩容操作,先进行扩容
6、存在hash冲突,利用synchronized (f) 加锁保证线程安全
7、如果是链表,则直接遍历插入,如果数量大于8,则需要转换成红黑树
8、如果是红黑树则按照红黑树规则插入
9、最后是检查是否需要扩容addCount()

并发

volatile、ThreadLocal的使用场景和原理

volatile原理
volatile变量进行写操作时,JVM 会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写会到系统内存。
Lock 前缀指令实际上相当于一个内存屏障(也称内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。

volatile的适用场景
1)状态标志,如:初始化或请求停机
2)一次性安全发布,如:da模式
3)独立观察,如:定期更新某个值
4)“volatile bean” 模式
5) 开销较低的“读-写锁”策略,如:计数器

ThreadLocal什么时候会出现OOM的情况?为什么?

ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMap,
ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。

说说线程安全问题,什么是线程安全,如何实现线程安全?

线程安全 - 如果线程执行过程中不会产生共享资源的冲突,则线程安全。
线程不安全 - 如果有多个线程同时在操作主内存中的变量,则线程不安全

实现线程安全的三种方式
1)互斥同步
临界区:syncronized、ReentrantLock
信号量 semaphore
互斥量 mutex
2)非阻塞同步
CAS(Compare And Swap)
3)无同步方案
可重入代码:使用Threadlocal 类来包装共享变量,做到每个线程有自己的copy
线程本地存储

进程、线程、协程、程序

进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位

在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。

协程: 又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,一个线程可以多个协程,线程进程都是同步机制,而协程则是异步
Java的原生语法中并没有实现协程,目前python、Lua和GO等语言支持

关系:一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程

程序:是含有指令和数据的⽂件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。

多线程的价值?

(1)发挥多核 CPU 的优势
多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。
(2)防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行, 哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
(3)便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

调用 yeild()会阻塞吗?

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。yield()只是使当前线程重新回到可执行状态,所以执行 yield()的线程有可能在进入到可执行状态后马上又被执行。sleep()可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;yield()只能使同优先级的线程有执行的机会。

继承Thread类

Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

实现Runnable接口

如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。

public class MyThread extends OtherClass implements Runnable {  public void run() {  System.out.println("MyThread.run()");  } }

ExecutorService、Callable、Future 有返回值线程

有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程

//创建一个线程池ExecutorService pool = Executors.newFixedThreadPool(taskSize);// 创建多个有返回值的任务List<Future> list = new ArrayList<Future>(); for (int i = 0; i < taskSize; i++) { Callable c = new MyCallable(i + " "); // 执行任务并获取 Future 对象Future f = pool.submit(c); list.add(f); } // 关闭线程池pool.shutdown(); // 获取所有并发任务的运行结果for (Future f : list) { // 从 Future 对象上获取任务的返回值,并输出到控制台System.out.println("res:" + f.get().toString()); }

为什么要用线程池

那先要明白什么是线程池:线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。
使用线程池的好处:
线程池改进了一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。线程池节省了 CLR 为每个短生存周期任务创建一个完整的线程的开销并可以在任务完成后回收资源。
线程池根据当前在系统中运行的进程来优化线程时间片。
线程池允许我们开启多个任务而不用为每个线程设置属性。
线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。
线程池可以用来解决处理一个特定请求最大线程数量限制问题。

基于线程池的方式

线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。

 // 创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); while(true) { threadPool.execute(new Runnable() { // 提交多个线程任务,并执行 @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running .."); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }

四种线程池

Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService。

newCachedThreadPool

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。==调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。==因此,长时间保持空闲的线程池不会使用任何资源

newFixedThreadPool

==创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。==在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

 ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);  scheduledThreadPool.schedule(newRunnable(){  @Override  public void run() { System.out.println("延迟三秒"); } }, 3, TimeUnit.SECONDS);scheduledThreadPool.scheduleAtFixedRate(newRunnable(){  @Override  public void run() { System.out.println("延迟 1 秒后每三秒执行一次"); } },1,3,TimeUnit.SECONDS);

newSingleThreadExecutor

Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!

如果你提交任务时,线程池队列已满,这时候会发生什么?

两种可能:

  1. 如果使用的是无界队列LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务
  2. 如果使用的是有界队列比如ArrayBlockingQueue中,ArrayBlock满了,会根据maximumPoolSize的值增加线程数量,如果还是处理不过来导致线程池再次满的话,那么则会触发线程池的拒绝策略RejectedExecutionHandler处理满了的任务

协程对于多线程优缺点

优点:
非常快速的上下文切换,不用系统内核的上下文切换,减小开销
单线程即可实现高并发,单核CPU可以支持上万的协程
由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁
缺点:
协程无法利用多核资源,本质也是个单线程
协程需要和进程配合才能运行在多CPU上
目前java没成熟的第三方库,存在风险
调试debug存在难度,不利于发现问题

为什么要使用多线程?

从计算机底层来说: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。另外,多核 CPU 时代意味着多个线程可以同时运⾏,这减少了线程上下⽂切换的开销
从当代互联⽹发展趋势来说: 现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。

再深⼊到计算机底层来探讨:

单核时代: 在单核时代多线程主要是为了提⾼ CPU 和 IO 设备的综合利⽤率。举个例⼦:当只有⼀个线程的时候会导致 CPU 计算时,IO 设备空闲;进⾏ IO 操作时,CPU 空闲。我们可以简单地说这两者的利⽤率⽬前都是 50%左右。但是当有两个线程的时候就不⼀样了,当⼀个线程执⾏ CPU 计算时,另外⼀个线程可以进⾏ IO 操作,这样两个的利⽤率就可以在理想情况下达到 100%了。

多核时代: 多核时代多线程主要是为了提高CPU利用率。举个例⼦:假如我们要计算⼀个复杂的任务,我们只⽤⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利⽤到,⽽创建多个线程就可以让多个 CPU 核⼼被利⽤到,这样就提⾼了 CPU 的利⽤率。

使用多线程可能带来什么问题?

并发编程的⽬的就是为了能提高程序的执行效率和提高程序运行速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下文切换、死锁

什么是上下文切换?

多线程编程中⼀般线程的个数都⼤于 CPU 核心的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使用,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当⼀个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
概括来说就是:当前任务在执⾏完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。

上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。

并发和并行的区别

串行:串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。相当于一条流水线执行一组任务一样
并发 concurrency一台处理器上同时处理任务, 这个同时实际上是交替处理多个任务,程序中可以同时拥有两个或者多个线程,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,并发指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。要解决大并发问题,通常是将大任务分解成多个小任务
并行 parallellism:多个CPU上同时处理多个任务,一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行

并发指在一段时间内宏观上去处理多个任务。 并行指同一个时刻,多个任务确实真的同时运行。

例子:
并发是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来
并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情

一个项目经理A和3个程序BCD的故事
单线程:A给B讲完,等B做完,给C讲,等C完成,给D讲,等D完成
并发:A给B讲完需求,B自己去实现,期间A继续给C和D讲,不用等待某个程序员去完成,期间项目经理没空闲下来
并行:直接找3个项目经理分别分配给3个程序员

你知道java里面实现多线程有哪几种方式,有什么不同,比较常用哪种

  • 继承Thread
    继承Thread,重写里面run方法,创建实例,执行start
    优点:代码编写最简单直接操作
    缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差
public class ThreadDemo1 extends Thread {    @Override    public void run() {        System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName());    }}public static void main(String[] args) {      ThreadDemo1 threadDemo1 = new ThreadDemo1();      threadDemo1.setName("demo1");      threadDemo1.start();      System.out.println("主线程名称:"+Thread.currentThread().getName());}
  • 实现Runnable
    自定义类实现Runnable,实现里面run方法,创建Thread类,使用Runnable接口的实现对象作为参数传递给Thread对象,调用Strat方法
    优点:线程类可以实现多个几接口,可以再继承一个类
    缺点:没返回值,不能直接启动,需要通过构造一个Thread实例传递进去启动
public class ThreadDemo2 implements Runnable {    @Override    public void run() {        System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());    }}public static void main(String[] args) {        ThreadDemo2 threadDemo2 = new ThreadDemo2();        Thread thread = new Thread(threadDemo2);        thread.setName("demo2");        thread.start();        System.out.println("主线程名称:"+Thread.currentThread().getName());}JDK8之后采用lambda表达式public static void main(String[] args) {    Thread thread = new Thread(()->{                System.out.println("通过Runnable实现多线程,称:"+Thread.currentThread().getName());            });    thread.setName("demo2");    thread.start();    System.out.println("主线程名称:"+Thread.currentThread().getName());}
  • 通过Callable和FutureTask方式
    创建callable接口的实现类,并实现call方法,结合FutureTask类包装Callable对象,实现多线程
    优点:有返回值,拓展性也高
    缺点:jdk5以后才支持,需要重写call方法,结合多个类比如FutureTask和Thread类
public class MyTask implements Callable<Object> {    @Override    public Object call() throws Exception {        System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());        return "这是返回值";    }} public static void main(String[] args) {        FutureTask<Object> futureTask = new FutureTask<>(()->{            System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());            return "这是返回值";        });//        MyTask myTask = new MyTask();//        FutureTask<Object> futureTask = new FutureTask<>(myTask);        //FutureTask继承了Runnable,可以放在Thread中启动执行        Thread thread = new Thread(futureTask);        thread.setName("demo3");        thread.start();        System.out.println("主线程名称:"+Thread.currentThread().getName());        try {            System.out.println(futureTask.get());        } catch (InterruptedException e) {            //阻塞等待中被中断,则抛出            e.printStackTrace();        } catch (ExecutionException e) {            //执行过程发送异常被抛出            e.printStackTrace();        }    }
  • 通过线程池创建线程
    自定义Runnable接口,实现run方法,创建线程池,调用执行方法并传入对象
    优点:安全高性能,复用线程
    缺点: jdk5后才支持,需要结合Runnable进行使用
public class ThreadDemo4 implements Runnable {    @Override    public void run() {        System.out.println("通过线程池+runnable实现多线程,名称:"+Thread.currentThread().getName());    }}public static void main(String[] args) {//指定线程池的大小为3        ExecutorService executorService = Executors.newFixedThreadPool(3);        for(int i=0;i<10;i++){            executorService.execute(new ThreadDemo4());        }        System.out.println("主线程名称:"+Thread.currentThread().getName());        //关闭线程池        executorService.shutdown();}
  1. corePoolSize 核心线程数目 - 池中会保留的最多线程数
  2. maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
  3. keepAliveTime 生存时间 - 救急线程(也就是)的生存时间,生存时间内没有新任务,此线程资源会释放
  4. unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  5. workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  6. threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
    抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy
    由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
    丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
    丢弃最早排队任务 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy

线程池中,有四个重要的参数,决定影响了拒绝策略:
corePoolSize - 核心线程数,也即最小的线程数。
workQueue - 阻塞队列 。
maximumPoolSize -最大线程数
拒绝策略

当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。

线程池

线程池不允许使用 Executors 去创建,要通过 ThreadPoolExecutor的方式原因?

Executors创建的线程池底层也是调用 ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等,如果使用不当,会造成资源耗尽问题;
直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险
常见的线程池问题:
newFixedThreadPool和newSingleThreadExecutor:队列使用LinkedBlockingQueue,队列长度为 Integer.MAX_VALUE,可能造成堆积,导致OOM
newScheduledThreadPool和newCachedThreadPool:线程池里面允许最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM

你知道阻塞队列BlockingQueue不?介绍下常见的阻塞队列

BlockingQueue: j.u.c包下的提供了线程安全的队列访问的接口,并发包下很多高级同步类的实现都是基于阻塞队列实现的
1、当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满
2、从阻塞队列读数据时,如果队列为空,线程将会阻塞等待直到队列里面是非空的时候
常见的阻塞队列
ArrayBlockingQueue:
基于数组实现的一个阻塞队列,需要指定容量大小,FIFO先进先出顺序
LinkedBlockingQueue:
基于链表实现的一个阻塞队列,如果不指定容量大小,默认 Integer.MAX_VALUE, FIFO先进先出顺序
PriorityBlockingQueue:
一个支持优先级的无界阻塞队列,默认情况下元素采用自然顺序升序排序,也可以自定义排序实现

java.lang.Comparable接口DelayQueue:
延迟队列,在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素,里面的对象必须实现 java.util.concurrent.Delayed 接口并实现CompareTo和getDelay方法

用过线程池不? 有什么好处, java里有哪些是常用的线程池?

好处:重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能
类别
newFixedThreadPool
一个定长线程池,可控制线程最大并发数
newCachedThreadPool
一个可缓存线程池
newSingleThreadExecutor
一个单线程化的线程池,用唯一的工作线程来执行任务
newScheduledThreadPool
一个定长线程池,支持定时/周期性任务执行

为什么要使用线程池

池化技术相⽐⼤家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应⽤。池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。

线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。 每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。

使用线程池的好处
  • 降低资源消耗。通过重复利⽤已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能⽴即执⾏。
  • 提高线程的可管理性。线程是稀缺资源,如果⽆限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使⽤线程池可以进⾏统⼀的分配,调优和监控。
线程池拒绝策略:

CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy: 直接丢弃,其他啥都没有
DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列workQueue 中最老的一个任务,并将新任务加入

自定义线程池:

自定义线程池:常驻线程数量,最大线程数量,过期时间,单位,阻塞队列,线程工厂,拒绝策略
ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等与maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

如何合理的设置线程池中线程数的大小

线程池究竟设成多大是要看你给线程池处理什么样的任务,任务类型不同,线程池大小的设置方式也是不同的。任务一般可分为:CPU密集型、IO密集型,对于不同类型的任务需要分配不同大小的线程池。

CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。

IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。
IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
一般经过监控程序,逐步调整线程池线程数,使其达到合理的数量

执⾏ execute()方法和 submit()⽅法的区别是什么呢?
  1. execute() ⽅法⽤于提交不需要返回值的任务,所以⽆法判断任务是否被线程池执⾏成功与否;
  2. submit() ⽅法⽤于提交需要返回值的任务。线程池会返回⼀个 Future 类型的对象,通过这个 Future对象可以判断任务是否执⾏成功,并且可以通过 Future 的 get() ⽅法来获取返回值, get() ⽅法会阻塞当前线程直到任务完成,⽽使⽤ get( long timeout, TimeUnit unit) ⽅法则会阻塞当前线程⼀段时间后⽴即返回,这时候有可能任务没有执⾏完。
说说你在平时的开发中,如何使用线程池?

平时使用线程池时,一般使用 new ThreadPoolExecutor 的方式去创建线程池,再联系到这种方式的运行机制:
通过new创建线程池时,除非调用prestartAllCoreThreads方法初始化核心线程,否则此时线程池中有0个线程,即使工作队列中存在多个任务,同样不会执行
1:当任务数<=核心线程数的时候,会创建于当前任务数相同的核心线程用于处理线程
2:当当前任务数 >= 核心线程数&& 当前任务数 < 工作队列容量 + 核心线程数 会启动设定的核心线程数个线程 其他的任务就放到阻塞队列里
3: 当前任务数 > 工作队列容量+核心线程数的容量时,分两种情况当当前任务数 - 工作队列容量 <= 最大线程数 ,此时启动 (当当前任务数 - 工作队列容量)个线程
当当前任务数 - 核心线程数-工作队列容量 > 最大线程数,会启动最大线程数数量的线程来处理

Runnable和Callable接口区别

(1)是否有返回值
(2)是否抛出异常
(3)实现方法名称不同,一个是run方法,一个是call方法

java线程状态

JDK的线程状态分6种,JVM里面9种,我们一般说JDK的线程状态
常见的5种状态
创建(NEW): 生成线程对象,但是并没有调用该对象start(), new Thread()
就绪(Runnable):当调用线程对象的start()方法,线程就进入就绪状态,但是此刻线程调度还没把该线程设置为当前线程,就是没获得CPU使用权。 如果线程运行后,从等待或者睡眠中回来之后,也会进入就绪状态
运行(Running):程序将处于就绪状态的线程设置为当前线程,即获得CPU使用权,这个时候线程进入运行状态,开始运行run里面的逻辑
注意:有些文档把就绪和运行两种状态统一称为 “运行中”
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回
阻塞(Blocked)

  • 等待阻塞:进入该状态的线程需要等待其他线程作出一定动作(通知或中断),这种状态的话CPU不会分配过来,他们需要被唤醒,可能也会无限等待下去。比如调用wait(状态就会变成WAITING状态),也可能通过调用sleep(状态就会变成TIMED_WAITING), join或者发出IO请求,阻塞结束后线程重新进入就绪状态
  • 同步阻塞:线程在获取synchronized同步锁失败,即锁被其他线程占用,它就会进入同步阻塞状态

死亡(TERMINATED):一个线程run方法执行结束,该线程就死亡了,不能进入就绪状态

lock vs synchronized

三个层面

不同点

  • 语法层面
    • synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
    • Lock 是接口,源码由 jdk 提供,用 java 语言实现
    • 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
  • 功能层面
    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
  • 性能层面
    • 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock 的实现通常会提供更好的性能

公平锁

  • 公平锁的公平体现
    • 已经处在阻塞队列中的线程(不考虑超时)始终都是公平的,先进先出
    • 公平锁是指未处于阻塞队列中的线程来争抢锁,如果队列不为空,则老实到队尾等待
    • 非公平锁是指未处于阻塞队列中的线程来争抢锁,与队列头唤醒的线程去竞争,谁抢到算谁的
  • 公平锁会降低吞吐量,一般不用

条件变量

  • ReentrantLock 中的条件变量功能类似于普通 synchronized 的 wait,notify,用在当线程获得锁后,发现条件不满足时,临时等待的链表结构
  • 与 synchronized 的等待集合不同之处在于,ReentrantLock 中的条件变量可以有多个,可以实现更精细的等待、唤醒控制

多线程开发常用方法

  • sleep
    属于线程Thread的方法
    让线程暂缓执行,等待预计时间之后再恢复
    交出CPU使用权,不会释放锁
    进入阻塞状态TIME_WAITGING,睡眠结束变为就绪Runnable
  • yield
    属于线程Thread的方法
    t1/t2/t3
    暂停当前线程的对象,去执行其他线程
    交出CPU使用权,不会释放锁,和sleep类似
    作用:让相同优先级的线程轮流执行,但是不保证一定轮流
    注意:不会让线程进入阻塞状态,直接变为就绪Runnable,只需要重新获得CPU使用权
  • join
    属于线程Thread的方法
    在主线程上运行调用该方法,会让主线程休眠,不会释放已经持有的对象锁
    让调用join方法的线程先执行完毕,在执行其他线程
    类似让救护车警车优先通过
  • wait
    属于Object的方法
    当前线程调用对象的wait方法,会释放锁,进入线程的等待队列
    需要依靠notify或者notifyAll唤醒,或者wait(timeout)时间自动唤醒
  • notify
    属于Object的方法
    唤醒在对象监视器上等待的单个线程,选择是任意
  • notifyAll
    属于Object的方法
    唤醒在对象监视器上等待的全部线程

说说 sleep() ⽅法和 wait() 方法区别和共同点?

两者最主要的区别在于: sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁

两者都可以暂停线程的执⾏。
wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。
线程是否会自动苏醒:wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或 者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout) 超时后线程会⾃动苏醒。

start与run区别

  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。

为什么我们调用 start() 方法时会执行 run() 方法?

new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间片后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏ run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() ⽅法,会把 run()⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结: 调用 start() 方法方可启动线程并使线程进⼊就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

线程池中 submit() 和 execute() 方法有什么区别?

execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。

多线程中 synchronized 锁升级的原理是什么?

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

平时业务代码里面使用过多线程吗,能举例几个多线程的业务场景吗?

异步任务:用户注册、记录日志
定时任务:定期备份日志、备份数据库
分布式计算:Hadoop处理任务mapreduce,master-wark(单机单进程)
服务器编程:Socket网络编程,一个连接一个线程

说一下 atomic 的原理?

atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法来保证原子操作,从而避免synchronized 的高开销,执行效率大为提升。

能举几个不是线程安全的数据结构吗?

HashMap、ArrayList、LinkedList

JAVA 后台线程

  1. 定义:守护线程–也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
  2. 优先级:守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
  3. 设置:通过 **setDaemon(true)**来设置线程为“守护线程”;将一个用户线程设置为守护线程的方式是在线程对象创建 之前 用线程对象的 setDaemon 方法。
  4. 在 Daemon 线程中产生的新线程也是 Daemon 的。
  5. 线程则是 JVM 级别的,以 Tomcat 为例,如果你在 Web 应用中启动一个线程,这个线程的生命周期并不会和 Web 应用程序保持同步。也就是说,即使你停止了 Web 应用,这个线程依旧是活跃的。
  6. example: 垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,==所以当垃圾回收线程是 JVM 上仅剩的线程时,垃圾回收线程会自动离开。==它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
  7. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当 JVM 中所有的线程都是守护线程的时候,JVM 就可以退出了;如果还有一个或以上的非守护线程则 JVM 不会退出。

sleep() 和 wait() 有什么区别?

类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。

CAS

CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
AtomicXXX 等原子类底层就是CAS实现,一定程度比synchonized好,因为后者是悲观锁

CAS会存在什么比较严重的问题?
自旋时间长CPU利用率增加,CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用
存在ABA问题

能否解释下什么是CAS里面的ABA问题,怎么避免这个问题呢?

如果一个变量V初次读取是A值,并且在准备赋值的时候也是A值,那就能说明A值没有被修改过吗?其实是不能的,因为变量V可能被其他线程改回A值,结果就是会导致CAS操作误认为从来没被修改过,从而赋值给V给变量加一个版本号即可,在比较的时候不仅要比较当前变量的值 还需要比较当前变量的版本号。在java5中,已经提供了AtomicStampedReference来解决问题,检查当前引用是否等于预期引用,其次检查当前标志是否等于预期标志,如果都相等就会以原子的方式将引用和标志都设置为新值

ThreadLocal 了解么?

ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使⽤ get()和 set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
再举个简单的例⼦:
⽐如有两个⼈去宝屋收集宝物,这两个共⽤⼀个袋⼦的话肯定会产⽣争执,但是给他们两个⼈每个⼈分配⼀个袋⼦的话就不会出现这样的问题。如果把这两个⼈⽐作线程的话,那么ThreadLocal 就是⽤来避免这两个线程竞争的。

ThreadLocal 内部维护的是⼀个类似 Map 的 ThreadLocalMap 数据结构, key 为当前对象的 Thread 对象,值为 Object 对象。

ThreadLocal 内存泄露问题了解不?

==ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。==所以,如果ThreadLocal 没有被外部强引⽤的情况下,==在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。==这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法

在Java中可以有哪些方法来保证线程安全

  • 加锁,比如synchronize/ReentrantLock
  • 使用volatile声明变量,轻量级同步,不能保证原子性(需要解释)
  • 使用线程安全类(原子类AtomicXXX,并发容器,同步容器CopyOnWriteArrayList/ConcurrentHashMap等
  • ThreadLocal本地私有变量/信号量Semaphore等

读写锁

第一:无锁状态,多线程抢夺资源,乱
第二:使用synchronized和ReentrantLock,都是独占的,每次只能来一个操作,读读1,读写1,写写1
第三:读写锁 reentrantReadWriteLock,读读,可共享,提升性能,同时多人进行读操作,写写1
reentrantReadWriteLock缺点(1):造成锁饥饿,一直读,没有写操作(2)读时候,不能进行写操作,只有完成之后才能进行写操作,写操作可以读

知道ReentrantReadWriteLock吗?和ReentrantLock有啥不同?

ReentrantReadWriteLock
1、读写锁接口ReadWriteLock接口的一个具体实现,实现了读写锁的分离,
2、支持公平和非公平,底层也是基于AQS实现
3、允许从写锁降级为读锁
流程:先获取写锁,然后获取读锁,最后释放写锁;但不能从读锁升级到写锁
4、重入:读锁后还可以获取读锁;获取了写锁之后既可以再次获取写锁又可以获取读锁
核心:读锁是共享的,写锁是独占的。 读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,
主要是提升了读写的性能

ReentrantLock是独占锁且可重入的,相比synchronized而言功能更加丰富也更适合复杂的并发场景,但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防止线程A在写数据, 线程B在读数据造成的数据不一致; 但线程A在读数据,线程C也在读数据,读数据是不会改变数据没有必要加锁,但是还是加锁了,降低了程序的性能,所以就有了ReadWriteLock读写锁接口

AQS

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
它是一个Java提高的底层同步工具类,比如CountDownLatch、ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的只要搞懂了AQS,那么J.U.C中绝大部分的api都能轻松掌握
简单来说:是用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态对象
一个是 state(用于计数器,类似gc的回收计数器)
一个是线程标记(当前线程是谁加锁的),
一个是阻塞队列(用于存放其他未拿到锁的线程)
例子:线程A调用了lock()方法,通过CAS将state赋值为1,然后将该锁标记为线程A加锁。如果线程A还未释放锁时,线程B来请求,会查询锁标记的状态,因为当前的锁标记为 线程A,线程B未能匹配上,所以线程B会加入阻塞队列,直到线程A触发了 unlock() 方法,这时线程B才有机会去拿到锁,但是不一定肯定拿到

acquire(int arg) 源码讲解,好比加锁lock操作tryAcquire()尝试直接去获取资源,如果成功则直接返回,AQS里面未实现但没有定义成abstract,因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实tryAcquireSharedtryReleaseShared,类似设计模式里面的适配器模式addWaiter() 根据不同模式将线程加入等待队列的尾部,有Node.EXCLUSIVE互斥模式、Node.SHARED共享模式;如果队列不为空,则以通过compareAndSetTail方法以CAS将当前线程节点加入到等待队列的末尾。否则通过enq(node)方法初始化一个等待队列acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回,如果在等待过程中被中断,则返回true,否则返回false
release(int arg)源码讲解 好比解锁unlock独占模式下线程释放指定量的资源,里面是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了;在自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false
unparkSuccessor方法用于唤醒等待队列中下一个线程

volatile关键字

原子性

  • 起因:多线程下,不同线程的指令发生了交错导致的共享变量的读写混乱
  • 解决:用悲观锁或乐观锁解决,volatile 并不能解决原子性

可见性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致的对共享变量所做的修改另外的线程看不到
  • 解决:用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

有序性

  • 起因:由于编译器优化、或缓存优化、或 CPU 指令重排序优化导致指令的实际执行顺序与编写顺序不一致
  • 解决:用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
  • 注意:
    • volatile 变量写加的屏障是阻止上方其它写操作越过屏障排到 volatile 变量写之下
    • volatile 变量读加的屏障是阻止下方其它读操作越过屏障排到 volatile 变量读之上
    • volatile 读写加入的屏障只能防止同一线程内的指令重排

volatile 关键字,它是如何保证可见性,有序性?

volatile 可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在 JVM 底层volatile 是采用“内存屏障”来实现的。
观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入volatile 关键字时,会多出一个 lock 前缀指令,lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:
I.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面 的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
II.它会强制将对缓存的修改操作立即写入主存;
III.如果是写操作,它会导致其他 CPU 中对应的缓存行无效。

并发编程三要素?

(1)原子性 原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其
他操作打断,要 么就全部都不执行。
(2)可见性 可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,
其他线程可以立 即看到修改的结果。
(3)有序性 有序性,即程序的执行顺序按照代码的先后顺序来执行。

实现可见性的方法有哪些?

synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。

volatile和synchronized区别

volatile是轻量级的synchronized,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象
volatile:保证可见性,但是不能保证原子性
synchronized:保证可见性,也保证原子性
使用场景
1、不能修饰写入操作依赖当前值的变量,比如num++、num=num+1,不是原子操作,肉眼看起来是,但是JVM字节码层面不止一步
2、由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱

为什么会出现脏读?

JAVA内存模型简称 JMM
JMM规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存就行操作,使用volatile修饰变量,每次读取前必须从主内存属性获取最新的值,每次写入需要立刻写到主内存中
volatile关键字修修饰的变量随时看到的自己的最新值,假如线程1对变量v进行修改,那么线程2是可以马上看见

volatile可以避免指令重排,能否解释下什么是指令重排指令重排序分两类 编译器重排序和运行时重排序

JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = abc //4
定义顺序 1,2,3,4
计算顺序 1,3,2,4 和 2,1,3,4 结果都是一样
虽然指令重排序可以提高执行效率,但是多线程上可能会影响结果,有什么解决办法?
解决办法:内存屏障
解释:内存屏障是屏障指令,使CPU对屏障指令之前和之后的内存操作执行结果的一种约束

happens-before

先行发生原则,volatile的内存可见性就体现了该原则之一
例子:
//线程A操作
int k = 1;
//线程B操作
int j = k;
//线程C操作
int k = 2
分析:
假设线程A中的操作“k=1”先行发生于线程B的操作“j=k”,那确定在线程B的操作执行后,变量j的值一定等于1,依据有两个:一是先行发生原则,“k=1”的结果可以被观察到;二是第三者线程C还没出现,线程A操作结束之后没有其他线程会修改变量k的值。
但是考虑线程C出现了,保持线程A和线程B之间的先行发生关系,线程C出现在线程A和线程B的操作之间,但是线程C与线程B没有先行发生关系,那j的值会是多少?答案是1和2都有可能,因为线程C对变量k的影响可能会被线程B观察到,也可能不会,所以线程B就存在读取到不符合预期数据的风险,不具备多线程安全性
八大原则
1、程序次序规则
2、管程锁定规则
3、volatile变量规则
4、线程启动规则
5、线程中断规则
6、线程终止规则
7、对象终结规则
8、传递性

并发编程三要素

原子性:一个不可再被分割的颗粒,原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题

int num = 1; // 原子操作
num++; // 非原子操作,从主内存读取num到线程工作内存,进行 +1,再把num写到主内存, 除非用原子类,即java.util.concurrent.atomic里的原子变量类
解决办法是可以用synchronized 或 Lock(比如ReentrantLock) 来把这个多步操作“变成”原子操作,但是volatile,前面有说到不能修饰有依赖值的情况

public class XdTest {    private int num = 0;    //使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作    Lock lock = new ReentrantLock();    public  void add1(){        lock.lock();        try {            num++;        }finally {            lock.unlock();        }    }    //使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住    public synchronized void add2(){        num++;    }}

解决核心思想:把一个方法或者代码块看做一个整体,保证是一个不可分割的整体

有序性: 程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序,JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)(volatile禁止了指令重排)
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = abc //4
上面的例子 执行顺序1,2,3,4 和 2,1,3,4 结果都是一样,指令重排序可以提高执行效率,但是多线程上可能会影响结果
假如下面的场景,正常是顺序处理
//线程1
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
run(); //核心业务代码
}
指令重排序后,导致顺序换了,程序出现问题,且难排查
//线程1
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
run(); //核心业务代码
}
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法

可见性: 一个线程A对共享变量的修改,另一个线程B能够立刻看到

// 线程 A 执行int num = 0;// 线程 A 执行num++;// 线程 B 执行System.out.print("num的值:" + num);

线程A执行 i++ 后再执行线程 B,线程 B可能有2个结果,可能是0和1。
因为 i++ 在线程A中执行运算,并没有立刻更新到主内存当中,而线程B就去主内存当中读取并打印,此时打印的就是0;也可能线程A执行完成更新到主内存了,线程B的值是1。
所以需要保证线程的可见性
synchronized、lock和volatile能够保证线程可见性

调度算法

先来先服务调度算法
按照作业/进程到达的先后顺序进行调度 ,即:优先考虑在系统中等待时间最长的作业
排在长进程后的短进程的等待时间长,不利于短作业/进程
短作业优先调度算法
短进程/作业(要求服务时间最短)在实际情况中占有很大比例,为了使得它们优先执行
对长作业不友好
高响应比优先调度算法:
在每次调度时,先计算各个作业的优先权:优先权=响应比=(等待时间+要求服务时间)/要求服务时间,
因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应比=响应时间/要求服务时间,选择优先权高的进行服务需要计算优先权信息,增加了系统的开销
时间片轮转调度算法:
轮流的为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
由于高频率的进程切换,会增加了开销,且不区分任务的紧急程度
优先级调度算法:
根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理
如果高优先级任务很多且持续产生,那低优先级的就可能很慢才被处理

线程的调度策略

线程调度器选择优先级最高的线程运行,但是,如果发生一下情况就回终止线程的运行:

  1. 线程体中调用了yield方法让出了对cpu的占用权利
  2. 线程体中调用了sleep方法时线程进入睡眠状态
  3. 线程由于IO操作受到阻塞
  4. 另外一个更高优先级线程出现
  5. 在支持时间片的系统中,该线程的时间片用完

常见的线程间的调度算法

线程调度是指系统为线程分配CPU使用权的过程,主要分两种
协同式线程调度(分时调度模式)线程执行时间由线程本身来控制,==线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。==最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里
抢占式线程调度每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞

Java线程调度就是抢占式调度,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程
所以我们如果希望某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。
JAVA的线程的优先级,以1到10的整数指定。当多个线程可以运行时,VM一般会运行最高优先级的线程(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)
在两线程同时处于就绪runnable状态时,优先级越高的线程越容易被系统选择执行。但是优先级并不是100%可以获得,只不过是机会更大而已。

有人会说,wait,notify不就是线程本身控制吗?
其实不是,wait是可以让出执行时间,notify后无法获取执行时间,随机等待队列里面获取而已

java常见的锁

悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized

乐观锁:每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否会去更新数据,通过版本号来判断,如果数据被修改了就拒绝更新,在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作,比如CAS是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败
小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁多

公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁 比如ReentrantLock(底层是同步队列FIFO:First Input First Output来实现)
非公平锁获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronizedReentrantLock :在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
小结:
非公平锁:效率高因为能重复利用CPU的时间,不过可能存在线程饿死
公平锁:效率相对较低

可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,synchronized(隐式)和lock(显式)
synchronized:

        Object o = new Object();        new Thread(()->{            synchronized (o){                System.out.println(Thread.currentThread().getName()+"外层");                synchronized (o){                    System.out.println(Thread.currentThread().getName()+"中层");                    synchronized (o){                        System.out.println(Thread.currentThread().getName()+"内层");                    }                }            }        },"T1").start();

lock:

        Lock lock = new ReentrantLock();        new Thread(()->{            lock.lock();            System.out.println("aaaa");            lock.unlock();        },"aa").start();        new Thread(()->{            try {                lock.lock();                System.out.println(Thread.currentThread().getName()+"外层");                try {                    lock.lock();                    System.out.println(Thread.currentThread().getName()+"中层");                    try {                        lock.lock();                        System.out.println(Thread.currentThread().getName()+"内层");                    }finally {                        lock.unlock();                    }                }finally {                    lock.unlock();                }            }finally {                lock.unlock();            }        },"T1").start();

不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞

小结:可重入锁能一定程度的避免死锁 synchronized、ReentrantLock 重入锁

    private void meathA(){            //获取锁 TODO        meathB();    }    private void meathB(){            //获取锁 TODO            //其他操作    }

分段锁、行锁、表锁

自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁.

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
小结:不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU
常见的自旋锁:TicketLock,CLHLock,MSCLock

共享锁:也叫S锁/读锁,能查看但无法修改和删除的一种数据锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享
互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁/ 该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得互斥锁的线程即能读数据又能修改数据

Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现
ReentrantReadWriteLock。

分段锁:分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践

下面三种是Jvm为了提高锁的获取与释放效率而做的优化针对Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程,
偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,获取锁的代价更低
轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点
重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会一直循环下去,当自旋一定次数的时候且还没有获取到锁,就会进入阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低

死锁

两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去
死锁代码:

        new Thread(()->{            synchronized (a){                System.out.println(Thread.currentThread().getName()+"持有a,试图获取b");                try {                    TimeUnit.SECONDS.sleep(1);                } catch (InterruptedException e) {                    e.printStackTrace();                }                synchronized (b){                    System.out.println(Thread.currentThread().getName()+"获取b");                }            }        },"a").start();        new Thread(()->{            synchronized (b){                System.out.println(Thread.currentThread().getName()+"持有b,试图获取a");                try {                    TimeUnit.SECONDS.sleep(1);                } catch (InterruptedException e) {                    e.printStackTrace();                }                synchronized (a){                    System.out.println(Thread.currentThread().getName()+"获a");                }            }        },"b").start();
如何查看死锁?
jps jstack 30288
死锁的4个必要条件

互斥条件:资源不能共享,只能由一个线程使用
请求与保持条件:线程已经获得一些资源,但因请求其他资源发生阻塞,对已经获得的资源保持不释放
不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放
循环等待条件:多个线程形成环形链,每个都占用对方申请的下个资源

只要发生死锁,上面的条件都成立;只要一个不满足,就不会发生死锁

如何避免死锁?
  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件 :⼀次性申请所有的资源。
  3. 破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。

synchronized

synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
synchronized是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块中
是非公平锁和可重入锁
每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性

两种形式:
方法:生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,也叫隐式同步
代码块:加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令,每个monitor维护着一个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,当一个线程获执行monitorenter后,该计数器自增1;当同一个线程执行monitorexit指令的时候,计数器再自减1。当计数器为0的时候,monitor将被释放.也叫显式同步
两种本质上没有区别,底层都是通过monitor来实现同步, 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

jdk1.6后进行了优化,你知道哪些大的变化
有得到锁的资源进入Block状态,涉及到操作系统用户模式和内核模式的切换,代价比较高
jdk6进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性能仍然较低

说说自己是怎么使用 synchronized 关键字

synchronized 关键字最主要的三种使⽤⽅式:
1.修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁

synchronized void method() { //业务代码}

作用于方法时,锁住的是对象的实例(this);

2.修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得 当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。所以,如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。

synchronized void staic method() { //业务代码}

当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;

3.修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized( .class) 表示进⼊同步代码前要获得 当前 class 的锁

synchronized(this) { //业务代码}

总结:

  • synchronized 关键字加到 static 静态⽅法和 synchronized(class) 代码块上都是是给 Class类上锁。
  • synchronized 关键字加到实例⽅法上是给对象实例上锁。尽量不要使⽤ synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

双重校验锁实现对象单例(线程安全)

public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进⼊加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; }}

另外,需要注意 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要。uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执⾏:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址
    但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不
    会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执
    ⾏了 1 和 3,此时 T2 调⽤ getUniqueInstance () 后发现 uniqueInstance 不为空,因此返回
    uniqueInstance ,但此时 uniqueInstance 还未被初始化。
    使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。

构造方法可以使⽤ synchronized 关键字修饰么?

构造⽅法本身就属于线程安全的,不存在同步的构造⽅法⼀说。

说说Synchronized与Lock的区别

synchronized的锁可重入、不可中断、非公平,每次使用会自动释放锁
Lock锁可重入、可中断、可公平,每次使用需要自己加锁以及释放锁,一般情况下需放在finally里进行锁的释放,否则,可能因为未能正确释放锁而导致程序出问题。Lock可以实现有限时间内的获取锁的等待,在指定的时间内如果获取不到锁,会返回false

讲⼀下 synchronized 关键字的底层原理

synchronized 同步语句块的情况

在这里插入图片描述
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。

当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。

在执⾏ monitorenter 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执⾏ monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前
线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。

synchronized 修饰方法的的情况

在这里插入图片描述
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调
⽤。

总结:
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。

不过两者的本质都是对对象监视器 monitor 的获取。

ReentrantLock

ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。

ReentrantLock 与 synchronized

  1. ReentrantLock 通过方法 lock()与 unlock()来进行加锁与解锁操作,与 synchronized 会 被 JVM 自动解锁机制不同,ReentrantLock 加锁后需要手动进行解锁。为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作。
  2. ReentrantLock 相比 synchronized 的优势是可中断、公平锁、多个锁。这种情况下需要使用 ReentrantLock。

ReentrantLock和synchronized都是独占锁
synchronized:
1、是悲观锁会引起其他线程阻塞,java内置关键字,
2、无法判断是否获取锁的状态,锁可重入、不可中断、只能是非公平
3、加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单但显得不够灵活
4、一般并发场景使用足够、可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁
5、synchronized操作的应该是对象头中mark word,参考原先原理图片
ReentrantLock:
1、是个Lock接口的实现类,是悲观锁,
2、可以判断是否获取到锁,可重入、可判断、可公平可不公平
3、需要手动加锁和解锁,且 解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
4、在复杂的并发场景中使用在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致 其他线程无法获得该锁。
5、创建的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁
6、底层不同是AQS的state和FIFO队列来控制加锁

Semaphore 信号量

Semaphore 是一种基于计数的信号量。==它可以设定一个阈值,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。===Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池
实现互斥锁(计数器为 1)
我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,表示两种互斥状态。

Semaphore 有什么用?

Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore 有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1,相当于变成了一个synchronized 了。

常用的并发工具类有哪些?

(1)CountDownLatch
(2)CyclicBarrier
(3)Semaphore
(4)Exchanger

CyclicBarrier 和 CountDownLatch 的区别

(1)CountDownLatch 简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用 countDown()方法发出通知后,当前线程才可以继续执行。
(2)cyclicBarrier 是所有线程都进行等待,直到所有线程都准备好进入 await()方法之后, 所有线程同时开始执行!
(3)CountDownLatch 的计数器只能使用一次。而 CyclicBarrier 的计数器可以使用reset() 方法重置。所以 CyclicBarrier 能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
(4) CyclicBarrier 还提供其他有用的方法, 比如 getNumberWaiting 方法可以获得CyclicBarrier 阻塞的线程数量。isBroken 方法用来知道阻塞的线程是否被中断。如果被中断返回 true,否则返回 false。

Executors 类是什么?

Executors 为 Executor,ExecutorService,ScheduledExecutorService,ThreadFactory 和Callable 类提供了一些工具方法。Executors 可以用于方便的创建线程池

线程类的构造方法、静态块是被哪个线程调用的

线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么:
(1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
(2)Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的

Java 线程数过多会造成什么异常?

(1)线程的生命周期开销非常高
(2)消耗过多的 CPU 资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU 资源时还将产生其他性能的开销。
(3)降低稳定性
JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出 OutOfMemoryError 异常。

Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比同步它有什么优势?

Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有:
(1)可以使锁更公平
(2)可以使线程在等待锁的时候响应中断
(3)可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间
(4)可以在不同的范围,以不同的顺序获取和释放锁

什么是 Future?

在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread 类还是实现 runnable 接口,都无法保证获取到之前的执行结果。通过实现Callback 接口,并用 Future 可以来接收多线程的执行结果。Future 表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加 Callback 以便在任务执行成功或失败后作出相应的操作。

多线程的5种通信方式

问题:有两个线程,A 线程向一个集合里面依次添加元素“abc”字符串,一共添加十次,当添加到第五次的时候,希望 B 线程能够收到 A 线程的通知,然后 B 线程执行相关的业务操作。线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。

  • 使用 volatile 关键字
    基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想。大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
public class TestSync {    //定义共享变量来实现通信,它需要volatile修饰,否则线程不能及时感知    static volatile boolean notice = false;    public static void main(String[] args) {        List<String>  list = new ArrayList<>();        //线程A        Thread threadA = new Thread(() -> {            for (int i = 1; i <= 10; i++) {                list.add("abc");                System.out.println("线程A添加元素,此时list的size为:" + list.size());                try {                    Thread.sleep(500);                } catch (InterruptedException e) {                    e.printStackTrace();                }                if (list.size() == 5)                    notice = true;            }        });        //线程B        Thread threadB = new Thread(() -> {            while (true) {                if (notice) {                    System.out.println("线程B收到通知,开始执行自己的业务...");                    break;                }            }        });        //需要先启动线程B        threadB.start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        // 再启动线程A        threadA.start();    }}
  • 使用 Object 类的 wait()/notify()
    Object 类提供了线程间通信的方法:wait()、notify()、notifyAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
    注意:wait/notify 必须配合 synchronized 使用,wait 方法释放锁,notify 方法不释放锁。wait 是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify(),notify并不释放锁,只是告诉调用过wait()的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放,调用 wait() 的一个或多个线程就会解除 wait 状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。
public class TestSync {    public static void main(String[] args) {        //定义一个锁对象        Object lock = new Object();        List<String>  list = new ArrayList<>();        // 线程A        Thread threadA = new Thread(() -> {            synchronized (lock) {                for (int i = 1; i <= 10; i++) {                    list.add("abc");                    System.out.println("线程A添加元素,此时list的size为:" + list.size());                    try {                        Thread.sleep(500);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    if (list.size() == 5)                        lock.notify();//唤醒B线程                }            }        });        //线程B        Thread threadB = new Thread(() -> {            while (true) {                synchronized (lock) {                    if (list.size() != 5) {                        try {                            lock.wait();                        } catch (InterruptedException e) {                            e.printStackTrace();                        }                    }                    System.out.println("线程B收到通知,开始执行自己的业务...");                }            }        });        //需要先启动线程B        threadB.start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        //再启动线程A        threadA.start();    }}

由输出结果,在线程 A 发出 notify() 唤醒通知之后,依然是走完了自己线程的业务之后,线程 B 才开始执行,正好说明 notify() 不释放锁,而 wait() 释放锁。

  • 使用JUC工具类 CountDownLatch
    jdk1.5 之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了并发编程代码的书写,CountDownLatch 基于 AQS 框架,相当于也是维护了一个线程间共享变量 state。
public class TestSync {    public static void main(String[] args) {        CountDownLatch countDownLatch = new CountDownLatch(1);        List<String>  list = new ArrayList<>();        //线程A        Thread threadA = new Thread(() -> {            for (int i = 1; i <= 10; i++) {                list.add("abc");                System.out.println("线程A添加元素,此时list的size为:" + list.size());                try {                    Thread.sleep(500);                } catch (InterruptedException e) {                    e.printStackTrace();                }                if (list.size() == 5)                    countDownLatch.countDown();            }        });        //线程B        Thread threadB = new Thread(() -> {            while (true) {                if (list.size() != 5) {                    try {                        countDownLatch.await();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }                System.out.println("线程B收到通知,开始执行自己的业务...");                break;            }        });        //需要先启动线程B        threadB.start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        //再启动线程A        threadA.start();    }}
  • 使用 ReentrantLock 结合 Condition
public class TestSync {    public static void main(String[] args) {        ReentrantLock lock = new ReentrantLock();        Condition condition = lock.newCondition();        List<String> list = new ArrayList<>();        //线程A        Thread threadA = new Thread(() -> {            lock.lock();            for (int i = 1; i <= 10; i++) {                list.add("abc");                System.out.println("线程A添加元素,此时list的size为:" + list.size());                try {                    Thread.sleep(500);                } catch (InterruptedException e) {                    e.printStackTrace();                }                if (list.size() == 5)                    condition.signal();            }            lock.unlock();        });        //线程B        Thread threadB = new Thread(() -> {            lock.lock();            if (list.size() != 5) {                try {                    condition.await();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            System.out.println("线程B收到通知,开始执行自己的业务...");            lock.unlock();        });        threadB.start();        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        threadA.start();    }}

这种方式使用起来并不是很好,代码编写复杂,而且线程 B 在被 A 唤醒之后由于没有获取锁还是不能立即执行,也就是说,A 在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait()/notify() 一样。

  • 基本 LockSupport 实现线程间的阻塞和唤醒
    LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。
public class TestSync {    public static void main(String[] args) {        List<String> list = new ArrayList<>();        //线程B        final Thread threadB = new Thread(() -> {            if (list.size() != 5) {                LockSupport.park();            }            System.out.println("线程B收到通知,开始执行自己的业务...");        });        //线程A        Thread threadA = new Thread(() -> {            for (int i = 1; i <= 10; i++) {                list.add("abc");                System.out.println("线程A添加元素,此时list的size为:" + list.size());                try {                    Thread.sleep(500);                } catch (InterruptedException e) {                    e.printStackTrace();                }                if (list.size() == 5)                    LockSupport.unpark(threadB);            }        });        threadA.start();        threadB.start();    }}

JVM

基本概念

JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
我们都知道 Java 源文件,通过编译器,能够生产相应的.Class 文件,也就是字节码文件,而字节码文件又通过 Java 虚拟机中的解释器,编译成特定机器上的机器码 。
也就是如下:
① Java 源文件—->编译器—->字节码文件
② 字节码文件—->JVM—->机器码
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,这也就是 Java 为什么能够跨平台的原因了 ,当一个程序从开始运行,这时虚拟机就开始实例化了,多个程序启动就会存在多个虚拟机实例。程序退出或者关闭,则虚拟机实例消亡,多个虚拟机实例之间数据不能共享。

线程

这里所说的线程指程序执行过程中的一个线程实体。JVM 允许一个应用并发执行多个线程。Hotspot JVM 中的 Java 线程与原生操作系统线程有直接的映射关系。当线程本地存储、缓冲区分配、同步对象、栈、程序计数器等准备好以后,就会创建一个操作系统原生线程。Java 线程结束,原生线程随之被回收。操作系统负责调度所有线程,并把它们分配到任何可
用的 CPU 上。当原生线程初始化完毕,就会调用 Java 线程的 run() 方法。当线程结束时,会释放原生线程和 Java 线程的所有资源。

JVM 内存区域

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 Hotspot VM 内, 每个线程都与操作系统的本地线程直接映射, 因此这部分内存区域的存/否跟随本地线程的生/死对应)。
线程共享区域随虚拟机的启动/关闭而创建/销毁。
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使用DirectByteBuffer 对象作为这块内存的引用进行操作(详见: Java I/O 扩展), 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。

ThreadLock 用过没有,说说它的作用?

ThreadLock 为本地线程,为每一个线程提供一个局部变量,也就是说只有当前线层可以访问,是线程安全的。原理:为每一个线程分配一个对象来工作,并不是由 ThreadLock来完成的,而是需要在应用层面保证的,ThreadLock 只是起到了一个容器的作用。原理为 ThreadLock 的 set()跟 get()方法。

虚拟机栈是线程共享的吗?

不是。
JVM 初始运行的时候都会分配好 Method Area(方法区) 和 Heap(堆) ,而 JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和 Native Method Stack (本地方法栈), 当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉。这也是为什么我把内存区域分为线程共享和非线程共享的原因,非线程共享的那三个区域的生命周期与所属线程相同,而线程共享的区域与 JAVA 程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说只发生在 Heap 上)的原因。
栈区:
每个线程包含一个栈区,栈中只保存基础数据类型的值(比如 int i=1 中 1 就是基础类型的对象)和对象的引用以及基础数据的引用每个栈中的数据(基础数据类型和对象引用)都是私有的,其他栈不能访问。
栈分为 3 个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

堆区:
存储的全部是对象,每个对象都包含一个与之对应的 class 的信息。(class 的目的是得到操作指令) jvm 只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身 。
方法区:
又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的 class 和 static 变量。方法区中包含的都是在整个程序中永远唯一的元素,如 class,static 变量。(两者区别为堆区存放 new 出来的对象信息,方法区存放本身就具有的类信息)

常量存放在 JVM 的那个区域?

方法区: 又叫静态区,跟堆一样,被所有的线程共享。它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https的默认值),以及主机 (两个页面的模数 Document.domain 设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

所有的对象都分配到堆中吗?

不一定

JM的主要组成部分?及其作用?

  • 类加载器(ClassLoader)
  • 运行时数据区(Runtime Data Area)
  • 执行引擎(Execution Engine)
  • 本地库接口(Natice Interface)

组建的作用:首先通过类加载器(ClassLoader)会把Java代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存,而字节码文件只是JVM的一套指令集规范,不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言的本地库接口(Natic Interface)来实现这个程序的功能

JVM运行时数据区?

不同虚拟机的运行时数据区可能略有不同,但都会遵循java虚拟机规范,java虚拟机规范规定的区域分为以下5个部分:

  • 程序计数器(PC):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,啦选取吓一跳需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能,都需要依赖这个计数器来完成
  • java虚拟机栈(Java Virtual Machine Stacks):用于存储局部变量表,操作数栈,动态链接,方法出口等信息
  • 本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务Java方法的,而本地方法栈是为虚拟机调用Native方法服务的
  • Java堆(Java Heap):java虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存
  • 方法区(Methed Area):用于存储已被虚拟机加载的类信息,常量,静态变量,即使编译后的代码等数据

Java内存分配

  • 寄存器:我们无法控制
  • 静态域:static定义的静态成员
  • 常量池:编译时被确定保存在.class文件中的(final)常量值和一些文本修饰的符号引用(类和接口的全限定名,字段的名称和描述符,方法和名称和描述符)
  • 非RAM存储:硬盘等永久存储空间
  • 堆内存:new创建的对象和数组,由java虚拟机自动垃圾回收器管理,存取速度很慢
  • 栈内存:基本类型的变量和对象的引用变量(堆内存空间的访问地址),速度快,可以共享,但是大小与生存期必须确定,缺乏灵活性

说一下JVM调优的工具?

JDK自带了很多监控工具,都位于JDK的bin目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具

  • jconsole:用于对JVM中的内存,线程和类等进行监控
  • jvisualvm:JDK自带的全能分析工具,可以分析:内存快照,线程快照,程序死锁,监控内存的变化,gc的变化等

什么是类加载?

虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的 class 对象

什么是类加载器,类加载器有哪些?

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器
主要有以下四种类加载器:

  • 启动类加载器(Bootstrap ClassLoader)用来加载Java核心类库,无法被Java程序直接引用
  • 扩展类加载器(extensions classloader):它用来加载Java的扩展库,Java虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载java类
  • 系统类加载器(System classloader):它根据java应用的类路径(CLASSPATH)来加载Java类,一般来说java应用的类都是由它来加载完成的,可以通过ClassLoader.getSystemClassLoader()来获取它
  • 用户自定义类加载器,通过集成java,lang.ClassLoader类的方式实现

类加载器双亲委派模型机制?

当一个类收到了类加载请求,不会自己先去加载这个类,而是将其委派给父类,有父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载

Java类加载过程?

编译 》加载 》验证 》准备 》解析 》初始化

编译:将java代码编译为字节码文件

加载:查找并通过io操作读入字节码文件,在内存中生出一个代表类的class对象,作为访问方法区的输入入口,使用到类的时候才会加载

验证:字节码的校验,是否正确

准备:给类的静态变量分配内存,并赋予默认值

解析:将符号引用替换为直接引用,静态链接过程

初始化:对类的静态遍历,初始化指定值,执行静态代码块

什么是GC,为什么要有GC?

GC是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至奔溃,java提供的gc功能可以自动检测对象是否超过作用域从而达到自动回收内存的目的,java语言没有提供释放已分配内存的显示操作方法

简述java垃圾回收机制

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自动执行,在jvm中,有一个垃圾回收线程,它是低优先级的在正常情况下不会指定的,只有一个虚拟机空闲或者当前堆内存不足才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到回收的集合中,进行回收

垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址,大小以及使用情况,通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象,通过这种方式确定那些对象是可达的那些对象是不可达的,当gc确定一些对象为不可达的时候,gc就有责任回收这些内存空间,程序员可以手动执行System,gc(),通知gc运行,但是java语言规范并不保证gc一定会执行

System.gc()和Runtime.gc()会做什么事情?

这两个方法用来提示JVM要进行垃圾回收,但是立即开始还是延迟进行垃圾回收是取决于JVM的

如果对象的人引用被置为null,垃圾回收器是否会立即释放对象占用的内存

不会,在下一个垃圾回收周期中,这个对象是可被回收的

什么是分布式垃圾回收(DGC)?它是如何工作的?

DGC叫做分布式垃圾回收,rmi使用dgc来做自动垃圾回收,因为rmi包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的,DGC使用引用技术算法来给远程对象提供自动内存管理

串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

吞吐量版本使用并行版本的新生代收集器,它用于中等规模和大规模数据的应用程序,而串行收集器对大对数的小应用(在现在处理器上需要大概100M左右的内存)就足够了

在java中,对象时候可以被垃圾回收?

当对象对当前使用这个兑现的应用程序变得不可触及的时候,这个对象就可以被回收了

简述java内存分配与回收速率以及MinorGC和MajorGc

对象优先在堆的Eden区分配
大对象直接进入老年代
长期存活的对象直接进入老年代
当Eden区没有足够的空间进行分配时,虚拟机会执行一次MinorGC,MinorGC通常发生在新生代的Eden区,在这个区的对象生存周期短,往往GC的频率较高,回收速度比较快,FullGC/MajorGC发生在老年代,一般情况下,触发老年代GC的时候不会触发MinorGC,但是可以通过配置,可以在FullGC之前进行一次MinorGC这样可以加快老年代的回收速度

JVM的永久代中会发生垃圾回收吗?

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(FullGC)
注:java8中已经移除了永久代,新佳乐一个叫做元数据区的native内存区

堆和栈的区别?

功能方面:堆是用来存放对象的,栈是用来执行程序的
共享性:堆是线程共享的,栈是线程私有的
空间大小:堆大小远远大于栈

堆中存什么?栈中存什么?

堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是可以动态变化的,但是在栈中,一个对象只对应了一个4btye的引用(堆栈分离的好处)。

为什么不把基本类型放堆中呢?因为其占用的空间一般是1~8个字节——需要空间比较少,而且因为是基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有什么意义的。可以这么说,基本类型和对象的引用都是存放在栈中,而且都是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本身就有所区别了,因为一个是栈中的数据一个是堆中的数据。

Java中的参数传递时传值呢?还是传引用?

要说明这个问题,先要明确两点:

  1. 不要试图与C进行类比,Java中没有指针的概念
  2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象本身。

明确以上两点后。Java在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以参考C的传值调用)。因此,很多书里面都说Java是进行传值调用,这点没有问题,而且也简化的C中复杂性。

但是传引用的错觉是如何造成的呢?在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,如果是传引用的方法调用,也同时可以理解为“传引用值”的传值调用,即引用的处理跟基本类型是完全一样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的是堆中的数据。所以这个修改是可以保持的了。对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面的所有内容。堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得Java的垃圾回收成为可能。
Java中,栈的大小通过-Xss来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现java.lang.StackOverflowError异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息都是方法返回的记录点

队列和栈是什么?有什么区别?

队列和栈都是用来预存储数据的
队列雨荨先进先出检索元素,但也有例外的情况,Deque接口云讯从两端检索元素
栈和队列很相似,但是它运行对元素进行先进后出的检索

怎么判断对象是否可以被回收?

  • 引用计数法:为每个对象创建一个引用计数,有对象引用时计数器+1,应用被释放时计数-1,当计数器为0时就可以被回收,它有一个缺点不能解决循环引用的问题
  • 可达性分析:从GCRoots开始向下搜索,搜索所有的路径称之为引用链,当一个对象到GCRoots没有任何引用链相连接时,则证明此对象是可以被回收的

简述分带垃圾收集器是怎么工作的?

分代回收期有两个分区:

java中会存在内存泄漏吗,请简单描述

所谓内存泄漏就是指一个不再被程序使用的对象或变量一直被占据在内存中,java中有垃圾回收机制,它可以保证对象不再被引用的时候,即对象变成孤儿的时候,对象将自动被垃圾回收器从内存中清楚点

由于java使用有向图的方式进行垃圾回收,可以消除引用循环依赖的问题,例如有两个对象,相互引用,只要他们和进程不可达的,那么gc也是可以回收它们的

浅拷贝和深拷贝

简单来说就是复制,克隆
Person p = new Person(“张三”)
浅拷贝就是对对象中的数据成员进行简单复制,如果存在动态成员或者指针就会报错,深拷贝就是对对象中存在的动态成员或指针重新开辟内存空间

浅克隆:当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
深克隆:除了对象本身被复制外,对象所包含的所有成员变量也将复制。

JVM内存参数

  • -Xms 最小堆内存(包括新生代和老年代)

  • -Xmx 最大对内存(包括新生代和老年代)

  • 通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好

  • -XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制

  • -Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等

  • 保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同

  • -XX:NewRatio=2:1 表示老年代占两份,新生代占一份

  • -XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份

  • class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制

  • non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)

  • class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制

注意:

  • 这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启

  • 如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起

  • 否则,分成三个区域

    • non-nmethods - JVM 自己用的代码
    • profiled nmethods - 部分优化的机器码
    • non-profiled nmethods - 完全优化的机器码

JVM垃圾回收

JVM垃圾回收算法

标记清除法
在这里插入图片描述

解释:

  1. 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
  2. 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
  3. 清除阶段:释放未加标记的对象占用的内存

要点:

  • 标记速度与存活对象线性关系
  • 清除速度与内存大小线性关系
  • 缺点是会产生内存碎片

标记整理法
在这里插入图片描述
解释:

  1. 前面的标记阶段、清理阶段与标记清除法类似
  2. 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生

特点:

  • 标记速度与存活对象线性关系
  • 清除与整理速度与内存大小成线性关系
  • 缺点是性能上较慢

标记复制法
在这里插入图片描述
解释:

  1. 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
  2. 标记阶段与前面的算法类似
  3. 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
  4. 复制完成后,交换 from 和 to 的位置即可

特点:

  • 标记与复制速度与存活对象成线性关系
  • 缺点是会占用成倍的空间

GC 与分代回收算法

GC

GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度

GC 要点:

  • 回收区域是堆内存,不包括虚拟机栈
  • 判断无用对象,使用可达性分析算法三色标记法标记存活对象,回收未标记对象
  • GC 具体的实现称为垃圾回收器
  • GC 大都采用了分代回收思想
    • 理论依据是大部分对象朝生夕灭,用完立刻就可以回收,另有少部分对象会长时间存活,每次很难回收
    • 根据这两类对象的特性将回收区域分为新生代老年代,新生代采用标记复制法、老年代一般采用标记整理法
  • 根据 GC 的规模可以分成 Minor GCMixed GCFull GC
垃圾回收

伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代,
在这里插入图片描述
当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
在这里插入图片描述
将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
在这里插入图片描述
将 from 和 to 交换位置
在这里插入图片描述
经过一段时间后伊甸园的内存又出现不足
在这里插入图片描述
标记伊甸园与 from(现阶段没有)的存活对象
在这里插入图片描述
将存活对象采用复制算法复制到 to 中
在这里插入图片描述
在这里插入图片描述
复制完毕后,伊甸园和 from 内存都得到释放
在这里插入图片描述
将 from 和 to 交换位置
在这里插入图片描述
老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)

垃圾回收器 - Parallel GC

  • eden 内存不足发生 Minor GC,采用标记复制算法,需要暂停用户线程
  • old 内存不足发生 Full GC,采用标记整理算法,需要暂停用户线程
  • 注重吞吐量

垃圾回收器 - ConcurrentMarkSweep GC

  • 它是工作在 old 老年代,支持并发标记的一款回收器,采用并发清除算法
    • 并发标记时不需暂停用户线程
    • 重新标记时仍需暂停用户线程
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
  • 注重响应时间

垃圾回收器 - G1 GC

  • 响应时间与吞吐量兼顾
  • 划分成多个区域,每个区域都可以充当 eden,survivor,old, humongous,其中 humongous 专为大对象准备
  • 分成三个阶段:新生代回收、并发标记、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 Full GC
GC的规模
  • Minor GC 发生在新生代的垃圾回收,暂停时间短

  • Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有

  • Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免

三色标记

即用三种颜色记录对象的标记状态

  • 黑色 – 已标记
  • 灰色 – 标记中
  • 白色 – 还未标记

起始的三个对象还未处理完成,用灰色表示
在这里插入图片描述

该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
在这里插入图片描述
依次类推
在这里插入图片描述
沿着引用链都标记了一遍
在这里插入图片描述
最后为标记的白色对象,即为垃圾
在这里插入图片描述

并发漏标问题

比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:

如图所示标记工作尚未完成
在这里插入图片描述
用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾
在这里插入图片描述
但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标
在这里插入图片描述
如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题
在这里插入图片描述
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:

  1. Incremental Update 增量更新法,CMS 垃圾回收器采用
    • 思路是拦截每次赋值动作,只要赋值发生,被赋值的对象就会被记录下来,在重新标记阶段再确认一遍
  2. Snapshot At The Beginning,SATB 原始快照法,G1 垃圾回收器采用
    • 思路也是拦截每次赋值动作,不过记录的对象不同,也需要在重新标记阶段对这些对象二次处理
    • 新加对象会被记录
    • 被删除引用关系的对象也被记录

内存溢出

误用线程池导致的内存溢出
查询数据量太大导致的内存溢出
动态生成类导致的内存溢出

类加载

类加载过程的三个阶段

  1. 加载
    1. 将类的字节码载入方法区,并创建类.class 对象
    2. 如果此类的父类没有加载,先加载父类
    3. 加载是懒惰执行
  2. 链接
    1. 验证 – 验证类是否符合 Class 规范,合法性、安全性检查
    2. 准备 – 为 static 变量分配空间,设置默认值
    3. 解析 – 将常量池的符号引用解析为直接引用
  3. 初始化
    1. 静态代码块、static 修饰的变量赋值、static final 修饰的引用类型变量赋值,会被合并成一个 <cinit> 方法,在初始化时被调用
    2. static final 修饰的基本类型变量赋值,在链接阶段就已完成
    3. 初始化是懒惰执行

内存结构

JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。

  • 执行 javac 命令编译源代码为字节码
  • 执行 java 命令
    1. 创建 JVM,调用类加载子系统加载 class,将类的信息存入方法区
    2. 创建 main 线程,使用的内存区域是 JVM 虚拟机栈,开始执行 main 方法代码
    3. 如果遇到了未见过的类,会继续触发类加载过程,同样会存入方法区
    4. 需要创建对象,会使用内存来存储对象
    5. 不再使用的对象,会由垃圾回收器在内存不足时回收其内存
    6. 调用方法时,方法内的局部变量、方法参数所使用的是 JVM 虚拟机栈中的栈帧内存
    7. 调用方法时,先要到方法区获得到该方法的字节码指令,由解释器将字节码指令解释为机器码执行
    8. 调用方法时,会将要执行的指令行号读到程序计数器,这样当发生了线程切换,恢复时就可以从中断的位置继续
    9. 对于非 java 实现的方法调用,使用内存称为本地方法栈(见说明)
    10. 对于热点方法调用,或者频繁的循环代码,由 JIT 即时编译器将这些代码编译成机器码缓存,提高执行性能

说明

  • 加粗字体代表了 JVM 虚拟机组件
  • 对于 Oracle 的 Hotspot 虚拟机实现,不区分虚拟机栈和本地方法栈

会发生内存溢出的区域

  • 不会出现内存溢出的区域 – 程序计数器
  • 出现 OutOfMemoryError 的情况
    • 堆内存耗尽 – 对象越来越多,又一直在使用,不能被垃圾回收
    • 方法区内存耗尽 – 加载的类越来越多,很多框架都会在运行期间动态产生新的类
    • 虚拟机栈累积 – 每个线程最多会占用 1 M 内存,线程个数越来越多,而又长时间运行不销毁时
  • 出现 StackOverflowError 的区域
    • JVM 虚拟机栈,原因有方法递归调用未正确结束、反序列化 json 时循环引用

方法区、永久代、元空间

  • 方法区是 JVM 规范中定义的一块内存区域,用来存储类元数据、方法字节码、即时编译器需要的信息等
  • 永久代是 Hotspot 虚拟机对 JVM 规范的实现(1.8 之前)
  • 元空间是 Hotspot 虚拟机对 JVM 规范的另一种实现(1.8 以后),使用本地内存作为这些信息的存储空间

.class字节码文件通过jvm解释生成特定机器码
在这里插入图片描述
在这里插入图片描述

JVM内存区域(运行时数据区)

在这里插入图片描述
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。

程序计数器

一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。

虚拟机栈(栈线程私有)

是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法区(线程私有)

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用
  • 本地方法是使用C语言实现的
  • 本地方法区和** Java Stack 作用类似**, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 VM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
  • 虚拟机栈为虚拟机执⾏ Java ⽅法 (也就是字节码)服务,⽽本地⽅法栈则为虚拟机使⽤到的 Native ⽅法服务。

堆(Heap-线程共享)-运行时数据区

是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。

方法区/永久代(线程共享)

永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区
⽅法区是 Java 虚拟机规范中的定义,是⼀种规范,而永久代是⼀种实现,⼀个是标准⼀个是实现

JVM运行时内存(堆)

Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代
在这里插入图片描述

新生代

由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区(伊甸园区)、ServivorFrom、ServivorTo 三个区。

Eden区

Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。

ServivorFrom

上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

ServivorTo

保留了一次 MinorGC 过程中的幸存者。

MinorGC 采用复制算法。
谁空谁是TO

老年代

主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。

MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

Java是否需要开发人员回收内存垃圾?

大多数情况下是不需要的,Java提供了一个系统级的线程来跟踪内存分配,不再使用的内存区将会自动回收

方法区的作用是什么?

方法区用于存储被虚拟机加载的类型信息,常量,静态变量,集市编译器编译后的代码缓存等数据
JDK8之前使用永生代实现方法区,容易内存溢出,因为永生代有上线,集市不设置也有默认大小,JDK7把永生代的字符串常量池,静态变量等移除,JDK8中永生代完全废弃,改用在本地内存中实现的元空间代替,把JDK7中永久代剩余内容(主要是类型信息)全部移到元空间

虚拟机贵方对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收,垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载,如果方法区无法满足新的内存分配需求,将抛出OOM

永久代

指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

JAVA8与元数据

在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存

垃圾回收与算法

引用计数法

在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

可达性分析

通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

哪些对象可以被看做是 GC Roots 呢?

1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
2)方法区中的类静态属性引用的对象,常量引用的对象;
3)本地方法栈中 JNI(Native 方法)引用的对象;

分代收集算法

JAVA引用类型

强引用

在 Java 中最常见的就是强引用,把==一个对象赋给一个引用变量,这个引用变量就是一个强引用。==当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
相当于必不可少对的生活物品

软引用

软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
相当于可有可无的生活物品

弱引用

弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
相当于可有可无的生活物品

虚引用

虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。

JVM类加载机制

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。

JAVA对象的创建过程

  1. 类加载检查
  2. 分配内存
  3. 初始化零值
  4. 设置对象头
  5. 执行init方法

类加载器

启动类加载器(Bootstrap ClassLoader)

  1. 负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
    扩展类加载器(Extension ClassLoader)
  2. 负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
    应用程序类加载器(Application ClassLoader)
  3. 负责加载用户路径(classpath)上的类库。JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
    在这里插入图片描述

双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
在这里插入图片描述

为什么需要双亲委派模型?

为了防止内存中出现多个相同的字节码,因为如果没有双亲委派的话,用户就可以自己定义一个 java.lang.String 类,那么就无法保证类的唯一性;

计算机基础知识

计算机网络系统

HTTP 有哪些问题,加密算法有哪些,针对不同加密方式可能产生的问题,及其 HTTPS 是如何保证安全传输的?

HTTP 的不足:通信使用明文,内容可能会被窃听;不验证通信方的身份,因此有可能遭遇伪装; 无法证明报文的完整性,有可能已遭篡改
常用加密算法:MD5 算法、DES 算法、AES 算法、RSA 算法

http 版本之间的比较

http0.9:
最初的 http 版本,仅支持 get 方法,只能传输纯文本内容,所以请求结束服务段会给客户端返回一个 HTML 格式的字符串,然后由浏览器自己渲染。
http0.9 是典型的无状态连接(无状态是指协议对于事务处理没有记忆功能,对同一个 url请求没有上下文关系,每次的请求都是独立的,服务器中没有保存客户端的状态)

http1.0:
这个版本后任何文件形式都可以被传输,本质上支持长连接,但是默认还是短连接,增加了 keep-alive 关键字来由短链接变成长连接。HTTP 的请求和回应格式也发生了变化,除了要传输的数据之外,每次通信都包含头信息,用来描述一些信息。还增加了状态码(status code)、多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。

http1.1:
HTTP1.1 最大的变化就是引入了长链接,也就是 TCP 链接默认是不关闭的可以被多个请求复用。客户端或者服务器如果长时间发现对方没有活动就会关闭链接,但是规范的做法是客户端在最后一个请求的时候要求服务器关闭链接。对于同一个域名,目前浏览器支持建立 6 个长链接。
节约带宽,HTTP1.1 支持只发送 header 头信息不带任何 body 信息,如果服务器认为客户端有权限请求指定数据那就返回 100,没有就返回 401,当客户端收到 100 的时候可以才把要请求的信息发给服务器。并且 1.1 还支持了请求部分内容,如果当前客户端已经有一部分资源了,只需要向服务器请求另外的部分资源即可,这也是支持文件断点续传的基础。
1.1 版本中增加了 host 处理,在 HTTP1.0 中认为每台服务器都绑定一个唯一的 ip 地址,因此在 URL 中并没有传递主机名,但是随着虚拟机技术的发展,可能在一台物理机器上存在多个虚拟主机,并且他们共享了一个 ip 地址,http1.1 中请求消息和响应消息都支持 host头域,如果不存在还会报出错误。

http2.0:
多路复用:在一个连接里面并发处理请求,不像 http1.1 在一个 tcp 连接中各个请求是串行的,花销很大。在 1.0 版本后增加了 header 头信息,2.0 版本通过算法把 header 进行了压缩这样数据体积就更小,在网络上传输就更快。服务端有了推送功能,将客户端感兴趣的东西推给客户端,当客户端请求这些时,直接去缓存中取就行。

TCP 第四次挥手后为什么要等待 2MSL 后才断开链接?等待时间为什么是 2MSL?

1.为了保证客户端最后一次挥手的报文能够到达服务器,若第 4 次挥手的报文段丢失了,服务器就会超时重传第 3 次挥手的报文段,所以客户端此时不是直接进入 CLOSED,而是保持 TIME_WAIT(等待 2MSL 就是 TIME_WAIT)。当客户端再次收到服务器因为超时重传而发送的第 3 次挥手的请求时,客户端就会重新给服务器发送第 4 次挥手的报文(保证服务器能够收到客户端的回应报文)。最后,客户端、服务器才真正断开连接。说白了,等待 2MSL 就是为了确保服务器能够受到客户端最后的回应。
2.如果客户端直接 CLOSED,然后又再次向服务器发起一个新连接,谁也不能保证新发起的连接和刚关闭的连接的端口号是不同的,有可能新、老连接的端口号就是一样的。假设新、老连接端口号一致,若老连接的一些数据仍滞留在网络中,这些滞留数据在新连接建立后才到达服务器,鉴于前后端口号一致,TCP 协议就默认这些数据属于新连接,于是数据就这样乱成一锅粥了。所以 TCP 连接还要在 TIME_WAIT 状态下等待 2MSL,确保所有老连接的数据都在网络中消失!
3.首先说明什么是 MSL,MSL 是 Maximum Segment Lifetime 的缩写,译为报文最大生存时间,也就是任何报文在网络上存活的最大时间,一旦超过该时间,报文就会被丢弃。

2MSL 也就是指的 2 倍 MSL 的时间。 为什么是 2 倍呢?
主动断开的一侧为 A,被动断开的一侧为 B。
第一个消息:A 发 FIN
第二个消息:B 回复 ACK
第三个消息:B 发出 FIN 此时此刻:B 单方面认为自己与 A 达成了共识,即双方都同意关闭连接。此时,B 能释放这个 TCP 连接占用的内存资源吗?不能,B 一定要确保 A 收到自己的 ACK、FIN。所以 B 需要静静地等待 A 的第四个消息的到来:
第四个消息:A 发出 ACK,用于确认收到 B 的 FIN当 B 接收到此消息,即认为双方达成了同步:双方都知道连接可以释放了,此时 B 可以安全地释放此 TCP 连接所占用的内存资源、端口号。所以被动关闭的 B 无需任何 wait time,直接释放资源。但,A 并不知道 B 是否接到自己的 ACK,A 是这么想的:
1)如果 B没有收到自己的 ACK,会超时重传 FiN那么 A再次接到重传的 FIN,会再次发送 ACK
2)如果 B收到自己的 ACK,也不会再发任何消息,包括 ACK无论是 1还是 2,A都需要等
待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:去向 ACK消息最大存活时间(MSL) + 来向 FIN消息的最大存活时间(MSL)。这恰恰就是2MSL( Maximum Segment Life)。等待 2MSL时间,A就可以放心地释放 TCP占用的资源、端口号,此时可以使用该端口号连接任何服务器。同时也能保证网络中老的链接全部消失。

HTTP2 新特性?

减少头部的体积、添加请求优先级、服务器推送、多路复用。

域名配置中cname和a记录的作用是?

一个http请求基本流程
客户端通过发起域名资源请求 -> DNS解析获得IP -> 寻找服务器获得资源
域名和ip的关系,DNS作用
DNS:Domain Name Server 域名服务器 域名虽然便于人们记忆,但网络中的计算机之间只能互相认识IP地址,它们之间的转换工作称为域名解析,域名解析需要由专门的域名解析服务器来完成,DNS 就是进行域名解析的服务器

什么是cname和a记录
a记录:用户可以在此设置域名并指向到自己的目标主机地址上,从而实现通过域名找到服务器(也叫ip指向域名配置)
cname:别名指向,可以为一个主机设置别名。比如设置open1024.com,用来指向一个主机
baidu.com那么以后就可以用open1024.com来代替访问 baidu.com了

Http状态码里面的1xx/2xx/3xx/4xx/5xx主要应用场景是?

浏览器向服务器请求时,服务端响应的消息头里面有状态码,表示请求结果的状态
分类
1XX: 收到请求,需要请求者继续执行操作,比较少用
2XX: 请求成功,常用的 200
3XX: 重定向,浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取;
好处:网站改版、域名迁移等,多个域名指向同个主站导流
301:永久性跳转,比如域名过期,换个域名
302:临时性跳转
4XX: 客服端出错,请求包含语法错误或者无法完成请求
400: 请求出错,比如语法协议
403: 没权限访问
404: 找不到这个路径对应的接口或者文件
405: 不允许此方法进行提交,Method not allowed,比如接口一定要POST方式,而你是用了GET
5XX: 服务端出错,服务器在处理请求的过程中发生了错误
500: 服务器内部报错了,完成不了这次请求
503: 服务器宕机

http 响应码 301 和 302 代表的是什么?有什么区别?

301:永久重定向。
302:暂时重定向。
它们的区别是,301 对搜索引擎优化(SEO)更加有利;302 有被提示为网络拦截的风险。

forward 和 redirect 的区别?

forward 是转发 和 redirect 是重定向:
地址栏 url 显示:foward url 不会发生改变,redirect url 会发生改变;
数据共享:forward 可以共享 request 里的数据,redirect 不能共享;
效率:forward 比 redirect 效率高。

简述 tcp 和 udp 的区别?

tcp 和 udp 是 OSI 模型中的运输层中的协议。tcp 提供可靠的通信传输,而 udp 则常被用于让广播和细节控制交给应用的通信传输。
两者的区别大致如下:
tcp 面向连接,udp 面向非连接即发送数据前不需要建立链接;
tcp 提供可靠的服务(数据传输),udp 无法保证;
tcp 面向字节流,udp 面向报文;
tcp 数据传输慢,udp 数据传输快;

tcp 为什么要三次握手,两次不行吗?为什么?

如果采用两次握手,那么只要服务器发出确认数据包就会建立连接,但由于客户端此时并未响应服务器端的请求,那此时服务器端就会一直在等待客户端,这样服务器端就白白浪费了一定的资源。若采用三次握手,服务器端没有收到来自客户端的再此确认,则就会知道客户端并没有要求建立请求,就不会浪费服务器的资源。

说一下 tcp 粘包是怎么产生的?

tcp 粘包可能发生在发送端或者接收端,分别来看两端各种产生粘包的原因:
发送端粘包:发送端需要等缓冲区满才发送出去,造成粘包;
接收方粘包:接收方不及时接收缓冲区的包,造成多个包接收。

HTTP 短连接怎么变成长连接?

在 header 中加入 --Connection:keep-alive。

OSI

物理层:传输比特流,比特,网卡工作层
数据链路层:如何格式化数据以进行传输,差错检测,保证数据传输的可靠性,帧,交换机
网络层:路由器,数据包
传输层:数据间传输,TCP和UDP,分段
会话层:
表示层:
应用层:

TCP/IP

在这里插入图片描述

Http

常见的Http Method有哪些,使用场景分别是?

http1.0定义了三种:
GET: 向服务器获取资源,比如常见的查询请求
POST: 向服务器提交数据而发送的请求
Head: 和get类似,返回的响应中没有具体的内容,用于获取报头

http1.1定义了六种
PUT:一般是用于更新请求,比如更新个人信息、商品信息全量更新
PATCH:PUT 方法的补充,更新指定资源的部分数据
DELETE:用于删除指定的资源
OPTIONS: 获取服务器支持的HTTP请求方法,服务器性能、跨域检查等
CONNECT: 方法的作用就是把服务器作为跳板,让服务器代替用户去访问其它网页,之后把数据原原本本的返回给用户,网页开发基本不用这个方法,如果是http代理就会使用这个,让服务器代理用户去访问其他网页,类似中介
TRACE:回显服务器收到的请求,主要用于测试或诊断

常见http状态码解析

浏览器向服务器请求时,服务端响应的消息头里面有状态码,表示请求结果的状态

分类
1XX: 收到请求,需要请求者继续执行操作,比较少用

2XX: 请求成功,常用的 200

3XX: 重定向,浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取;

好处:网站改版、域名迁移等,多个域名指向同个主站导流
必须记住: 301:永久性跳转,比如域名过期,换个域名 302:临时性跳转

4XX: 客服端出错,请求包含语法错误或者无法完成请求
必须记住:
400: 请求出错,比如语法协议
403: 没权限访问
404: 找不到这个路径对应的接口或者文件
405: 不允许此方法进行提交,Method not allowed,比如接口一定要POST方式,而你是用了GET

5XX: 服务端出错,服务器在处理请求的过程中发生了错误
必须记住:
500: 服务器内部报错了,完成不了这次请求
503: 服务器宕机

Cookie和Session

说下Cookie和Session的区别和联系
cookie数据保存在客户端,session数据保存在服务端
cookie不是很安全,容易泄露,不能直接明文存储信息
Cookie大小和数量存储有限制

你们公司C端业务登录的是怎样做的(业务量大,集群部署)
部分业务是采用redis替代本身的tomcat单机session (业务需要高度可控)
还有其他业务是使用JSON Web token (C端普通业务)

说下常用浏览器输入一个url到用户看到结果,中间经过哪些流程

1、浏览器输入url, 解析url地址是否合法
​2、浏览器检查是否有缓存, 如果有直接显示。如果没有跳到第三步。
​3、在发送http请求前,需要域名解析(DNS解析),解析获取对应过的ip地址。
​4、浏览器向服务器发起tcp链接,完成tcp三次握手
​5、握手成功后,浏览器向服务器发送http请求
​6、服务器收到处理的请求,将数据返回至浏览器
​7、浏览器收到http响应。
​8、浏览器解析响应。如果响应可以缓存,则存入缓存
​9、浏览器进行页面渲染

输入url,解析url是否合法,查找浏览器是否有缓存,有的话直接进行显示,没有的话,进行dns域名解析,拿到ip地址,然后发起tcp连接,三次握手,服务器对请求作出响应,发回数据到浏览器,浏览器收集癖到jttp响应,进行解析和进行数据渲染。

浏览器同源策略

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。
由Netscape提出的一个著名的安全策略。
当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面
当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,
即检查是否同源,只有和百度同源的脚本才会被执行。
如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发
出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收

为什么会出现跨域,有什么常见的解决方案
跨域:浏览器同源策略 1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。 最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"
协议相同 http https
域名相同 www.baidu.com
端口相同 80 81

一句话:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域

浏览器控制台跨域提示:
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
解决方法

  • JSONP
  • 页面这层再包装一层服务,目前最多就是nodejs
  • Http响应头配置允许跨域
  • nginx代理服务器
  • 后端程序代码配置

程序代码中处理 SpringBoot 通过拦截器配置
//表示接受任意域名的请求,也可以指定域名
response.setHeader(“Access-Control-Allow-Origin”, request.getHeader(“origin”));
//该字段可选,是个布尔值,表示是否可以携带cookie
response.setHeader(“Access-Control-Allow-Credentials”, “true”);
response.setHeader(“Access-Control-Allow-Methods”, “GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS”);
response.setHeader(“Access-Control-Allow-Headers”, “*”);

在公司上班时,时常因为内网与外网之间的切换烦恼,该如何解决?

公司来了一位新的女同事,不懂网络,领导想让你帮她的笔记本电脑设置一下网络,使得能够内外网通,请问怎么设置?
备注:
公司内网子网192.168.43.0
剩余可分配ip地址 192.168.43.30 ~ 192.168.43.60
内网网关ip地址:192.168.43.1
外网路由器的子网 192.168.1.0
网关ip地址:192.168.190.1

答案:
第一步:先设置有线网卡的ip地址与网关,ip设置为192.168.43.30 ~ 192.168.43.60中的其中一个,网关192.168.43.1,子网掩码:255.255.255.0
第二步:无线网卡的ip地址设置自动获取ip即可
第三步:route delete 0.0.0.0 “删除所有0.0.0.0的路由”
第四步:route add -p 0.0.0.0 mask 0.0.0.0 192.168.1.1 “添加0.0.0.0网络路由”
第五步:route add -p 192.168.43.0 mask 255.255.255.0 192.168.43.1 “添加192.168.43.0网络路由”

操作系统

什么是线程死锁?死锁如何产生?如何避免线程死锁?

死锁的介绍:
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程进入对象的 synchronized 代码块时,便占有了资源,直到它退出该代码块或者调用 wait 方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

死锁的产生的一些特定条件:
互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 。
请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
不可剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用。
循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

如何避免:

  1. 加锁顺序:当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。
  2. 加锁时限:加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。
  3. 死锁检测:死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph 等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实
    现按序加锁并且锁超时也不可行的场景。

算法

数据库

MySQL

MySQL 中有哪几种锁?

1、表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
2、行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
3、页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

sql 语句优化会不会,说出你知道的?

(1)避免在列上做运算,可能会导致索引失败
(2)使用 join 时应该小结果集驱动大结果集,同时把复杂的 join 查询拆分成多个 query,不然 join 越多表,会导致越多的锁定和堵塞。
(3)注意 like 模糊查询的使用,避免使用%%
(4)不要使用 select * 节省内存
(5)使用批量插入语句,节省交互
(6)Limit 基数比较大时,使用 between and
(7)不要使用 rand 函数随机获取记录
(8)避免使用 null,建表时,尽量设置 not nul,提高查询性能
(9)不要使用 count(id),应该使用 count(*)
(10)不要做无谓的排序,尽可能在索引中完成排序
(11)From 语句中一定不要使用子查询
(12)使用更多的 where 加以限制,缩小查找范围
(13)合理运用索引
(14)使用 explain 查看 sql 性能

MySQL 优化经验

对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。
任何地方都不要使用 select * from t ,用具体的字段列表代替 *,不要返回用不到的任何字段。
避免频繁创建和删除临时表,以减少系统表资源的消耗。

数据库三大范式

第一范式:列都是不可再分的
第二范式:每个表只描述一件事情
第三范式:不存在对非主键列的传递依赖

MySQL事务四大特性

原子性Atomicity: 一个事务必须被事务不可分割的最小工作单元,整个操作要么全部成功,要么全部失败,一般就是通过commit和rollback来控制
一致性Consistency: 数据库总能从一个一致性的状态转换到另一个一致性的状态,只要有任何一方发生异常就不会成功提交事务,比如下单支付成功后,开通视频播放权限
隔离性Isolation: 一个事务相对于另一个事务是隔离的,一个事务所做的修改是在最终提交以前,对其他事务是不可见的
持久性Durability:==一旦事务提交,则其所做的修改就会永久保存到数据库中。==此时即使系统崩溃,修改的数据也不会丢失

脏读、不可重复读、幻读

脏读: 事务中的修改即使没有提交,其他事务也能看见,事务可以读到未提交的数据称为脏读
不可重复读: 同个事务前后多次读取,不能读到相同的数据内容,中间另一个事务也操作了该同一数据
幻读当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,发现两次不一样,产生幻读

幻读和不可重复读的区别是:幻读是一个范围,不可重复读是本身,从总的结果来看, 两者都表现为两次读取的结果不一致

事务隔离级别

事务的隔离级别越高,事务越安全,但是并发能力越差。
Read Uncommitted(未提交读,读取未提交内容):事务中的修改即使没有提交,其他事务也能看见,事务可以读到为提交的数据称为脏读,也存在不可重复读、幻读问题
例子:一个活动,原价500元的课程,配置成50元,但是事务没提交。你刚好看到那么便宜准备购买,但是马上回滚了事务,重新配置并提交了事务,你准备下单的时候发现价格变回了500元
Read Committed(提交读,读取提交内容):一个事务开始后只能看见已经提交的事务所做的修改,在事务中执行两次同样的查询可能得到不一样的结果,也叫做不可重复读(前后多次读取,不能读到相同的数据内容),也存幻读问题
例子:你有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分,但是女友同时也在别的地方登录,把1000积分兑换了《SpringCloud微服务专题课程》,且在你之前提交事务;当系统帮你兑换《面试专题课程》是发现积分预计没了,兑换失败。
事务A事先读取了数据,事务B紧接了更新了数据且提交了事务,事务A再次读取该数据时,数据已经发生了改变
Repeatable Read(可重复读,mysql默认的事务隔离级别):解决脏读、不可重复读的问题,存在幻读的问题,使用 MMVC机制 实现可重复读
例子:有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分,女友同时也在别的地方登录先兑换了这个《面试专题课程》,事务提交的时候发现存在了,之前读取的没用了,像是幻觉
幻读问题:MySQL的InnoDB引擎通过MVCC自动帮我们解决,即多版本并发控制
Serializable(可串行化):解决脏读、不可重复读、幻读,可保证事务安全,但强制所有事务串行执行,所以并发效率低

Mysql常见的存储引擎

常见的有:InnoDB、MyISAM、MEMORY、MERGE、ARCHIVE、CSV等
一般比较常用的有InnoDB、MyISAM
MySQL 5.5以上的版本默认是InnoDB,5.5之前默认存储引擎是MyISAM

mysql的存储引擎 innodb和myisam区别

区别项 Innodb myisam
事务 支持 不支持
锁粒度 行锁,适合高并发 表锁,不适合高并发
是否默认 默认 非默认
支持外键(物理) 支持外键 不支持
适合场景 读写均衡,写大于读场景,需要事务 读多写少场景,不需要事务
全文索引 不支持,可以通过插件实现, 更多使用ElasticSearch 支持全文索引

是否⽀持事务和崩溃后的安全恢复:
MyISAM 强调的是性能,每次查询具有原⼦性,其执⾏速度⽐InnoDB类型更快,但是不提供事务⽀持。
InnoDB 提供事务⽀持事务,外部键等⾼级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能⼒(crash recoverycapabilities)的事务安全(transaction-safe (ACID compliant))型表。

是否⽀持MVCC :
仅 InnoDB ⽀持。应对⾼并发事务, MVCC⽐单纯的加锁更⾼效;MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下⼯作;MVCC可以使⽤ 乐 观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统⼀。

锁机制与InnoDB锁算法

MyISAM采⽤表级锁(table-level locking)
InnoDB⽀持**⾏级锁(row-level locking)**和表级锁,默认为⾏级锁

表级锁和⾏级锁对⽐:
表级锁: MySQL中锁定 粒度最⼤ 的⼀种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最⼤,触发锁冲突的概率最⾼,并发度最低,MyISAM和 InnoDB引擎都⽀持表级锁。
⾏级锁: MySQL中锁定 粒度最⼩ 的⼀种锁,只针对当前操作的⾏进⾏加锁。 ⾏级锁能⼤⼤减少数据库操作的冲突。其加锁粒度最⼩,并发度⾼,但加锁的开销也最⼤,加锁慢,会出现死锁。

InnoDB存储引擎的锁的算法有三种:

  • Record lock:单个⾏记录上的锁
  • Gap lock:间隙锁,锁定⼀个范围,不包括记录本身
  • Next-key lock:record+gap 锁定⼀个范围,包含记录本身

索引

索引的出现就是为了提高查询的效率,就像一本书的目录。对于一张表来说,索引其实就是它的目录。
MySQL索引使⽤的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余⼤部分场景,建议选择BTree索引。
MySQL的BTree索引使⽤的是B树中的B+Tree,但对于主要的两种存储引擎的实现⽅式是不同的。

  • MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”
    叶节点存的是地址

  • InnoDB: 其数据⽂件本身就是索引⽂件。相⽐MyISAM,索引⽂件和数据⽂件是分离的,其表数据⽂件本身就是按B+Tree组织的⼀个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据⽂件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。⽽其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值⽽不是地址,这也是和MyISAM不同的地⽅。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再⾛⼀遍主索引。 因此,在设计表的时候,不建议使⽤过⻓的字段作为主键,也不建议使⽤⾮单调的字段
    作为主键,这样会造成主索引频繁分裂。
    树节点存的是完整的数据记录

mysql的功能索引

索引名称 特点 创建语句
普通索引 最基本的索引,仅加速查询 CREATE INDEX idx_name ON table_name(filed_name)
唯一索引 加速查询,列值唯一,允许为空;
组合索引则列值的组合必须唯一 CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)
主键索引 加速查询,列值唯一,
一个表只有1个,不允许有空值 ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )
组合索引 加速查询,多条件组合查询 CREATE INDEX idx_name ON table_name(filed_name_1,filed_name_2);
覆盖索引 索引包含所需要的值,不需要“回表”查询,比如查询 两个字段,刚好是 组合索引 的两个字段
全文索引 对内容进行分词搜索,仅可用于Myisam, 更多用ElasticSearch做搜索 ALTER TABLE table_name ADD FULLTEXT ( filed_name )

什么情况下有索引,但用不上?
a)如果条件中有 OR,即使其中有部分条件带索引也不会使用。注意:要想使用 or,又想让索引生效,只能将 or 条件中的每个列都加上索引。
b)对于多了索引,不是使用的第一部分,则不会使用索引。
c)Like 查询以%开头,不使用索引
d)存在索引列的数据类型隐形转换,则用不上索引,比如列类型是 字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
e)Where 子句里对索引列上有数学运算,用不上索引
f)Where 子句中对索引列使用函数,用不上索引
g)Mysql 估计使用全表扫描要比用索引快,不使用索引

什么情况下不推荐使用索引?
a)数据唯一性差的字段不要使用索引
b)频繁更新的字段不要使用索引
c)字段不在 where 语句中出现时不要添加索引,如果 where 后含 IS NULL/IS NOT NULL/LIKE ‘%输入符%’等条件,不要使用索引
d)Where 子句里对索引使用不等于(<>),不建议使用索引,效果一般

mysql 优化会不会,mycat 分库,垂直分库,水平分库?

(1)为查询缓存优化你的查询
(2)EXPLAIN select 查询:explain 的查询结果会告诉你索引主键是如何被利用的
(3)只需要一行数据时使用 limit1
(4)为搜索字段添加索引
(5)在关联表的时候使用相当类型的例,并将其索引
(6)千万不要 ORDER BY RAND()
(7)避免 select*
(8)永远为每张表设置一个 ID
(9)使用 ENUM 而不是 VARCHAR
(10)从 PROCEDURE ANALYS()提取建议
(11)尽可能的使用 NOT NULL
(12)Java 中使用 Prepared Statements
(13)无缓冲的查询
(14)把 IP 地址存成 UNSIGNED INT
(15)固定表的长度
(16)垂直分库:“垂直分割”是一种把数据库中的表按列变成几张表的方法,这样可以降低表的复杂度和字段的数目,从而达到优化的目的。
(17)水平分库:“水平分割”是一种把数据库中的表按行变成几张表的方法,这样可以降低表的复杂度和字段的数目,从而达到优化的目的。
(18)越小的列会越快
(19)选择正确的存储引擎
(20)使用一个对象关系映射器
(21)小心永久链接
(22)拆分大的 DELETE 活 INSERT 语句

你们线上数据量每天有多少新增,都是存储在mysql库吗,有没做优化

中型公司或者业务发展好的公司,一天新增几百万数据量
业务核心数据存储在Mysql里面,针对业务创建合适的索引
打点数据、日志等存储在ElasticSearch或者MongoDB里面

innodb 支持全文索引吗?

5.6 版本之后 InnoDB 存储引擎开始支持全文索引,5.7 版本之后通过使用 ngram 插件开始支持中文。之前仅支持英文,因为是通过空格作为分词的分隔符,对于中文来说是不合适的。MySQL 允许在 char、varchar、text 类型上建立全文索引。

innodb 支持表锁吗?

支持,补充:普通的增删改 是表锁,加入索引的增删改是行锁,执行查询时不加任何锁的。

索引的优缺点

考虑点:结合实际的业务场景,在哪些字段上创建索引,创建什么类型的索引

  • 索引好处:
    快速定位到表的位置,减少服务器扫描的数据
    有些索引存储了实际的值,特定情况下只要使用索引就能完成查询
  • 索引缺点:
    索引会浪费磁盘空间,不要创建非必要的索引
    插入、更新、删除需要维护索引,带来额外的开销
    索引过多,修改表的时候重构索引性能差
  • 索引优化实践
    前缀索引,特别是TEXT和BLOG类型的字段,只检索前面几个字符,提高检索速度
    尽量使用数据量少的索引,索引值过长查询速度会受到影响
    选择合适的索引列顺序
    内容变动少,且查询频繁,可以建立多几个索引
    内容变动频繁,谨慎创建索引
    根据业务创建适合的索引类型,比如某个字段常用来做查询条件,则为这个字段建立索引提高查询速度
    组合索引选择业务查询最相关的字段

查询指令执行顺序

说下执行顺序 select、where、from、group by、having、order by
from 从哪个表查询
where 初步过滤条件
group by 过滤后进行分组[重点]
having 对分组后的数据进行二次过滤[重点]
select 查看哪些结果字段
order by 按照怎样的顺序进行排序返回[重点]
select video_id,count(id) num from chapter group by video_id having num >10 order by video_id desc

大表优化

当MySQL单表记录数过⼤时,数据库的CRUD性能会明显下降,⼀些常见的优化措施如下:
限定数据的范围:务必禁止不带任何限制数据范围条件的查询语句。比如:我们当⽤户在查询订单历史的时候,我们可以控制在⼀个⽉的范围内;

读/写分离:经典的数据库拆分⽅案,主库负责写,从库负责读;

垂直分区:根据数据库⾥⾯数据表的相关性进⾏拆分。 例如,⽤户表中既有⽤户的登录信息⼜有⽤户的基本信息,可以将⽤户表拆分成两个单独的表,甚⾄放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,把⼀张列⽐较多的表拆分为多张表。

在这里插入图片描述

  • 垂直拆分的优点: 可以使得列数据变⼩,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
  • 垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应⽤层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
    水平分区:保持数据表结构不变,通过某种策略存储数据分⽚。这样每⼀⽚数据分散到不同的表或者库中,达到了分布式的⽬的。⽔平拆分可以⽀撑⾮常⼤的数据量。⽔平拆分是指数据表⾏的拆分,表的⾏数超过200万⾏时,就会变慢,这时可以把⼀张的表的数据拆成多张表来存放。
    举个例⼦:我们可以将⽤户信息表拆分成多个⽤户信息表,这样就可以避免单⼀表数据量过⼤对性能造成影响。
    在这里插入图片描述
    ⽔平拆分可以⽀持⾮常⼤的数据量。需要注意的⼀点是:分表仅仅是解决了单⼀表数据过⼤的问题,但由于表的数据还是在同⼀台机器上,其实对于提升MySQL并发能⼒没有什么意义,所以⽔平拆分最好分库 。
    ⽔平拆分能够⽀持⾮常⼤的数据量存储,应⽤端改造也少,但分⽚事务难以解决 ,跨节点Join性能较差,逻辑复杂。尽量不要对数据进⾏分⽚,因为拆分会带来逻辑、部署、运维的各种复杂度 ,⼀般的数据表在优化得当的情况下⽀撑千万以下的数据量是没有太⼤问题的。如果实在要分⽚,尽量选择客户端分⽚架构,这样可以减少⼀次和中间件的⽹络I/O。
    下⾯补充⼀下数据库分⽚的两种常⻅⽅案:
    客户端代理: 分⽚逻辑在应⽤端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当⽹的Sharding-JDBC 、阿⾥的TDDL是两种⽐较常⽤的实现。
    中间件代理: 在应⽤和数据中间加了⼀个代理层。分⽚逻辑统⼀维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、⽹易的DDB等等都是这种架构的实现。

Mysql搭建数据库主从复制,会有同步延迟问题,怎么解决

  • 硬件提升
保证性能第一情况下,不能百分百解决主从同步延迟问题,只能增加缓解措施。现象:主从同步,大数据量场景下,会发现写入主库的数据,在从库没找到。原因:1、主从复制是单线程操作,当主库TPS高,产生的超过从库sql线程执行能力2、从库执行了大的sql操作,阻塞等待3、服务器硬件问题,如磁盘,CPU,还有网络延迟等解决办法:1、业务需要有一定的容忍度,程序和数据库直接增加缓存,降低读压力2、业务适合的话,写入主库后,再写缓存,读的时候可以读缓存,没命中再读从库3、读写分离,一主多从,分散主库和从库压力4、提高硬件配置,比如使用SSD固态硬盘、更好的CPU和网络5、进行分库分表,减少单机压力
  • 业务侧解决,比如核心业务强制走主库,结合缓存使用

Mysql主从复制数据一致性校验方案怎么做

Mysql主从复制是基于binlog复制,难免出现复制数据不一致的风险,引起用户数据访问前后不一致的风险,所以要定期开展主从复制数据一致性的校验并修复,避免这些问题
解决方案之一,使用Percona公司下的工具
pt-table-checksum工具进行一致性校验
原理:
主库利用表中的索引,将表的数据切割成一个个chunk(块),然后进行计算得到checksum值。
从库也执相应的操作,并在从库上计算相同数据块的checksum,然后对比主从中各个表的checksum是否一致并存储到数据库,最后通过存储校验结果的表就可以判断出哪些表的数据不一致
pt-table-sync(在从库执行)工具进行修复不一致数据,可以修复主从结构数据的不一致,也可以修复非主从结构数据表的数据不一致
原理:在主库上执行数据的更改,再同步到从库上,不会直接更改成从的数据。在主库上执行更改是基于主库现在的数据,也不会更改主库上的数据,可以同步某些表或整个库的数据,但它不同步表结构、索引,只同步不一致的数据
注意:
默认主库要检查的表在从库都存在,并且同主库表有相同的表结构
如果表中没有索引,pt-table-checksum将没法处理,一般要求最基本都要有主键索引
pt-table-sync工具会修改数据,使用前最好备份下数据,防止误操作

什么是数据库连接池?为什么需要数据库连接池?

池化设计应该不是⼀个新名词。我们常⻅的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。
这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好⽐你去⻝堂打饭,打饭的⼤妈会先把饭盛好⼏份放那⾥,你来了就直接拿着饭盒加菜即可,不⽤再临时⼜盛饭⼜打菜,效率就⾼了。
除了初始化资源,池化设计还包括如下这些特征:池⼦的初始值、池⼦的活跃值、池⼦的最⼤值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。
数据库连接本质就是⼀个socket的连接。数据库服务端还要维护⼀些缓存和⽤户权限信息之类的,所以占⽤了⼀些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重⽤这些连接。为每个⽤户打开和维护数据库连接,尤其是对动态数据库驱动的⽹站应⽤程序的请求,既昂贵⼜浪费资源。在连接池中,创建连接后,将其放置在池中,并再次使⽤它,因此不必建⽴新的连接。如果使⽤了所有连接,则会建⽴⼀个新连接并将其添加到池中。 连接池还减少了⽤户必须等待建⽴与数据库的连接的时间。

分库分表之后,id 主键如何处理?

因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要⼀个全局唯⼀的id 来⽀持。

⽣成全局 id 有下⾯这⼏种⽅式:

UUID:不适合作为主键,因为太⻓了,并且⽆序不可读,查询效率低。比较适合⽤于⽣成唯⼀的名字的标示⽐如⽂件的名字。
数据库自增 id : 两台数据库分别设置不同步⻓,⽣成不重复ID的策略来实现⾼可⽤。这种⽅式⽣成的 id 有序,但是需要独⽴部署数据库实例,成本⾼,还会有性能瓶颈。
利⽤ redis ⽣成 id : 性能比较好,灵活⽅便,不依赖于数据库。但是,引⼊了新的组件造成系统更加复杂,可⽤性降低,编码更加复杂,增加了系统成本。
Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
美团的Leaf分布式ID生成系统 :Leaf 是美团开源的分布式ID⽣成器,能保证全局唯⼀性、趋势递增、单调递增、信息安全,⾥⾯也提到了⼏种分布式⽅案的对⽐,但也需要依赖关系数据库、Zookeeper等中间件。

MySQL中的varchar和char有什么区别,应该怎么选择

varchar(len) char(len) len存储的是字符
在这里插入图片描述

MySQL中的datetime和timestamp有什么区别

在这里插入图片描述

  • 为什么timestamp只能到2038年
    MySQL的timestamp类型是4个字节,最大值是2的31次方减1,结果是2147483647,
    转换成北京时间就是2038-01-19 11:14:07

NOW()和 CURRENT_DATE()有什么区别?

NOW()命令用于显示当前年份,月份,日期,小时,分钟和秒。
CURRENT_DATE()仅显示当前年份,月份和日期。

解释 MySQL 外连接、内连接与自连接的区别

先说什么是交叉连接: 交叉连接又叫笛卡尔积,它是指不使用任何条件,直接将一个表的所有记录和另一个表中的所有记录一一匹配。

内连接 则是只有条件的交叉连接,根据某个条件筛选出符合条件的记录,不符合条件的记录不会出现在结果集中,即内连接只连接匹配的行。

外连接 其结果集中不仅包含符合连接条件的行,而且还会包括左表、右表或两个表中的所有数据行,这三种情况依次称之为左外连接,右外连接,和全外连接。

左外连接,也称左连接,左表为主表,左表中的所有记录都会出现在结果集中,对于那些在右表中并没有匹配的记录,仍然要显示,右边对应的那些字段值以NULL 来填充。
右外连接,也称右连接,右表为主表,右表中的所有记录都会出现在结果集中。左连接和右连接可以互换,MySQL 目前还不支持全外连接。

什么叫视图?游标是什么?

视图是一种虚拟的表,具有和物理表相同的功能。可以对视图进行增,改,查,操作,视图通常是有一个表或者多个表的行或列的子集。对视图的修改不影响基本表。它使得我们获取数据更容易,相比多表查询。

游标:是对查询出来的结果集作为一个单元来有效的处理。游标可以定在该单元中的特定行,从结果集的当前行检索一行或多行。可以对结果集当前行做修改。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。

千万级Mysql数据表分页查询优化

线上数据库的一个商品表数据量过千万,做深度分页的时候性能很慢,有什么优化思路
现象:千万级别数据很正常,比如数据流水、日志记录等,数据库正常的深度分页会很慢
慢的原因:select * from product limit N,M
MySQL执行此类SQL时需要先扫描到N行,然后再去取M行,N越大,MySQL扫描的记录数越多,SQL的性能就会越差

1、后端、前端缓存

2、使用ElasticSearch分页搜索

3、合理使用 mysql 查询缓存,覆盖索引进行查询分页
select title,cateory from product limit 1000000,100

4、如果id是自增且不存在中间删除数据,使用子查询优化,定位偏移位置的 id
select * from oper_log where type=‘BUY’ limit 1000000,100; //5.秒

select id from oper_log where type=‘BUY’ limit 1000000,1; // 0.4秒

select * from oper_log where type=‘BUY’ and id>=(select id from oper_log where type=‘BUY’ limit 1000000,1) limit 100; //0.8秒

数据库性能监控和优化

针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析
大厂一般都有数据库监控后台,里面指标很多,但是开发人员也必须知道

业务性能
1、应用上线前会审查业务新增的sql,和分析sql执行计划
比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
数据安全
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署

mysql常见日志

redo 重做日志
作用:确保事务的持久性,防止在发生故障,脏页未写入磁盘。重启数据库会进行redo log执行重做,到达事务一致性

undo 回滚日志
作用:保证数据的原子性,记录事务发生之前的数据的一个版本,用于回滚。
innodb事务的可重复读和读取已提交 隔离级别就是通过mvcc+undo实现

errorlog 错误日志
作用:Mysql本身启动、停止、运行期间发生的错误信息

slow query log 慢查询日志
作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功

binlog 二进制日志
作用:用于主从复制,实现主从同步

relay log 中继日志
作用:用于数据库主从同步,将主库发送来的binlog先保存在本地,然后从库进行回放

general log 普通日志
作用:记录数据库操作明细,默认关闭,开启会降低数据库性能

针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析

大厂一般都有数据库监控后台,里面指标很多,但是开发人员也必须知道
业务性能
1、应用上线前会审查业务新增的sql,和分析sql执行计划
比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
数据安全
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署

数据库主从同步

你们搭建数据库主从复制的目的有哪些

容灾使用,用于故障切换
业务需要,进行读写分离减少主库压力

既然你们搭建了主从同步,且你们日增量数据量也不少,有没遇到同步延迟问题
为什么会有同步延迟问题,怎么解决?
保证性能第一情况下,不能百分百解决主从同步延迟问题,只能增加缓解措施。

现象:主从同步,大数据量场景下,会发现写入主库的数据,在从库没找到。

原因:
1、主从复制是单线程操作,当主库TPS高,产生的超过从库sql线程执行能力

2、从库执行了大的sql操作,阻塞等待

3、服务器硬件问题,如磁盘,CPU,还有网络延迟等

解决办法:
1、业务需要有一定的容忍度,程序和数据库直接增加缓存,降低读压力

2、业务适合的话,写入主库后,再写缓存,读的时候可以读缓存,没命中再读从库

3、读写分离,一主多从,分散主库和从库压力

4、提高硬件配置,比如使用SSD固态硬盘、更好的CPU和网络

5、进行分库分表,减少单机压力

什么场景下会出现主从数据不一致
1、本身复制延迟导致
2、主库宕机或者从库宕机都会导致复制中断
3、把一个从库提升为主库,可能导致从库和主库的数据不一致性

是否有做过主从一致性校验,你是怎么做的,如果没做过,你计划怎么做
如果不一致你会怎么修复
Mysql主从复制是基于binlog复制,难免出现复制数据不一致的风险,引起用户数据访问前后不一致的风险
所以要定期开展主从复制数据一致性的校验并修复,避免这些问题

解决方案之一,使用Percona公司下的工具

pt-table-checksum工具进行一致性校验

原理:
主库利用表中的索引,将表的数据切割成一个个chunk(块),然后进行计算得到checksum值。
从库也执相应的操作,并在从库上计算相同数据块的checksum,然后对比主从中各个表的checksum是否一致并存储到数据库,最后通过存储校验结果的表就可以判断出哪些表的数据不一致

pt-table-sync(在从库执行)工具进行修复不一致数据,可以修复主从结构数据的不一致,也可以修复非主从结构数据表的数据不一致

原理:在主库上执行数据的更改,再同步到从库上,不会直接更改成从的数据。在主库上执行更改是基于主库现在的数据,也不会更改主库上的数据,可以同步某些表或整个库的数据,但它不同步表结构、索引,只同步不一致的数据

注意:
默认主库要检查的表在从库都存在,并且同主库表有相同的表结构
如果表中没有索引,pt-table-checksum将没法处理,一般要求最基本都要有主键索引
pt-table-sync工具会修改数据,使用前最好备份下数据,防止误操作

pt-table-checksum怎么保证某个chunk的时候checksum数据一致性?
当pt工具在计算主库上某chunk的checksum时,主库可能在更新且从库可能复制延迟,那该怎么保证主库与从库计算的是”同一份”数据,答案把要checksum的行加上for update锁并计算,这保证了主库的某个chunk内部数据的一致性

Redis

什么是 Redis?

Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库。

Redis 与其他 key - value 缓存产品有以下三个特点:
Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
Redis 不仅仅支持简单的 key-value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
Redis 支持数据的备份,即 master-slave 模式的数据备份。

Redis 优势
性能极高 – Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。
丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及Ordered Sets 数据类型操作。
原子 – Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC指令包起来。
丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。

Redis 与其他 key-value 存储有什么不同?
Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样 Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

使用 Redis 有哪些好处?

1、速度快,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O1)
2、支持丰富数据类型,支持 string,list,set,Zset,hash 等
3、支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
4、丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除

Redis 相比 Memcached 有哪些优势?

1、Memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类
2、Redis 的速度比 Memcached 快很多
3、Redis 可以持久化其数据

Memcache 与 Redis 的区别都有哪些?

1、存储方式 Memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis 有部份存在硬盘上,这样能保证数据的持久性。
2、数据支持类型 Memcache 对数据类型支持相对简单。 Redis 有复杂的数据类型。
3、使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

Redis 是单进程单线程的?

Redis 是单进程单线程的,redis 利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销。

一个字符串类型的值能存储最大容量是多少?

512M

为什么 Redis 需要把所有数据放到内存中?

Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能。在内存越来越便宜的今天,redis 将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。

Redis 的同步机制了解么?

Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

Pipeline 有什么好处,为什么要用 pipeline?

可以将多次 IO 往返的时间缩减为一次,前提是 pipeline 执行的指令之间没有因果相关性。使用 redis-benchmark 进行压测的时候可以发现影响 redis 的 QPS峰值的一个重要因素是 pipeline 批次指令的数目。

是否使用过 Redis 集群,集群的原理是什么?

1)、Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为master,继续提供服务。
2)、Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储。

Redis 如何设置密码及验证密码?

设置密码:config set requirepass 123456
授权密码:auth 123456

Redis 的内存用完了会发生什么?

如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以将 Redis 当缓存来使用配置淘汰机制,当 Redis 达到内存上限时会冲刷掉旧的内容。

Redis 最适合的场景?

1、会话缓存(Session Cache)最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗? 幸运的是,随着 Redis 这些年的改进,很容
易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件。
2、全页缓存(FPC)
除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。 再次以 Magento 为例,Magento提供一个插件来使用 Redis 作为全页缓存后端。 此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加
载你曾浏览过的页面。
3、队列
Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。 如果你快速的在 Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看。
4,排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,
你需要这样执行: ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。
5、发布/订阅
最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!

简单介绍⼀下 Redis 呗!

使用C语言开发,基于内存,效率特别高,单线程的,运用了IO多路复用技术,主要用来做缓存,和分布式锁,甚至可以用来做消息队列,Redis还支持事务和持久化,lua脚本的方案.此外,还可以做到高性能和高可用,高性能的话主要是由于使用了单线程,可以通过搭建主从架构还有哨兵模式实现redis的高可用方案
简单来说 Redis 就是⼀个使⽤ C 语⾔开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,也就是它是内存数据库,所以读写速度⾮常快,因此 Redis 被⼴泛应⽤于缓存⽅向。另外,Redis 除了做缓存之外,Redis 也经常⽤来做分布式锁,甚⾄是消息队列。Redis 提供了多种数据类型来⽀持不同的业务场景。Redis 还⽀持事务 、持久化、Lua 脚本、多种集群⽅案。

说下分布式缓存和本地缓存的区别,如何选择?

什么是缓存
程序经常要调用的对象存在内存中,方便其使用时可以快速调用,不必去数据库或者其他持久化设备中查询,主要就是提高性能
DNS缓存、前端缓存、代理服务器缓存Nginx、应用程序缓存(本地缓存、分布式缓存)、数据库缓存

  • 分布式缓存
    • 与应用分离的缓存组件或服务,与本地应用隔离一个独立的应用,多个应用可直接的共享缓存
    • 常见的分布式缓存 Redis、Memcached等
  • 本地缓存
    • 和业务程序一起的缓存,例如myabtis的一级或者二级缓存,本地缓存自然是最快的,但是不能在多个节点共享
    • 常见的本地缓存:myabtis 一级缓存、mybatis二级缓存;框架本身的缓存;
    • redis本地单机服务;ehchche;guava cache、Caffeine等

选择本地缓存和分布式缓存
和业务数据结合去选择
高并发项目里面一般都是有本地缓存和分布式缓存共同存在的

说说如何实现分布式锁

常见的实现分布式锁有两种方式

  • 基于redis实现分布式锁:核心思想是获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁
  • 基于zookeeper的实现方式,核心思想在zk中是为每个线程生成一个有序的临时节点,为确保有序性,在排序一次全部节点,获取全部节点,每个线程判断自己是否最小,如果是的话,获得锁,执行操作,操作完删除自身节点。如果不是第一个的节点则监听它的前一个节点,当它的前一个节点被删除时,则它会获得锁,以此类推。特别注意,这里新建节点必须要是临时节点,确保获取到锁的客户端宕机也不影响其他客户端获取锁

使用redis的优点是性能高,缺点是特定情况下master宕机,数据没完成同步,其他客户端可以继续获取到锁
使用zk的优点是能保证一致性,缺点是频繁读写,性能较差

你们业务用了redis,为啥不用其他缓存,比如memcached呢

  • redis数据结构比memcached更丰富,基本可以完全替换
  • redis社区比较活跃,性能也强大,也支持持久化等功能
  • 要和业务结合,比如电商系统的热销商品,需要用到zset,所以使用redis

Redis哪些数据结构? 说下这些结构的使用场景有哪些

String

  1. 介绍 :string 数据结构是简单的 key-value 类型。虽然 Redis 是⽤ C 语⾔写的,但是 Redis并没有使⽤ C 的字符串表示,⽽是⾃⼰构建了⼀种简单动态字符串(simple dynamic string,SDS)。相⽐于 C 的原⽣字符串,Redis 的 SDS 不光可以保存⽂本数据还可以保存⼆进制数据,并且获取字符串⻓度复杂度为O(1)(C 字符串为 O(N)),除此之外,Redis 的SDS API 是安全的,不会造成缓冲区溢出。
  2. 常⽤命令: set,get,strlen,exists,dect,incr,setex 等等。
  3. 应⽤场景 :⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等等。

hash
4. 介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,
Redis 的 hash 做了更多优化。另外,hash 是⼀个 string 类型的 field 和 value 的映射表,
特别适合⽤于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的
值。 ⽐如我们可以 hash 数据结构来存储⽤户信息,商品信息等等。
5. 常⽤命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
6. 应⽤场景: 系统中对象数据的存储,购物车。

list
7. 介绍 :==list 即是链表。==链表是⼀种常见的数据结构,特点是易于数据元素的插⼊和删除并且且可以灵活调整链表⻓度,但是链表的随机访问困难。许多⾼级编程语⾔都内置了链表的实现⽐如 Java 中的LinkedList,但是 C 语⾔并没有实现链表,所以 Redis 实现了⾃⼰的链表数据结构。Redis 的 list 的实现为⼀个双向链表,即可以⽀持反向查找和遍历,更⽅便操作,不过带来了部分额外的内存开销。
8. 常⽤命令: rpush,lpop,lpush,rpop,lrange、llen 等。
9. 应⽤场景: 发布与订阅或者说消息队列、慢查询。

set
10. 介绍 : set 类似于 Java 中的 HashSet。Redis 中的 set 类型是⼀种无序集合,集合中的元素没有先后顺序。当你需要存储⼀个列表数据,⼜不希望出现重复数据时,set 是⼀个很好的选择,并且 set 提供了判断某个成员是否在⼀个 set 集合内的重要接⼝,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。⽐如:你可以将⼀个⽤户所有的关注⼈存在⼀个集合中,将其所有粉丝存在⼀个集合。Redis 可以⾮常⽅便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
11. 常⽤命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
12. 应⽤场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景

sroted set
13. 介绍: 和 set 相⽐,sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score进⾏有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap和 TreeSet 的结合体。
14. 常⽤命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
15. 应⽤场景: 需要对数据根据某个权重进⾏排序的场景。⽐如在直播系统中,实时排⾏信息包含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息(可以理解为按消息维度的消息排⾏榜)等信息。

缓存数据的处理流程是怎样的?

  1. 如果⽤户请求的数据在缓存中就直接返回。
  2. 缓存中不存在的话就看数据库中是否存在。
  3. 数据库中存在的话就更新缓存中的数据。
  4. 数据库中不存在的话就返回空数据。
    查询是否在缓存中,在的话直接返回,不在的话查询数据库,查到的话更新缓存,没有查到的话返回空的数据

为什么要用 Redis/为什么要用缓存?

假如⽤户第⼀次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,⽤户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心i地将该⽤户访问的数据存在缓存中。这样有什么好处呢? 那就是保证⽤户下⼀次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。不过,要保持数据库和缓存中的数据的⼀致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
⾼并发:
⼀般像 MySQL 这类的数据库的 QPS ⼤概都在 1w 左右(4 核 8g) ,但是使⽤ Redis 缓存之后很容易达到 10w+,甚⾄最⾼能达到 30w+(就单机 redis 的情况,redis 集群的话会更⾼)。

QPS(Query Per Second):服务器每秒可以执⾏的查询次数;

所以,直接操作缓存能够承受的数据库请求数量是远远⼤于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样⽤户的⼀部分请求会直接到缓存这⾥⽽不⽤经过数据库。进而,我们也就提⾼的系统整体的并发。

Redis 单线程模型详解

Redis 基于 Reactor 模式来设计开发了⾃⼰的⼀套⾼效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是⾼性能 IO 的基⽯),这套事件处理模型对应的是 Redis中的⽂件事件处理器(file event handler)。由于⽂件事件处理器(file event handler)是单线程⽅式运⾏的,所以我们⼀般都说 Redis 是单线程模型。

既然是单线程,那怎么监听⼤量的客户端连接呢?
Redis 通过IO 多路复用程序来监听来⾃客户端的⼤量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发⽣。这样的好处⾮常明显: I/O 多路复⽤技术的使⽤让 Redis 不需要额外创建多余的线程来监听客户端的⼤量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。另外, Redis 服务器是⼀个事件驱动程序,服务器需要处理两类事件: 1. ⽂件事件; 2. 时间事件。

redis是单线程,为什么这么快?

  • 基于内存,绝大部分请求是纯粹的内存操作,CPU不是Redis的瓶颈
  • 避免了不必要的CPU上下文切换和其他竞争条件,比如锁操作等
  • 底层是使用多路I/O复用模型,非阻塞IO
  • Redis6 后支持多线程,但是默认不开启

Redis 给缓存数据设置过期时间有啥用?

为了处理只在一段时间内有效的数据,比如用户验证码.还有分布式锁的时候也需要指定过期时间,防止持有锁的线程down掉之后锁未释放

因为内存是有限的,如果缓存中的所有数据都是⼀直保存的话,分分钟直接Out of memory。
过期时间除了有助于缓解内存的消耗,还有什么其他⽤么?

很多时候,我们的业务场景就是需要某个数据只在某⼀时间段内存在,⽐如我们的短信验证码可能只在1分钟内有效,⽤户登录的 token 可能只在 1 天内有效。如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多。

过期的数据的删除策略了解么?

如果假设你设置了⼀批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进⾏删除的呢?

  1. 惰性删除 :只会在取出key的时候才对数据进⾏过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除 : 每隔⼀段时间抽取⼀批 key 执⾏删除过期key操作。并且,Redis 底层会通过限制删除操作执⾏的时⻓和频率来减少删除操作对CPU时间的影响。

定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采⽤的是 定期
删除+惰性/懒汉式删除 。

但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉
了很多过期 key 的情况。这样就导致⼤量过期 key 堆积在内存⾥,然后就Out of memory了。
怎么解决这个问题呢?答案就是: Redis 内存淘汰机制

缓存淘汰策略

一般会使用淘汰策略
常见的淘汰策略有 FIFO、LRU、LFU
能分别说下FIFO、LRU、LFU这些策略不

  • 先进先出First In,First Out
    新访问的数据插入FIFO队列尾部,数据在FIFO队列中顺序移动,淘汰FIFO队列头部的数据

  • 最近最少使用 Least recently used
    根据数据的历史访问记录来进行数据淘汰,如果数据最近被访问过,那么将来被访问的几率也更高
    新数据插入到链表头部,每当缓存数据被访问,则将数据移到链表头部,当链表满的时候,将链表尾部的数据丢弃。

  • 最近不经常使用 Least Frequently Used
    根据数据的历史访问频率来淘汰数据,如果数据过去被访问多次,那么将来被访问的频率也更高
    把数据加入到链表中,按频次排序,一个数据被访问过,把它的频次+1,发生淘汰的时候,把频次低的淘汰掉

redis持久化

支持AOF和RDB持久化

  • AOF
    以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录
    支持秒级持久化、兼容性好,对于相同数量的数据集而言,AOF文件通常要大于RDB文件,所以恢复比RDB慢

  • RDB
    在指定的时间间隔内将内存中的数据集快照写入磁盘,可以指定时间归档数据(形成冷数据),但不能做到实时持久化
    文件紧凑,体积小,对于灾难恢复而言,RDB是非常不错的选择,相比于AOF机制,如果数据集很大,RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
    有save和bgsave两种方式,bgsave会在后台fork一个子进程进行持久化操作

Redis事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。
使⽤ MULTI命令后可以输⼊多个命令。Redis不会⽴即执⾏这些命令,⽽是将它们放到队列,当调⽤了EXEC命令将执⾏所有命令。
我们知道事务具有四⼤特性:1. 原⼦性,2. 隔离性,3. 持久性,4. ⼀致性。
Redis 是不⽀持 roll back 的,因⽽不满⾜原⼦性的(⽽且不满⾜持久性)

缓存穿透、击穿和雪崩

  • 缓存击穿 (某个热点key缓存失效了)
    缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力增大。
    和缓存雪崩的区别在于这里针对某一key缓存,后者则是很多key。
    预防:设置热点数据不过期,定时任务定时更新缓存,或者设置互斥锁

  • 缓存雪崩 (多个热点key都过期)
    大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩
    预防:存数据的过期时间设置随机,防止同一时间大量数据过期现象发生,设置热点数据永远不过期,定时任务定时更新

  • 缓存穿透(查询不存在数据)
    查询一个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id为“-1”不存在的数据
    如果从存储层查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在大量查询不存在的数据,可能DB就挂掉了,这也是黑客利用不存在的key频繁攻击应用的一种方式。
    预防:接口层增加校验,数据合理性校验,缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置短点的过期时间,防止同个key被一直攻击

系统设计

常用框架

Spring

Spring 基础

什么是 Spring 框架?

Spring是一款开源的轻量级的Java开发框架,一般说的Spring框架都是SpringFramework,它是很多模块的集合,通过这些模块可以很快速的完成我们的开发工作,比如AOP和IOC,还有集成测试,同时也可以很方便的集成第三方组件,比如邮件啊缓存啊各种。

Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发,比如说 Spring 支持 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。
Spring 最核心的思想就是不重新造轮子,开箱即用,提高开发效率。

Spring 翻译过来就是春天的意思,可见其目标和使命就是为 Java 程序员带来春天啊!感动!
Spring 提供的核心功能主要是 IoC 和 AOP。学习 Spring ,一定要把 IoC 和 AOP 的核心思想搞懂!

Spring 包含的模块有哪些?

Spring5.x 版本
Spring5.x主要模块
Spring5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。

Spring 各个模块的依赖关系如下:
Spring 各个模块的依赖关系
Core Container
Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块,我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。

  • spring-core :Spring 框架基本的核心工具类。
  • spring-beans :提供对 bean 的创建、配置和管理等功能的支持。
  • spring-context :提供对国际化、事件传播、资源加载等功能的支持。
  • spring-expression :提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。
    AOP
  • spring-aspects :该模块为与 AspectJ 的集成提供支持。
  • spring-aop :提供了面向切面的编程实现。
  • spring-instrument :提供了为 JVM 添加代理(agent)的功能。具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文 件,就像这些文件是被类加载器加载的一样。没有理解也没关系,这个模块的使用场景非常有限。
    Data Access/Integration
  • spring-jdbc :提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。
  • spring-tx :提供对事务的支持。
  • spring-orm :提供对 Hibernate、JPA 、iBatis 等 ORM 框架的支持。
  • spring-oxm :提供一个抽象层支撑 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。
  • spring-jms : 消息服务。自 Spring Framework 4.1 以后,它还提供了对 spring-messaging 模块的继承。
    Spring Web
  • spring-web :对 Web 功能的实现提供一些最基础的支持。
  • spring-webmvc :提供对 Spring MVC 的实现。
  • spring-websocket :提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。
  • spring-webflux :提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。
    Messaging
    spring-messaging是从 Spring4.0 开始新加入的一个模块,主要职责是为 Spring 框架集成一些基础的报文传送应用。

Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。

Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。

Spring,Spring MVC,Spring Boot 之间什么关系?

很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。

Spring 包含了多个功能模块(上面刚刚提高过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。

下图对应的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。
Spring主要模块
Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
在这里插入图片描述
使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!

Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。

Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!

为什么要使用Spring?
  • Spring提供ioc技术,容器会帮你管理依赖的对象,从而不需要自己创建和管理依赖对象了,更轻松的实现了代码的解耦
  • Spring提供了事务支持,使得事务操作变得更加方便
  • Spring提供了面向切面编程,这样可以更方便的处理某一类的问题
  • 更方便的框架集成,Spring可以很方便的集成其他框架,比如Mybtis,hibernate等
使用Spinrg框架的好处是什么?

轻量:Spring是轻量的,基本的版本大约2MB
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象
面向切面编程(AOP):Spring支持面向切面编程,并且把应用业务逻辑和系统服务分开
容器:Spring包含并管理应用中对象的生命周期和配置
MVC框架:Spring的WEB框架是个精心设计的框架,是web框架的一个很好的替代品
事务管理:Spring提供了一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)
异常处理:Spring提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转换为一致的unchecked异常

BeanFactory 和 ApplicationContext 有什么区别

BeanFactory 可以理解为含有 bean 集合的工厂类。BeanFactory 包含了bean 的定义, 以便在接收到客户端请求时将对应的 bean 实例化。
BeanFactory 还能在实例化对象的时生成协作类之间的关系。此举将 bean 自身与 bean客户端的配置中解放出来。BeanFactory 还包含了 bean 生命周期的控制,调用客户端的初始化方法(initialization methods)和销毁方法(destruction methods)。从表面上看,application context 如同 bean factory 一样具有 bean 定义、bean 关联关系的设置,根据请求分发 bean 的功能。但 application context 在此基础上还提供了其他的功能。

提供了支持国际化的文本消息
统一的资源文件读取方式
已在监听器中注册的 bean 的事件

Spring Bean 的生命周期

Spring Bean 的生命周期简单易懂。在一个 bean 实例被初始化时,需要执行一系列的初始化操作以达到可用的状态。同样的,当一个 bean 不在被调用时需要进行相关的析构操作,并从 bean 容器中移除。
Spring bean factory 负责管理在 spring 容器中被创建的 bean 的生命周期。Bean 的生命周期由两组回调(call back)方法组成。
初始化之后调用的回调方法。
销毁之前调用的回调方法。

Spring 框架提供了以下四种方式来管理 bean 的生命周期事件:
InitializingBean 和 DisposableBean 回调接口
针对特殊行为的其他 Aware 接口
Bean 配置文件中的 Custom init()方法和 destroy()方法
@PostConstruct 和@PreDestroy 注解方式

Spring IoC

IOC的优点是什么?

IOC或依赖注入把应用的代码量降到最低,它使应用容易测试,单元测试不再需要单例和JNDI查找机制,最小的代价和最小的侵入性使松散耦合得以实现,IOC容易支持加载服务时的饿汉式初始化和懒加载

谈谈自己对于 Spring IoC 的了解

**IoC(Inverse of Control:控制反转)**是一种设计思想,而不是一个具体的技术实现。==IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。==不过, IoC 并非 Spring 特有,在其他语言中也有应用。

为什么叫控制反转?

控制 :指的是对象创建(实例化、管理)的权力
**反转 **:控制权交给外部环境(Spring 框架、IoC 容器)
在这里插入图片描述
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。

在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。

Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是SpringBoot 注解配置就慢慢开始流行起来。

IOC 控制反转,指将对象的创建权,反转到Spring容器
DI 依赖注入,指Spring创建对象的过程中,将对象依赖属性通过配置进行注入,不能单独存在,需要在IOC的基础上完成操作
依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情,通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。

什么是 Spring Bean?

简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。

我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。

<!-- Constructor-arg with 'value' attribute --><bean id="..." class="...">   <constructor-arg value="..."/></bean>

下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
在这里插入图片描述
org.springframework.beans和 org.springframework.context 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看

将一个类声明为 Bean 的注解有哪些?
  • @Component :通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。
  • @Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
  • @Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
@Component 和 @Bean 的区别是什么?
  • @Component 注解作用于类,而@Bean注解作用于方法。
  • @Component通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用@ComponentScan 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。@Bean 注解通常是我们在标有该注解的方法中定义产生这个 bean,@Bean告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。
  • @Bean 注解比 @Component 注解的自定义性更强,而且很多地方我们只能通过 @Bean 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 Spring容器时,则只能通过 @Bean来实现。
    @Bean注解使用示例:
@Configurationpublic class AppConfig {    @Bean    public TransferService transferService() {        return new TransferServiceImpl();    }}

上面的代码相当于下面的 xml 配置

<beans>    <bean id="transferService" class="com.acme.TransferServiceImpl"/></beans>

下面这个例子是通过 @Component 无法实现的。

@Beanpublic OneService getService(status) {    case (status)  {        when 1:                return new serviceImpl1();        when 2:                return new serviceImpl2();        when 3:                return new serviceImpl3();    }}
注入 Bean 的注解有哪些?

Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。
在这里插入图片描述
@Autowired 和@Resource使用的比较多一些。

@Autowired 和 @Resource 的区别是什么?

Autowired 属于 Spring 内置的注解,默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

这会有什么问题呢? 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。

这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。

// smsService 就是我们上面所说的名称@Autowiredprivate SmsService smsService;

举个例子,SmsService 接口有两个实现类: SmsServiceImpl1和 SmsServiceImpl2,且它们都已经被 Spring 容器所管理。

// 报错,byName 和 byType 都无法匹配到 bean@Autowiredprivate SmsService smsService;// 正确注入 SmsServiceImpl1 对象对应的 bean@Autowiredprivate SmsService smsServiceImpl1;// 正确注入  SmsServiceImpl1 对象对应的 bean// smsServiceImpl1 就是我们上面所说的名称@Autowired@Qualifier(value = "smsServiceImpl1")private SmsService smsService;

我们还是建议通过 @Qualifier 注解来显示指定名称而不是依赖变量的名称。

@Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType。

@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。

public @interface Resource {    String name() default "";    Class<?> type() default Object.class;}

如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName。

// 报错,byName 和 byType 都无法匹配到 bean@Resourceprivate SmsService smsService;// 正确注入 SmsServiceImpl1 对象对应的 bean@Resourceprivate SmsService smsServiceImpl1;// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)@Resource(name = "smsServiceImpl1")private SmsService smsService;

简单总结一下:

  • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
  • Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。
  • 当一个接口存在多个实现类的情况下,@Autowired 和@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显示指定名称,@Resource可以通过 name 属性来显示指定名称。
Bean 的作用域有哪些?

Spring 中 Bean 的作用域通常有下面几种:

  • singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
  • prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
  • request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
  • session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
  • application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),,该 bean 仅在当前应用启动时间内有效。
  • websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
    如何配置 bean 的作用域呢?

xml 方式:

<bean id="..." class="..." scope="singleton"></bean>

注解方式:

@Bean@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)public Person personPrototype() {    return new Person();}
单例 Bean 的线程安全问题了解吗?

大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。

常见的有两种解决办法:

在 Bean 中尽量避免定义可变的成员变量。
在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。

Bean 的生命周期了解么?

  • Bean 容器找到配置文件中 Spring Bean 的定义。
  • Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。
  • 如果涉及到一些属性值 利用 set()方法设置一些属性值。
  • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的名字。
  • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。
  • 如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入 BeanFactory对象的实例。
  • 与上面的类似,如果实现了其他 Aware接口,就调用相应的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
  • 如果 Bean 实现了InitializingBean接口,执行afterPropertiesSet()方法。
  • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
  • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法
  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
  • 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。
    Spring Bean 生命周期

Spring AOP

AOP实现技术

AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务。
一、场景
事务管理、安全检查、权限控制、数据校验、缓存、对象池管理等
二、实现技术
AOP(这里的AOP指的是面向切面编程思想,而不是Spring AOP)主要的的实现技术主要有
Spring AOP和AspectJ。
1)AspectJ的底层技术。
AspectJ的底层技术是静态代理,即用一种AspectJ支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。
2)Spring AOP
Spring AOP采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持。

JDK动态代理只能为接口创建动态代理实例,而不能对类创建动态代理。需要获得被目标类的接口信息(应用Java的反射技术),生成一个实现了代理接口的动态代理类(字节码),再通过反射机制获得动态代理类的构造函数,利用构造函数生成动态代理类的实例对象,在调用具体方法前调用invokeHandler方法来处理。

CGLib动态代理需要依赖asm包,把被代理对象类的class文件加载进来,修改其字节码生成子类。

但是Spring AOP基于注解配置的情况下,需要依赖于AspectJ包的标准注解。

Spring AOP 实现原理

Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态代理。JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。
如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。

CGLIB (Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为final, 那么它是无法使用 CGLIB 做动态代理的。

谈谈自己对于 AOP 的了解

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

Aspect Oriented Program 面向切面编程, 在不改变原有逻辑上增加额外的功能

AOP思想把功能分两个部分,分离系统中的各种关注点

核心关注点
业务的主要功能
横切关注点
非核心、额外增加的功能

用户下单为例子
核心关注点:创建订单
横切关注点:记录日志、控制事务
好处
减少代码侵入,解耦
可以统一处理横切逻辑
方便添加和删除横切逻辑

Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
SpringAOPProcess
当然你也可以使用 AspectJ !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。

AOP 切面编程设计到的一些专业术语:
在这里插入图片描述

AOP里面常见的概念

横切关注点
对哪些方法进行拦截,拦截后怎么处理,这些就叫横切关注点
比如 权限认证、日志、事物

通知 Advice
在特定的切入点上执行的增强处理
做啥? 比如你需要记录日志,控制事务 ,提前编写好通用的模块,需要的地方直接调用

连接点 JointPoint
要用通知的地方,业务流程在运行过程中需要插入切面的具体位置,一般是方法的调用前后,全部方法都可以是连接点

切入点 Pointcut
不能全部方法都是连接点,通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
过滤出相应的 Advice 将要发生的joinpoint地方

切面 Aspect
通常是一个类,里面定义 切入点+通知 , 定义在什么地方; 什么时间点、做什么事情
通知 advice指明了时间和做的事情(前置、后置等)
切入点 pointcut 指定在什么地方干这个事情
web接口设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面,对象和对象,方法和方法之间都是一个个切面

目标 target
目标类,真正的业务逻辑,可以在目标类不知情的条件下,增加新的功能到目标类的链路上

织入 Weaving
把切面(某个类)应用到目标函数的过程称为织入

在这里插入图片描述

Spring常见面试题静态代理和动态代理

能否解释下什么是静态代理
什么是静态代理

  • 由程序创建或特定工具自动生成源代码,在程序运行前,代理类的.class文件就已经存在
  • 通过将目标类与代理类实现同一个接口,让代理类持有真实类对象,然后在代理类方法中调用真实类方法,在调用真实类方法的前后添加我们所需要的功能扩展代码来达到增强的目的
    A -> B -> C

优点

  • 代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可
  • 方便增加功能,拓展业务逻辑

缺点

  • 代理类中出现大量冗余的代码,非常不利于扩展和维护
  • 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度

能否解释下什么是动态代理,spring aop是用什么代理

  • 在程序运行时,运用反射机制动态创建而成,无需手动编写代码
  • Spring AOP 就是要对目标进行代理对象的创建, Spring AOP是基于动态代理的,有动态代理机制: JDK动态代理和CGLIB动态代理
    动态代理:在虚拟机内部,运行的时候,动态生成代理类(运行时生成,runtime生成) ,并不是真正存在的类
静态代理

Count.java

/**  * 定义一个账户接口  *   * @author Administrator  *   */  public interface Count {      // 查看账户方法      public void queryCount();        // 修改账户方法      public void updateCount();    }  

CountImpl.java

/**  * 委托类(包含业务逻辑)  *   * @author Administrator  *   */  public class CountImpl implements Count {        @Override      public void queryCount() {          System.out.println("查看账户方法...");        }        @Override      public void updateCount() {          System.out.println("修改账户方法...");        }    }    //CountProxy.java  package net.battier.dao.impl;    import net.battier.dao.Count;    /**  * 这是一个代理类(增强CountImpl实现类)  *   * @author Administrator  *   */  public class CountProxy implements Count {      private CountImpl countImpl;        /**      * 覆盖默认构造器      *       * @param countImpl      */      public CountProxy(CountImpl countImpl) {          this.countImpl = countImpl;      }        @Override      public void queryCount() {          System.out.println("事务处理之前");          // 调用委托类的方法;          countImpl.queryCount();          System.out.println("事务处理之后");      }        @Override      public void updateCount() {          System.out.println("事务处理之前");          // 调用委托类的方法;          countImpl.updateCount();          System.out.println("事务处理之后");        }    }  

TestCount.java

/**  *测试Count类  *   * @author Administrator  *   */  public class TestCount {      public static void main(String[] args) {          CountImpl countImpl = new CountImpl();          CountProxy countProxy = new CountProxy(countImpl);          countProxy.updateCount();          countProxy.queryCount();        }  }  

观察代码可以发现每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理,而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那么此时就必须使用动态代理完成。

JDK动态代理

1 JDK动态代理需要一个接口和一个类
1.1 InvocationHandler (调用处理程序)
InvocationHandler 是生成代理实例的类需要实现的接口,然后需要实现接口中的 invoke() 方法,在这个方法中进行对代理实例的处理
1.2 Proxy (代理)
Proxy 是所有代理实例的父类,它提供了创建动态代理实例的静态方法.
2 代码演示一
2.1 创建一个抽象对象

//租房public interface Rend {    public void rend();}

2.2 创建一个 真实对象

public class Homeowner implements Rend {    @Override    public void rend(){        System.out.println("房东出租了房子");    }}

3.3 创建一个生成代理实例的类(核心)

//这个类是用来生成代理实例的类public class ProxyInvocationHandle implements InvocationHandler {    //被代理的接口    private Rend rend;    public void setRend(Rend rend) {        this.rend = rend;    }/** * 参数说明:  * ClassLoader loader:类加载器  * Class<?>[] interfaces:得到全部的接口  * InvocationHandler h:得到InvocationHandler接口的子类实例 */    //生成得到代理类    public Object getProxy(){        return Proxy.newProxyInstance(this.getClass().getClassLoader(),rend.getClass().getInterfaces(),this);    }        /**         * 参数说明:          * Object proxy:指被代理的对象。          * Method method:要调用的方法          * Object[] args:方法调用时所需要的参数          */    //处理代理实例,并返回结果    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {        //动态代理的本质,就是使用反射机制实现!        seeHouse();        seeHouse();        Object result = method.invoke(rend, args);        fare();        return result;    }    public void seeHouse(){        System.out.println("中介带你看房子");    }    public void fare(){        System.out.println("收中介费");    }}

3.4 创建客户类

public class Client {    public static void main(String[] args) {        //真实角色        Homeowner homeowner = new Homeowner();        //代理角色:现在没有        ProxyInvocationHandle pih = new ProxyInvocationHandle();        //通过调用程序处理角色来处理我们要调用的接口对象!        pih.setRend(homeowner);        Rend proxy = (Rend) pih.getProxy();//这里的proxy就是动态生成的,我们并没有写        proxy.rend();    }}

但是,JDK的动态代理依靠接口实现,如果有些类并没有实现接口,则不能使用JDK代理,这就要使用cglib动态代理了。

4 总结

  • 动态代理解决了静态代理创建过多的代理类导致开发效率降低的问题
  • 动态代理的角色和静态代理的是相同的
  • 动态代理的代理类是动态生成的 . 静态代理的代理类是我们提前写好的
  • 一个动态代理 , 一般代理某一类业务
  • 一个动态代理可以代理多个类,代理的是接口
  • 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情
  • 公共的业务由代理来完成 . 实现了业务的分工
  • 公共业务发生扩展时变得更加集中和方便
Cglib动态代理

JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。

1、BookFacadeCglib.java

public interface BookFacade {      public void addBook();  }  

2、BookCadeImpl1.java

/**  * 这个是没有实现接口的实现类  *   * @author student  *   */  public class BookFacadeImpl1 {      public void addBook() {          System.out.println("增加图书的普通方法...");      }  } 

BookFacadeProxy.java

/**  * 使用cglib动态代理  *   * @author student  *   */  public class BookFacadeCglib implements MethodInterceptor {      private Object target;        /**      * 创建代理对象      *       * @param target      * @return      */      public Object getInstance(Object target) {          this.target = target;          Enhancer enhancer = new Enhancer();          enhancer.setSuperclass(this.target.getClass());          // 回调方法          enhancer.setCallback(this);          // 创建代理对象          return enhancer.create();      }        @Override      // 回调方法      public Object intercept(Object obj, Method method, Object[] args,              MethodProxy proxy) throws Throwable {          System.out.println("事物开始");          proxy.invokeSuper(obj, args);          System.out.println("事物结束");          return null;          }    } 

4、TestCglib.java

public class TestCglib {            public static void main(String[] args) {          BookFacadeCglib cglib=new BookFacadeCglib();          BookFacadeImpl1 bookCglib=(BookFacadeImpl1)cglib.getInstance(new BookFacadeImpl1());          bookCglib.addBook();      }  }  
JDK动态代理和CGLib动态代理的区别

动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理,解耦和易维护

  • JDK动态代理,要求目标对象实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以用CGLib动态代理
  • JDK动态代理是自带的,CGlib需要引入第三方包
  • CGLib动态代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展
  • CGLib动态代理基于继承来实现代理,所以无法对final类、private方法和static方法实现代理
Spring AOP中的代理使用的默认策略?
  • 如果目标对象实现了接口,则默认采用JDK动态代理
  • 如果目标对象没有实现接口,则采用CgLib进行动态代理
  • 如果目标对象实现了接口,程序里面依旧可以指定使用CGlib动态代理
Spring AOP 和 AspectJ AOP 有什么区别?

Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,

如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。

AspectJ 定义的通知类型有哪些?
  • Before(前置通知):目标对象的方法调用之前触发
  • After (后置通知):目标对象的方法调用之后触发
  • AfterReturning(返回通知):目标对象的方法调用完成,在返回结果值之后触发
  • AfterThrowing(异常通知) :目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。
  • Around:(环绕通知)编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法
多个切面的执行顺序如何控制?

1、通常使用@Order 注解直接定义切面顺序

// 值越小优先级越高@Order(3)@Component@Aspectpublic class LoggingAspect implements Ordered {

2、实现Ordered 接口重写 getOrder 方法。

@Component@Aspectpublic class LoggingAspect implements Ordered {    // ....    @Override    public int getOrder() {        // 返回值越小优先级越高        return 1;    }}

@RestController vs @Controller

@Controller 返回⼀个⻚⾯
单独使⽤ @Controller 不加 @ResponseBody 的话⼀般使⽤在要返回⼀个视图的情况,这种情况属于比较传统的Spring MVC 的应⽤,对应于前后端不分离的情况。
在这里插入图片描述

@RestController 返回JSON 或 XML 形式数据
但 @RestController 只返回对象,对象数据直接以 JSON 或 XML 形式写⼊ HTTP 响应
(Response)中,这种情况属于 RESTful Web服务,这也是⽬前⽇常开发所接触的最常⽤的情况
(前后端分离)。
在这里插入图片描述
@Controller +@ResponseBody 返回JSON 或 XML 形式数据
@ResponseBody 注解的作⽤是将 Controller 的⽅法返回的对象通过适当的转换器转换为指定的格式之后,写⼊到HTTP 响应(Response)对象的 body 中,通常⽤来返回 JSON 或者XML 数据,返回 JSON 数据的情况比较多。
在这里插入图片描述

Spring 框架中用到了哪些设计模式?

  • 工厂设计模式 : Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。

Spring 事务

Spring 管理事务的方式有几种?

编程式事务 :在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
声明式事务 :在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)

Spring 事务中哪几种事务传播行为?

事务传播行为是为了解决业务层方法之间互相调用的事务问题。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

正确的事务传播行为可能的值如下:

1.TransactionDefinition.PROPAGATION_REQUIRED

使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

2.TransactionDefinition.PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。

3.TransactionDefinition.PROPAGATION_NESTED

如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

4.TransactionDefinition.PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

这个使用的很少。

若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚:

  • TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
Spring 事务中的隔离级别有哪几种?

和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类:Isolation

public enum Isolation {    DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),    READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),    READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),    REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),    SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);    private final int value;    Isolation(int value) {        this.value = value;    }    public int value() {        return this.value;    }}

下面我依次对每一种事务隔离级别进行介绍:

  • TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
@Transactional(rollbackFor = Exception.class)注解了解吗?

Exception 分为运行时异常 RuntimeException 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。

当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。

在 @Transactional 注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上 rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。

解释JDBC抽象和DAOm欧快

通过使用JDBC抽象和DAO模块,保证数据库代码的简介,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上,提供了一个统一的异常访问层,它还利用Spring的AOP模块给Spring应用中的对象提供事务管理服务

Spring常用的注入方式有哪些?

setter属性注入
构造方法注入
注解方式注入

Sping配置文件作用

Spring配置文件是个XML文件,这个文件包含了类信息,描述了如何配置它们,已经如何相互调用

什么是SpingBeans?

SpringBeans是那些形成Spring应用的主干的java对象,它们被SpringIOC容器初始化,装配和管理,这些beans通过容器中配置的元数据创建,比如,以xml文件中的形式定义
Spring’框架定义的beans都是单间beans,在bean tag中有个属性"singleton",如果它被赋为True,bean就是单件,否则就是一个prototype bean,默认是true,所以所有在spring框架中的beans缺省都是单件

一个SpringBean定义包含什么?

一个SpringBean的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖

Spring中的bean是线程安全的吗?

spring中的bean默认是单例模式,spring框架并没有对单例bean进行多线程的封装处理
实际上大部分时候springbean无状态的(比如果dao类),所以某种程度上来说bean也是安全的,但是如果bean有状态的话(比如view model对象),那就要开发者自己去保证线程安全了,最简单的就是改变bean的作用域,把"singleton"变更为"prototype",这样请求bean相当于new Bean()了,所以就可以保证线程安全了

  • 有状态就是有数据存储功能
  • 无状态就是不会保存数据

Spring自动装配bean有哪些方式?

  • no:默认值,表示没有自动装配,应使用显示bean引用进行装配
  • byName:它根据bean的名称注入对象依赖项
  • byType:它根据类型注入对象依赖项
  • 构造函数:通过构造函数来注入依赖项,需要设置大量的参数
  • autodetect:容器首先通过构造函数使用autowired装配,如果不能,则通过byType自动装配

什么是基于java的spring注解配置?给一些注解的例子

基于java的配置,允许你在少量的java注解的帮助下,进行你的大部分Spring配置而非通过xml文件
以@Configuration注解为例,它用来标记类可以当做一个bean的定义,被SpringIOC容器使用,另一个例子是@Bean注解,它表示次方法将要返回一个对象,作为一个bean注册进Spring应用上下文

什么是基于注解的容器配置?

相对于xml文件,注解型的配置依赖于通过字节码元数据装配组件,而非尖括号的声明
开发者通过在相应的类,方法或属性上使用注解的方式,直接组件类中进行配置,而不使用xml表述bean的装配关系

@Required注解

这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显示的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializatioonException

@Autowired注解

@Autowired注解提供了一种更细粒度的控制,包括在何处以及如何完成自动装配,它的用法和@Required一样.修饰setter方法,构造器,属性或者具有任意名称和多个参数的方法

@Qualifier注解

当有多个相同类型的bean却只有一个需要自动装配时候,将@Qualifier注解和@Autowired注解结合使用以消除这种混淆,指定需要装配的确切的bean

在Spring框架中如何更有效的使用jdbc?

使用SpringJDBC框架,资源管理和错误处理的代价将会被减轻,所有开发者只需要写statements和querues从数据存取数据,jdbc也可以在spring框架提供的模板类的帮助下更有效的被使用,这个模板叫做jdbcTemplate

jdbcTemplate

jdbcTemplate类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理

Spring MVC

说说自己对于 Spring MVC 了解?

MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
在这里插入图片描述
Model 1 时代

很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。

这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。

Model 2 时代

学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。

Model:系统涉及的数据,也就是 dao 和 bean。
View:展示模型中的数据,只是用来展示。
Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。
Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。

于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。

Spring MVC 时代

随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。

MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。

Spring MVC 的核心组件有哪些?

记住了下面这些组件,也就记住了 SpringMVC 的工作原理。

  • DispatcherServlet :核心的中央处理器,负责接收请求、分发,并给予客户端响应。
  • HandlerMapping :处理器映射器,根据 uri 去匹配查找能处理的 Handler ,并会将请求涉及到的拦截器和 Handler 一起封装。
  • HandlerAdapter :处理器适配器,根据 HandlerMapping 找到的 Handler ,适配执行对应的 Handler;
  • Handler :请求处理器,处理实际请求的处理器。
  • ViewResolver :视图解析器,根据 Handler 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 DispatcherServlet 响应客户端

SpringMVC 工作原理了解吗?

Spring MVC 原理如下图所示:
在这里插入图片描述
流程说明(重要):

1.客户端(浏览器)发送请求, DispatcherServlet拦截请求。
2.DispatcherServlet 根据请求信息调用 HandlerMapping 。HandlerMapping 根据 uri 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
3.DispatcherServlet 调用 HandlerAdapter适配执行 Handler 。
4.Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServlet,ModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
5.ViewResolver 会根据逻辑 View 查找实际的 View。
6.DispaterServlet 把返回的 Model 传给 View(视图渲染)。
7.把 View 返回给请求者(浏览器)

统一异常处理怎么做?

推荐使用注解的方式统一异常处理,具体会使用到 @ControllerAdvice + @ExceptionHandler 这两个注解 。

@ControllerAdvice@ResponseBodypublic class GlobalExceptionHandler {    @ExceptionHandler(BaseException.class)    public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {      //......    }    @ExceptionHandler(value = ResourceNotFoundException.class)    public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {      //......    }}

这种异常处理方式下,会给所有或者指定的 Controller 织入异常处理的逻辑(AOP),当 Controller 中的方法抛出异常的时候,由被@ExceptionHandler 注解修饰的方法进行处理。

ExceptionHandlerMethodResolver 中 getMappedMethod 方法决定了异常具体被哪个被 @ExceptionHandler 注解修饰的方法处理异常。

@Nullable private Method getMappedMethod(Class<? extends Throwable> exceptionType) {  List<Class<? extends Throwable>> matches = new ArrayList<>();    //找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系  for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {   if (mappedException.isAssignableFrom(exceptionType)) {    matches.add(mappedException);   }  }    // 不为空说明有方法处理异常  if (!matches.isEmpty()) {      // 按照匹配程度从小到大排序   matches.sort(new ExceptionDepthComparator(exceptionType));      // 返回处理异常的方法   return this.mappedMethods.get(matches.get(0));  }  else {   return null;  } }

从源代码看出:getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。

Spring Boot

Spring Boot比Spring做了哪些改进?

1)Spring Boot可以建立独立的Spring应用程序;
2)内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了;
3)无需再像Spring那样搞一堆繁琐的xml文件的配置;
4)可以自动配置Spring。SpringBoot将原有的XML配置改为Java配置,将bean注入改为使用注解注入的方式(@Autowire),并将多个xml、properties配置浓缩在一个appliaction.yml配置文件中。
5)提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能;
6)整合常用依赖(开发库,例如spring-webmvc、jackson-json、validation-api和tomcat等),提供的POM可以简化Maven的配置。当我们引入核心依赖时,SpringBoot会自引入其他依赖。

SpringBoot核心配置文件是什么?

  • bootstrap(.yam或者.properties):bootstrap由父ApplicationContext加载的,比application优先加载,且boostrap里面的的属性不能被覆盖
  • application(.yam或者.properties):用于SpringBoot项目的自动化配置

SpringBoot有哪些方式可以实现热部署?

使用devtools启动热部署,添加devtools库,在配置文件中把spring.devtools.restart.enable设置为true
使用IDEA编辑器,勾上自动编译或者手动重新编译

SpringBoot中的监视器是什么?

SpringBootActuator是Spring启动框架中的重要功能之一,SpringBoot监视器可帮助你访问生产环境中正在运行的应用程序的当前状态,有几个指标必须在生产环境中进行检查和监控,集市一些外部应用程序可以正在使用这些服务来向相关人员触发警报消息,监视器模块公开了一组可直接作为HTTP url 访问的rest端点来检查状态

如何在SpringBoot中禁用Actuator端点安全性?

默认情况下,所有敏感的http端点都是安全的,只有具有Actuator角色的用户才能访问它们,安全性是使用标准的HttpServletRequest.isUserInRole方法实施的
我们可以使用management.security.enabled=false来禁用安全性
只有在执行机构端点在防火墙后访问时,才建议禁用安全性

如何使用SringBoot实现异常处理?

Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法,我们可以通过实现一个ControllerAdvice类,来处理控制器类抛出的所有异常

什么是WebSockets?

WebSocket是一种计算机通信协议,通过单个tcp连接提供全双工通信信道
WebSocket是双向的,使用WebSocket客户端或服务器可以发起消息发送
WebSocket是全双工的,客户端和服务器通信是相互独立的
单个tcp连接,初始连接使用http,然后将此连接升级到基于套接字的连接,然后这个单一连接用于所有未来的通信Light,与http相比,WebSocket消息i数据交换要轻得多

分布式事务的两阶段提交

第一阶段:准备阶段;第二阶段:提交阶段。

准备阶段

事务协调者(事务管理器)给每个参与者(资源管理器)发送 Prepare 消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的 redo 和 undo 日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

提交阶段:

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作。

@Mapper和@Repository区别

相同点:

  • @Mapper和@Repository都是作用在dao层接口,使得其生成代理对象bean,交给spring 容器管理
  • 对于mybatis来说,都可以不用写mapper.xml文件

不同点:

  • @Mapper不需要配置扫描地址,可以单独使用,如果有多个mapper文件的话,可以在项目启动类中加入@MapperScan(“mapper文件所在包”)
  • @Repository不可以单独使用,否则会报错误,要想用,必须配置扫描地址(@MapperScannerConfigurer)

@PathVariable

在路由中定义变量规则后,通常我们需要在处理方法(也就是@RequestMapping注解的方法)中获取这个URL的具体值,并根据这个值(例如用户名)做相应的操作,SpringMVC提供的@PathVariable可以帮助我们:
@RequestMapping(value=“/user/{username}”)
public String userProfile(@PathVariable(value=“username”) String username) {
return “user”+username;
}
在上面的例子中,当@Controller处理HTTP请求时,userProfile的参数username会自动设置为URL中对应变量username(同名赋值)的值。

@RequestParam

在SpringMVC框架中,可以通过定义@RequestMapping来处理URL请求。和@PathVariable一样,需要在处理URL的函数中获取URL中的参数,也就是?key1=value1&key2=value2这样的参数列表。通过注解@RequestParam可以轻松地将URL中的参数绑定到处理函数方法的变量中:一旦我们在方法中定义了@RequestParam变量,如果访问的URL中不带有相应的参数,就会抛出异常——这是显然的,Spring尝试帮我们进行绑定,然而没有成功。但有的时候,参数确实不一定永远都存在,这时我们可以通过定义required属性:@RequestParam(value = “username”,required = false)

@RequestParam和@PathVariable的相同点和区别

@RequestParam和@PathVariable都能够完成类似的功能——因为本质上,它们都是用户的输入,只不过输入的部分不同,一个在URL路径部分,另一个在参数部分。要访问一篇博客文章,这两种URL设计都是可以的:
通过@PathVariable,例如/blogs/1
通过@RequestParam,例如blogs?blogId=1
那么究竟应该选择哪一种呢?建议:
1、当URL指向的是某一具体业务资源(或资源列表),例如博客,用户时,使用@PathVariable
2、当URL需要对资源或者资源列表进行过滤,筛选时,用@RequestParam

@RequestPart和@RequestBody

@RequestPart 接收文件以及其他更为复杂的数据类型
比如 XXX(@RequestPart(“file”) MultipartFile file, @RequestPart(“userVO”) UserVO userVO) 复杂协议

@RequestBody
主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);而最常用的使用请求体传参的无疑是POST请求了,
所以使用@RequestBody接收数据时,一般都用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()
可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
RequestBody 接收的是请求体里面的数据;而RequestParam接收的是key-value里面的参数

@JsonProperty

注解主要用于实体类的属性上,作用可以简单的理解为在反序列化的时候给属性重命名(多一个名字来识别)

Mybatis

什么是 Mybatis?

1、Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。
2、MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
3、通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。(从执行 sql 到返回 result 的过程)。

Mybaits 的优点

1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML标签,支持编写动态 SQL 语句,并可重用。
2、与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;
3、很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC 支持的数据库 MyBatis 都支持)。
4、能够与 Spring 很好的集成;
5、提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护

MyBatis 框架的缺点

1、SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL 语句的功底有一定要求。
2、SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

MyBatis 框架适用场合

1、MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。
2、对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis 将是不错的选择。

#{}和${}的区别是什么?

#{}是预编译处理,KaTeX parse error: Expected 'EOF', got '#' at position 22: …替换。 Mybatis 在处理#̲{}时,会将 sql 中的#{…{}时,就是把${}替换成变量的值。
使用#{}可以有效的防止 SQL 注入,提高系统安全性。

当实体类中的属性名和表中的字段名不一样 ,怎么办?

第 1 种: 通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。

<select id=”selectorder” parametertype=”int” resultetype=me.gacl.domain.order”>select order_id id, order_no orderno ,order_price price formorders where order_id=#{id};</select>

第 2 种: 通过来映射字段名和实体类属性名的一一对应的关系。

<select id="getOrder" parameterType="int"resultMap="orderresultmap">select * from orders where order_id=#{id}</select><resultMap type=”me.gacl.domain.order” id=”orderresultmap”><!–用 id 属性来映射主键字段–><id property=”id” column=”order_id”><!–用 result 属性来映射非主键字段,property 为实体类属性名,column为数据表中的属性–><result property = “orderno” column =”order_no”/><result property=”price” column=”order_price” /></reslutMap>

模糊查询 like 语句该怎么写?

第 1 种:在 Java 代码中添加 sql 通配符。

string wildcardname = “%smi%”;list<name> names = mapper.selectlike(wildcardname);<select id=”selectlike”>select * from foo where bar like #{value}</select>

第 2 种:在 sql 语句中拼接通配符,会引起 sql 注入

string wildcardname = “smi”;list<name> names = mapper.selectlike(wildcardname);<select id=”selectlike”>select * from foo where bar like "%"#{value}"%"</select>

通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?

Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个、、、标签,都会被解析为一个MapperStatement 对象。
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id 为
findStudentById 的 MapperStatement。
Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。

Mybatis 是如何进行分页的?分页插件的原理是什么?

Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。

Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式?

第一种是使用标签,逐一定义数据库列名和对象属性名之间的映射关系。
第二种是使用 sql 列的别名功能,将列的别名书写为对象属性名。有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。

如何执行批量插入?

首先,创建一个简单的 insert 语句:

<insert id=”insertname”>insert into names (name) values (#{value})</insert>

然后在 java 代码中像下面这样执行批处理插入:

list < string > names = new arraylist();names.add(“fred”);names.add(“barney”);names.add(“betty”);names.add(“wilma”);// 注意这里 executortype.batchsqlsession sqlsession =sqlsessionfactory.opensession(executortype.batch);try {namemapper mapper = sqlsession.getmapper(namemapper.class);for (string name: names) {mapper.insertname(name);}sqlsession.commit();} catch (Exception e) {e.printStackTrace();sqlSession.rollback();throw e;}finally {sqlsession.close();}

如何获取自动生成的(主)键值?

insert 方法总是返回一个 int 值 ,这个值代表的是插入的行数。如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。
示例:

<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”>insert into names (name) values (#{name})</insert>
name name = new name();name.setname(“fred”);int rows = mapper.insertname(name);// 完成后,id 已经被设置到对象中system.out.println(“rows inserted =+ rows);system.out.println(“generated key value =+ name.getid());

在 mapper 中如何传递多个参数?

1、第一种:
DAO 层的函数

public UserselectUser(String name,String area);

对应的 xml,#{0}代表接收的是 dao 层中的第一个参数,#{1}代表 dao 层中第二参数,更多参数一致往后加即可。

<select id="selectUser"resultMap="BaseResultMap">select * fromuser_user_t whereuser_name = #{0}anduser_area=#{1}</select>

2、第二种: 使用 @param 注解:

public interface usermapper {user selectuser(@param(“username”) stringusername,@param(“hashedpassword”) string hashedpassword);}

然后,就可以在 xml 像下面这样使用(推荐封装为一个 map,作为单个参数传递给mapper):

<select id=”selectuser” resulttype=”user”>select id, username, hashedpasswordfrom some_tablewhere username = #{username}and hashedpassword = #{hashedpassword}</select>

3、第三种:多个参数封装成 map

try {//映射文件的命名空间.SQL 片段的 ID,就可以调用对应的映射文件中的SQL//由于我们的参数超过了两个,而方法中只有一个 Object 参数收集,因此我们使用 Map 集合来装载我们的参数Map < String, Object > map = new HashMap();map.put("start", start);map.put("end", end);return sqlSession.selectList("StudentID.pagination", map);} catch (Exception e) {e.printStackTrace();sqlSession.rollback();throw e;} finally {MybatisUtil.closeSqlSession();}

Mybatis 动态 sql 有什么用?执行原理?有哪些动态 sql?

Mybatis 动态 sql 可以在 Xml 映射文件内,以标签的形式编写动态 sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接 sql 的功能。
Mybatis 提供了 9 种动态 sql 标签:trim | where | set | foreach | if | choose| when | otherwise | bind。

Xml 映射文件中,除了常见的 select|insert|updae|delete标签之外,还有哪些标签?

、、、、
,加上动态 sql 的 9 个标签,其中为 sql 片段标签,通过标签引入 sql 片段,为不支持自增的主键生成策略标签

Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复?

不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;原因就是 namespace+id 是作为 Map<String, MapperStatement>的 key
使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同namespace+id 自然也就不同

为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?

Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。

一对一、一对多的关联查询 ?

<mapper namespace="com.lcb.mapping.userMapper"><!--association 一对一关联查询 -->第 43 页 共 485 页<select id="getClass" parameterType="int"resultMap="ClassesResultMap">select * from class c,teacher t where c.teacher_id=t.t_id andc.c_id=#{id}</select><resultMap type="com.lcb.user.Classes" id="ClassesResultMap"><!-- 实体类的字段名和数据表的字段名映射 --><id property="id" column="c_id"/><result property="name" column="c_name"/><association property="teacher"javaType="com.lcb.user.Teacher"><id property="id" column="t_id"/><result property="name" column="t_name"/></association></resultMap><!--collection 一对多关联查询 --><select id="getClass2" parameterType="int"resultMap="ClassesResultMap2">select * from class c,teacher t,student s where c.teacher_id=t.t_idand c.c_id=s.class_id and c.c_id=#{id}</select><resultMap type="com.lcb.user.Classes" id="ClassesResultMap2"><id property="id" column="c_id"/><result property="name" column="c_name"/><association property="teacher"javaType="com.lcb.user.Teacher"><id property="id" column="t_id"/><result property="name" column="t_name"/></association><collection property="student"ofType="com.lcb.user.Student"><id property="id" column="s_id"/><result property="name" column="s_name"/></collection></resultMap></mapper>

MyBatis 实现一对一有几种方式?具体怎么操作的?

有联合查询和嵌套查询

联合查询是几个表联合查询,只查询一次, 通过在resultMap 里面配置 association 节点配置一对一的类就可以完成;

嵌套查询是先查一个表,根据这个表里面的结果的 外键 id,去再另外一个表里面查询数据,也是通过 association 配置,但另外一个表的查询通过 select 属性配置。

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,支持延迟加载的原理都是一样的。

什么是 MyBatis 的接口绑定?有哪些实现方式?

接口绑定,就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL 语句绑定, 我们直接调用接口方法就可以,这样比起原来了 SqlSession 提供的方法我们可以有更加灵活的选择和设置。

接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上@Select、@Update 等注解,里面包含 Sql 语句来绑定;另外一种就是通过 xml里面写 SQL 来绑定, 在这种情况下,要指定 xml 映射文件里面的 namespace 必须为接口的全路径名。当 Sql 语句比较简单时候,用注解绑定, 当 SQL 语句比较复杂时候,用 xml 绑定,一般用 xml 绑定的比较多。

使用 MyBatis 的 mapper 接口调用时有哪些要求?

1、Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同;
2、Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的parameterType 的类型相同;
3、Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的resultType 的类型相同;
4、Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径。

Mapper 编写有哪几种方式?

第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写mapper 接口,mapper 接口实现类、mapper.xml 文件。
1、在 sqlMapConfig.xml 中配置 mapper.xml 的位置

<mappers><mapper resource="mapper.xml 文件的地址" /><mapper resource="mapper.xml 文件的地址" /></mappers>

1、定义 mapper 接口
3、实现类集成 SqlSessionDaoSupport
mapper 方法中可以 this.getSqlSession()进行数据增删改查。
4、spring 配置

<bean id=" " class="mapper 接口的实现"><property name="sqlSessionFactory"ref="sqlSessionFactory"></property></bean>

第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean:
1、在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和mappre 接口的名称相同且在同一个目录,这里可以不用配置

<mappers><mapper resource="mapper.xml 文件的地址" /><mapper resource="mapper.xml 文件的地址" /></mappers>

2、定义 mapper 接口:
1、mapper.xml 中的 namespace 为 mapper 接口的地址
2、mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致
3、Spring 中定义

<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean"><property name="mapperInterface" value="mapper 接口地址" /><property name="sqlSessionFactory"ref="sqlSessionFactory" /></bean>

第三种:使用 mapper 扫描器:
1、mapper.xml 文件编写:
mapper.xml 中的 namespace 为 mapper 接口的地址;
mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致;
如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml
中进行配置。
2、定义 mapper 接口:
注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录
3、配置 mapper 扫描器:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><property name="basePackage" value="mapper 接口包地址"></property><property name="sqlSessionFactoryBeanName"value="sqlSessionFactory"/></bean>

4、使用扫描器后从 spring 容器中获取 mapper 的实现对象。

简述 Mybatis 的插件运行原理,以及如何编写一个插件

Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种接口的插件,Mybatis 使用 JDK 的动态代
理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 invoke()方法,当然,只会拦截那些你指定需要拦截的方法。

编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。

JDBC连接数据库的开发步骤

  1. 加载数据库驱动
  2. 获取数据连接对象
  3. 获取语句对象
    会话对象有两种Statement和PreparedStatement执行语句,他们区别是?
    PreparedStatement在执行之前会进行预编译
    效率高于Statement,且能够有效防止SQL注入
    PreparedStatement支持?占位符而不是直接拼接,提高可读性
  4. 处理结果集
  5. 关闭资源
    rs.close()、st.close()、conn.close() 注意关闭顺序以及处理异常

Mybatis加载的流程

  • 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心
  • SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得
  • SqlSessionFactoryBuilder 可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出SqlSessionFactory 实例
  • 工厂设计模式里面 需要获取SqlSession ,里面提供了在数据库执行 SQL 命令所需的所有方法

Dao 接口

通常⼀个 Xml 映射⽂件,都会写⼀个 Dao 接⼝与之对应,请问,这个 Dao 接⼝的⼯作原理是什么?Dao 接⼝⾥的⽅法,参数不同时,⽅法能重载吗?
Dao 接⼝,就是⼈们常说的 Mapper 接⼝,接⼝的全限名,就是映射⽂件中的 namespace的值,接⼝的⽅法名,就是映射⽂件中 MappedStatement 的 id 值,接⼝⽅法内的参数,就是传递给 sql 的参数。 Mapper 接⼝是没有实现类的,当调⽤接⼝⽅法时,接⼝全限名+⽅法名拼接字符串作为 key 值,可唯⼀定位⼀个 MappedStatement
举例: com.mybatis3.mappers.StudentDao.findStudentById ,可以唯⼀找到 namespace
为 com.mybatis3.mappers.StudentDao 下⾯ id = findStudentById 的 MappedStatement 。在 Mybatis
中,每⼀个 、 、 、 标签,都会被解析为⼀个 MappedStatement 对象。
Dao 接⼝⾥的⽅法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao 接⼝的⼯作原理是 JDK 动态代理,Mybatis 运⾏时会使⽤ JDK 动态代理为 Dao 接⼝⽣成代理 proxy 对象,代理对象 proxy 会拦截接⼝⽅法,转⽽执⾏ MappedStatement 所代表的 sql,然后将 sql 执⾏结果返回。

mybatis3.x 防止sql注入

# { }和${}的区别是什么?
#{}是预编译处理,${}是字符串替换。Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;Mybatis在处理${}时,就是把${}替换成变量的值。使用#{}可以有效的防止SQL注入,提高系统安全性

XML映射文件标签

select|insert|updae|delete 标签之外,还有哪些标签?
、 、 、 、 ,加上动态 sql 的 9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind 等,其中为 sql ⽚段标签,通过 标签引⼊ sql ⽚段, 为不⽀持⾃增的主键⽣成策略标签。

动态sql

Mybatis 动态 sql 可以让我们在 Xml 映射⽂件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,Mybatis 提供了 9 种动态 sql 标签trim|where|set|foreach|if|choose|when|otherwise|bind 。

Myabtis多级缓存

有没用过Mybatis一级缓存,能否介绍下
一级缓存的作用域是SQLSession,同一个SqlSession中执行相同的SQL查询(相同的SQL和参数),第一次会去查询数据库并写在缓存中,第二次会直接从缓存中取
基于PerpetualCache 的 HashMap本地缓存
默认开启一级缓存
失效策略:当执行SQL时候两次查询中间发生了增删改的操作,即insert、update、delete等操作commit后会清空该SQLSession缓存; 比如sqlsession关闭,或者清空等

有没用过Mybatis二级缓存,能否介绍下
二级缓存是namespace级别的,多个SqlSession去操作同一个namespace下的Mapper的sql语句,多个SqlSession可以共用二级缓存,如果两个mapper的namespace相同,(即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中,但是最后是每个Mapper单独的名称空间)

基于PerpetualCache 的 HashMap本地缓存,可自定义存储源,如 Ehcache/Redis等

默认是没有开启二级缓存

操作流程:
第一次调用某个namespace下的SQL去查询信息,查询到的信息会存放该mapper对应的二级缓存区域。
第二次调用同个namespace下的mapper映射文件中,相同的sql去查询信息,会去对应的二级缓存内取结果
失效策略:执行同个namespace下的mapepr映射文件中增删改sql,并执行了commit操作,会清空该二级缓存

注意:实现二级缓存的时候,MyBatis建议返回的POJO是可序列化的, 也就是建议实现Serializable接口
缓存淘汰策略:会使用默认的 LRU 算法来收回(最近最少使用的)

一级缓存和二级缓存同时启用,查询顺序是怎样的?
优先查询二级缓存-》查询一级缓存-》数据库

Mybatis3.X 懒加载

什么是Mybatis3.X的懒加载?

  • 按需加载,先从单表查询,需要时再从关联表去关联查询,能大大提高数据库性能, 并不是所有场景下使用懒加载都能提高效率

哪些查询配置支持懒加载

  • resultMap里面的association、collection有延迟加载功能
<resultMap id="VideoOrderResultMapLazy" type="VideoOrder">        <id column="id" property="id"/>        <result column="user_id" property="userId"/>        <result column="out_trade_no" property="outTradeNo"/>        <result column="create_time" property="createTime"/>        <result column="state" property="state"/>        <result column="total_fee" property="totalFee"/>        <result column="video_id" property="videoId"/>        <result column="video_title" property="videoTitle"/>        <result column="video_img" property="videoImg"/><!-- select: 指定延迟加载需要执行的statement id column: 和select查询关联的字段-->        <association property="user" javaType="User" column="user_id" select="findUserByUserId"/></resultMap>    <!--一对一管理查询订单, 订单内部包含用户属性  懒加载--><select id="queryVideoOrderListLazy" resultMap="VideoOrderResultMapLazy">        select         o.id id,         o.user_id ,         o.out_trade_no,         o.create_time,         o.state,         o.total_fee,         o.video_id,         o.video_title,         o.video_img         from video_order o</select><select id="findUserByUserId" resultType="User">       select  * from user where id=#{id}</select>

parameterType

parameterType为输入参数,在配置的时候,配置相应的输入参数类型即可。parameterType有基本数据类型和复杂的数据类型配置。
1.基本数据类型,如输入参数只有一个,其数据类型可以是基本的数据类型,也可以是自己定的类类型。包括int,String,Integer,Date,如下:

select from user where id = #{id}

JPA

Netty

认证权限

Cookie、Session

说下Cookie和Session的区别和联系
cookie数据保存在客户端,session数据保存在服务端
cookie不是很安全,容易泄露,不能直接明文存储信息
Cookie大小和数量存储有限制

你们公司C端业务登录的是怎样做的(业务量大,集群部署)
部分业务是采用redis替代本身的tomcat单机session (业务需要高度可控)
还有其他业务是使用JSON Web token (C端普通业务)

JWT

JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名

JWT格式组成 头部、负载、签名

header+payload+signature
头部:主要是描述签名算法
负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
简单来说: 就是通过一定规范来生成token,然后可以通过解密算法逆向解密token,这样就可以获取用户信息
为啥使用这个呢,有什么优缺点
优点

  • 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
  • 存储在客户端,不占用服务端的内存资源,使用加解密的方式进行校验,在分布式业务中能较好的提高性能和节省空间

缺点

  • token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如用户权限,密码等
  • 如果没有服务端存储,则不能做登录失效处理,除非服务端改秘钥

生成的token,在客户端或者浏览器是怎么存储的
可以存储在cookie,localstorage和sessionStorage里面

JWT令牌刷新机制

问题来源
JWT令牌保存在客户端,会存在过期时间,那么如果令牌一直没有变化,那么过期时间也不会发生变化。假设一个JWT令牌的过期时间是5天,
但是用户在这5天内一直在使用本系统,那么理论上当到了第五天的时候就应该是自动对这个令牌进行续期操作,而不是让用户重新登录。
解决办法
双令牌机制
设置长短日期的两个令牌,两个令牌都传给客户端,客户端每次携带两个令牌请求
当两个令牌都没有过期的时候,服务端正常验证逻辑
如果短令牌过期,长令牌没有过期,那么服务端重新生成两个新的令牌返回给客户端,客户端下次就带着新的令牌请求,完成了令牌的自动刷新。
缓存令牌机制
服务端不仅将令牌返回给客户端,同时将令牌缓存到Redis中,缓存时间是客户端令牌的过期时间的一倍
如果客户端令牌过期了,但是Redis中的没有过期,那么就生成一个新的令牌返回给客户端,完成自动的令牌续期
如果两者都过期了,那么就让用户重新登录。

SSO

SSO(Single Sign On)即单点登录说的是⽤户登陆多个⼦系统的其中⼀个就有权访问与其相关的其他系统。举个例⼦我们在登陆了京东⾦融之后,我们同时也成功登陆京东的京东超市、京东家电等⼦系统

OAuth2

OAuth 是⼀个⾏业的标准授权协议,主要⽤来授权第三⽅应⽤获取有限的权限。⽽ OAuth 2.0是 对 OAuth 1.0 的完全重新设计,OAuth 2.0更快,更容易实现,OAuth 1.0 已经被废弃。实际上它就是⼀种授权机制,它的最终⽬的是为第三⽅应⽤颁发⼀个有时效性的令牌 token,使得第三⽅应⽤能够通过该令牌获取相关的资源。
OAuth 2.0 比较常⽤的场景就是第三⽅登录,当你的⽹站接⼊了第三⽅登录的时候⼀般就是使⽤的 OAuth 2.0 协议。另外,现在OAuth 2.0也常⻅于⽀付场景(微信⽀付、⽀付宝⽀付)和开发平台(微信开放平
台、阿⾥开放平台等等)。

分布式

分布式事务实现

1)基于XA协议的两阶段提交(2PC)XA 规范主要 定义了 ( 全局 ) 事务管理器 ( Transaction Manager ) 和 ( 局部 ) 资源管理器 (Resource Manager ) 之间的接口。

2)两阶段提交
事务的提交分为两个阶段:
预提交阶段(Pre-Commit Phase)
决策后阶段(Post-Decision Phase)

3)补偿事务(TCC)
针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段
Try 阶段主要是对业务系统做检测及资源预留
Confirm 阶段主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,
默认Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。
Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放

4)本地消息表(MQ 异步确保)
其基本的设计思想是将远程分布式事务拆分成一系列的本地事务。

5)MQ 事务消息
有一些第三方的 MQ 是支持事务消息的,比如 RocketMQ,他们支持事务消息的方式也是类似
于采用的二阶段提交,但是市面上一些主流的 MQ 都是不支持事务消息的,比如 RabbitMQ 和
Kafka 都不支持。

6)Sagas 事务模型
该模型其核心思想就是拆分分布式系统中的长事务为多个短事务,或者叫多个本地事务,然后由
Sagas 工作流引擎负责协调,如果整个流程正常结束,那么就算是业务成功完成,如果在这过程
中实现失败,那么Sagas工作流引擎就会以相反的顺序调用补偿操作,重新进行业务回滚。

7)其他补偿方式
加入详细日志记录的,一旦系统内部引发类似致命异常,会有邮件通知。同时,后台会有定时任务扫描和分析此类日志,检查出这种特殊的情况,会尝试通过程序来补偿并邮件通知相关人员。在某些特殊的情况下,还会有 “人工补偿” 的,这也是最后一道屏障。

JDBC如何实现事务

在JDBC中处理事务,都是通过Connection完成的。
同一事务中所有的操作,都在使用同一个Connection对象。
Connection的三个方法与事务有关:
setAutoCommit(boolean):设置是否为自动提交事务,如果true(默认值为true)表示自动提交,也就是每条执行的SQL语句都是一个单独的事务,如果设置为false,那么相当于开启了事务了;
con.setAutoCommit(false) 表示开启事务。
commit():提交结束事务。
rollback():回滚结束事务。

什么情况下需要用到分布式事务?

a)当本地数据库断电、机器宕机、网络异常、消息丢失、消息乱序、 数据错误、不可靠TCP、存储数据丢失、其他异常等需要用到分布式事务。
b)例如:当本地事务数据库断电的这种情况,如何保证数据一致性?数据库由连个文件组成的,一个数据库文件和一个日志文件,数据库任何写入操作都要先写日志,在操作前会把日志文件写入磁盘,那么断电的时候及时才做没有完成,在重启数据库的时候,数据库会根据当前数据情况进行 undo 回滚活 redo 前滚, 保证了数据的强一致性。
c)分布式理论:当单个数据库性能产生瓶颈的时候,可能会对数据 库进行分区(物理分区),分区之后不同的数据库不同的服务器 上 ,此时单个数据库的 ACID 不适应这种情况啊,在此集群环境下很难达到集群的 ACID,甚至效率性能大幅度下降,重要的是再很难扩展新的分区了。此时就需要引用一个新的理论来使用这种集群情况:CAP 定理

什么是中间件?

中间件是处于操作系统和应用程序之间软件,使用时往往是一组中间件集成在一起,构成一个平台(开发平台+运行平台),在这组中间件中必须要有一个通信中间件,即中间件=平台+通信。该定义也限定了只有用于分布式系统中才能称为中间件。
主要分类:远程过程调用、面向消息的中间件、对象请求代理、事物处理监控。

说说CAP原理

CAP原理指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),最多只能同时三个特性中的两个,三者不可兼得。
一致性指:在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态
可用性是指:系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果
分区容错性指:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障

如:zk集群保证了CP,redis集群保证了AP,因此在实现分布式锁的时候,如果使用zk实现,那么当master宕机之后,集群需要重新进行选举,此时集群短暂不可用。而如果使用redis实现分布式锁,因为redis保证AP,所有当master宕机之后,如果master数据没同步,可能会产生数据一致性的问题,,导致有多个应用获取到分布式锁。

分布式应用的登录校验解决方案有哪几种?

  • 方案一
    • 实的应用不可能单节点部署,所以就有个多节点登录session共享的问题需要解决
    • tomcat支持session共享,但是有广播风暴;用户量大的时候,占用资源就严重,不推荐
  • 方案二
    • 使用redis存储token:
      • 服务端使用UUID生成随机64位或者128位token,放入redis中,然后返回给客户端并存储在cookie中
      • 用户每次访问都携带此token,服务端去redis中校验是否有此用户即可
  • 方案三
    • JWT,即 json wen token
    • 什么是JWT
      • JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名
      • 简单来说: 就是通过一定规范来生成token,然后可以通过解密算法逆向解密token,这样就可以获取用户信息

优点:生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库存储在客户端,不占用服务端的内存资源
缺点:token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如用户权限,密码等,如果没有服务端存储,则不能做登录失效处理,除非服务端改秘钥

Zookeeper

ZooKeeper 是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

Zookeeper 保证了如下分布式一致性特性:
1、顺序一致性
2、原子性
3、单一视图
4、可靠性
5、实时性(最终一致性)
客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求,这些请求会同时发给其他 zookeeper 机器并且达成一致后,请求才会返回成功。因此,随着 zookeeper 的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳,这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个zookeeper 最新的 zxid。

ZooKeeper 提供了什么?

1、文件系统
2、通知机制

Zookeeper 文件系统

Zookeeper 提供一个多层级的节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为1M

四种类型的数据节点 Znode

1、PERSISTENT-持久节点
除非手动删除,否则节点一直存在于 Zookeeper 上

2、EPHEMERAL-临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper 连接断开不一定会话失效),那么这个客户端创建的所有临时节点都会被移除。

3、PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。

4、EPHEMERAL_SEQUENTIAL-临时顺序节点
基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的
自增整型数字。

Zookeeper Watcher 机制 – 数据变更通知

Zookeeper 允许客户端向服务端的某个 Znode 注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。
工作机制:
1、客户端注册 watcher
2、服务端处理 watcher
3、客户端回调 watcher
Watcher 特性总结:
1、一次性
无论是服务端还是客户端,一旦一个 Watcher 被触发,Zookeeper 都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。
2、客户端串行执行
客户端 Watcher 回调的过程是一个串行同步的过程。
3、轻量
3.1、Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。
3.2、客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象实体传递到服务端,仅仅是在客户端请求中使用 boolean 类型属性进行了标记。
4、watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket 进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。
Zookeeper 只能保证最终的一致性,而无法保证强一致性。
5、注册 watcher getData、exists、getChildren
6、触发 watcher create、delete、setData
7、当一个客户端连接到一个新的服务器上时,watch 将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到 watch 的。而当 client 重新连接时,如果需要的话,所有先前注册过的 watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch 可能会丢失:对于一个未创建的 znode的 exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 watch 事件可能会被丢失。

服务器角色

Leader
1、事务请求的唯一调度和处理者,保证集群事务处理的顺序性
2、集群内部各服务的调度者

Follower
1、处理客户端的非事务请求,转发事务请求给 Leader 服务器
2、参与事务请求 Proposal 的投票
3、参与 Leader 选举投票

Observer
1、3.0 版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力
2、处理客户端的非事务请求,转发事务请求给 Leader 服务器
3、不参与任何形式的投票

Zookeeper 的 java 客户端都有哪些?

java 客户端:zk 自带的 zkclient 及 Apache 开源的 Curator。

说几个 zookeeper 常用的命令。

常用命令:ls get set create delete 等。

zookeeper 简单介绍一下,为什么要用 zk?zk 的架构?zab?

zk 介绍及功能:
Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。ZooKeeper 本质上是一个分布式的小文件存储系统。提供基于类似于文件系统的目录树方
式的数据存储,并且可以对树中的节点进行有效管理。从而用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。 诸如:统一命名服务(dubbo)、分布式配置管理(solr 的配置集中管理)、分布式消息队列(sub/pub)、分布式锁、分布式协调等功能。

zk 架构:
Leader:
Zookeeper 集群工作的核心;
事务请求(写操作) 的唯一调度和处理者,保证集群事务处理的顺序性; 集群内部各个服务器的调度者。对于 create, setData, delete 等有写操作的请求,则需要统一转发给 leader 处理,leader 需要决定编号、执行操作,这个过程称为一个事务。
Follower:
处理客户端非事务(读操作) 请求,转发事务请求给 Leader;参与集群 Leader 选举投票 2n-1 台可以做集群投票。
此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者角色。
Observer:
观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给 Leader 服务器进行处理。不会参与任何形式的投票只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。

简答:说白了就是增加并发的读请求

ZAB 协议全称:Zookeeper Atomic Broadcast(Zookeeper 原子广播协议)。
ZAB 协议是专门为 zookeeper 实现分布式协调功能而设计。zookeeper 主要是根据 ZAB协议是实现分布式系统数据一致性。
zookeeper 根据 ZAB 协议建立了主备模型完成 zookeeper 集群中数据的同步。这里所说的主备系统架构模型是指,在 zookeeper 集群中,只有一台 leader 负责处理外部客户端的事物请求(或写操作),然后 leader 服务器将客户端的写操作数据同步到所有的 follower节点中。

Kafka

Kafka:
高吞吐量、低延迟:kafka 每秒可以处理几十万条消息,它的延迟最低只有几毫秒;
可扩展性:kafka 集群支持热扩展;
持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失;
容错性:允许集群中节点故障(若副本数量为 n,则允许 n-1 个节点故障);
高并发:支持数千个客户端同时读写。

Kafka 应用场景:
日志收集:一个公司可以用 Kafka 可以收集各种服务的 log,通过 kafka 以统一接口服务的方式开放给各种 consumer;
消息系统:解耦生产者和消费者、缓存消息等;
用户活动跟踪:kafka 经常被用来记录 web 用户或者 app 用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到 kafka 的 topic 中,然后消费者通过订阅这些 topic 来做实时的监控分析,亦可保存到数据库;
运营指标:kafka 也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告;
流式处理:比如 spark streaming 和 flink。

kafka可以脱离zookeeper单独使用吗?为什么?

Kafka不能脱离zookeeper单独使用,因为kafka使用zookeeper管理和协调kafka的节点服务器(3.0可以脱离zookeeper)

kafka有几种数据保留的策略?

kafka有两种数据保存策略:按照过期时间保留和按照存储的消息大小保留

kafka同时设置了七天和10G清除数据,到达第五天的时候消息到达了101G,这个时候kafka将如何处理?

这个时候kafka会执行数据清除工作,时间和大小不论那个满足条件,都会清空数据

什么情况会导致kafka运行变慢?

  • cpu性能瓶颈
  • 磁盘读写瓶颈
  • 网络瓶颈

使用kafka集群需要注意什么?

  • 集群的数量不是越多越好,最好不要超过7个,因为节点越多,消息复制需要的时间就越长,整个集群的吞吐量就越低
  • 集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高

kafka的设计是什么样的呢?

kafka将消息以topic为单位进行归纳
将向kafka topic发布消息的程序称为producers
将预定topics并消费消息的程序称为consumer
kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker
producers通过网络将消息发送到kafka集群,集群向消费者提供消息

数据传输的事务定义为哪三种?

数据传输的事务定义通常有以下三种级别:

  • 最多一次:消息不会被重复发送,最多被传输一次,但也有可能一次不传输
  • 最少一次:消息不会被漏发送,最少被传输一次,但也有可能被重复发送
  • 精确的一次(Exactly once):不会漏传输也不会重复传输,每个消息都被传输一次而且仅仅被传输一次,这是大家所期望的

kafka判断一个节点是否还存活有那两个条件?

  • 节点必须可以维护和zookeeper的连接,zookeeper通过心跳机制检查每个节点的连接
  • 如果节点是个follower,它必须能及时得同步leader的写操作,延时不能太久

producer是否直接将数据发送到broker的leader(主节点)?

producer直接将数据发送到broker的leader(主节点),不需要再多个节点进行分发,为了帮助prodecer做到这点,所有kafka节点都可以及时的告知,那些节点是活动的,目标topic目标分区的leader在哪,这样producer就可以直接将消息发送到目的地了

kafka consumer是否可以消费指定分区消息?

kafka consumer消费消息时,向broker发出"fetch"请求去消费特定分许的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,consumer拥有了offset的控制权,可以向后回滚去重新消费之前的消息

kafka消息是采用pull模式,还是push模式

kafka最初考虑的问题是,consumer应该从broker拉去消息还是broker将消息推送到consumer,也就是pull还是push,在这方面,kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息

一些消息系统比如scribe和 apache flume 采用了push模式,将消息推送到下游的consumer,这样做有好处也有坏处,由broker决定消息的速率,对于不同消费塑料的consumer就不太好处理了,消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是.push模式下,当broker推送的速率远大于comsumer消费的速率时,consumer恐怕就要崩溃了,最终kafka还是选取了传统的pull模式

pull模式的另外个好处是consumer可自主决定是否批量的从broker拉去数据,push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送,如果为了避免consumer奔溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费,pull模式下,consumer就可以根据自己的消费能力去决定这些策略

pull有个缺点是,如果broker没有可供消费的消息,将大道至consumer不断在循环中轮询,知道新消息到达,为了避免这一些,kafka有个参数可以让consumer阻塞直到新的消息到达(当然也可以阻塞直到消息的数量达到某个特定的量这样就可以批量发)

kafka存储在硬盘上的消息格式是什么?

消息由一个固定长度的头部和可变长度的字节数组组成,头部包含了一个版本号和CRC32校验码
消息长度:4bytes
版本号:1byte
CRC校验码:4bytes
具体的消息:n bytes

kafka高效文件存储设计特点:

  1. kafka把topic中一个parition大文件分成多个小文件段,通过多个小文件段,就容易定期清除或删除已经消费完文件,减少磁盘占用
  2. 通过索引信息可以快速定位message和确定response的最大大小
  3. 通过index元数据全部映射到memory,可以避免segment file 的IO磁盘操作
  4. 通过索引文件稀疏存储,可以大幅降低index文件元数据占用空间大小

kafka与传统消息系统之间有三个关键区别

  1. kafka持久化文件,这些日志可以被重复读取和无限期保留
  2. kafka是一个分布式系统,它可以以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
  3. kafka支持实时的流式处理

partition的数据如何保存到硬盘

topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增,且消息有序
parition文件下有多个segment(xxx.index,xxx.log)
segment文件里的大小和配置文件大小一致可以根据要求修改,默认为1g
如果大小大于1g时,会滚动一个新的segemtn并且以上一个segmen最后一条消息的偏移量命名

kafka的ack机制

request.required.acks有三个值0,1,-1
0:生产者不会等带broker的ack,这个延迟最低但是存储的保证最弱,当server挂掉的时候就会丢数据
1:服务端会等待ack值leader副本确认接收到消息会发送ack但是如果leader挂掉后它不确保是否复制完成新laader也会导致数据丢失
01:同样在1的基础上,服务端会等所有follower的副本收到数据后才会收到leader发出的ack,这样数据不会丢失

kafka的消费者如何消费数据

消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置等到下次消费时,它会接着上次位置继续消费

消费者负载均衡策略

一个消费者组中的一个分片对应一个消费者成员,它能保证消费者成员都能访问,如果组中成员太多会有空闲的成员

数据有序

一个消费者组里它的内部是有序的,消费者组与消费者之间是无序的

kafka生产数据时数据的分组策略

生产者决定数据产生到集群的哪一个pariton中,每个消息都是以key value格式,key是由生产者发送数据传入,所以生产者(key)决定了数据产生到集群的那个parition

kafka新建的分区会在那个目录下创建

在启动kafka集群之前,我们需要配置好log.dirs参数,其值是kafka数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分割,通过这些目录是分布在不同的磁盘上用于提高读写性能
当然我们也可以配置log,dir参数,含义一样,只需要设置设其中一个即可
如果log.dirs参数只配置了一个目录,那么分配到各个broker上的分区肯定只能在这个目录下创建文件夹用于存放数据
但是如果log,dirs参数配置了多个目录,那么kafka会在那个文件夹中创建目录呢?
答案是:kafka会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为Topic名+分区ID,注意,是分区文件夹最少的目录,而不会磁盘使用量最少的目录,也就是说,如果你给log,dirs参数新增了一个新的磁盘,新的分区目录肯定实先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少的为之

kafka 数据分区和消费者的关系, kafka 的数据 offset 读取流程,kafka 内部如何保证顺序,结合外部组件如何保证消费者的顺序?

1 、 kafka 数据分区和消费者的关系: 1 个 partition 只能被同组的⼀个 consumer 消费,同组的 consumer 则起到均衡效果
2 、 kafka 的数据 offset 读取流程
1.连接 ZK 集群,从 ZK 中拿到对应 topic 的 partition 信息和 partition 的 Leader 的相关信息
2.连接到对应 Leader 对应的 broker
3.consumer 将⾃⼰保存的 offset 发送给 Leader
4.Leader 根据 offset 等信息定位到 segment (索引⽂件和⽇志⽂件)
5.根据索引文件中的内容,定位到日志文件中该偏移量对应的开始位置读取相应长度的数据并返回给 consumer

3 、 kafka 内部如何保证顺序:
kafka 只能保证 partition 内是有序的,但是 partition 间的有序是没办法的。爱奇艺的搜索架构,是从业务上把需要有序的打到同⼀个partition。

Elasticsearch

elasticsearch 的倒排索引是什么

解答:通俗解释一下就可以。
传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。
而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。
有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率

RPC

Dubbo

Dubbo的负载均衡策略有哪些?

主要有random(随机,这种是默认的负载均衡策略)、RoundRobin (轮询)、LeastActive (最少活跃数)、ConsistentHash(一致性hash)可以在暴露服务的时候使用loadbalance进行指定。
随机:在一个截面上碰撞的概率高,调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
轮询:存在慢的提供者累积请求的问题,一台机器很慢,但没挂,当请求轮询到那台机子就卡在那,久而久之,所有请求都卡在那台服务器上。
最少活跃数:使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
一致性hash:当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。可以方便节点的增加及移除

Dubbo服务调用超时怎么办?

dubbo在调用服务不成功时,默认是会重试两次的。这样在服务端的处理时间超过了设定的超时时间时,就会有重复请求,此时在接口设计的时候,需要考虑接口的幂等性,避免重复调用导致出现脏数据。

默认使用的是什么通信框架,还有别的选择吗?

默认也推荐使用 netty 框架,还有 mina。

说说Dubbo的运行机制、整体架构

Dubbo基于生产者、消费者的模式,
首先服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

消息队列

业务系统有没做消息的重复消费处理,是怎么做的?

幂等性:一个请求,不管重复来多少次,结果是不会改变的。
RabbitMQ、RocketMQ、Kafka等任何队列不保证消息不重复,如果业务需要消息不重复消费,则需要消费端处理业务消息要保持幂等性

方式一:Redis的setNX() , 做消息id去重 java版本目前不支持设置过期时间

//Redis中操作,判断是否已经操作过 TODOboolean flag = jedis.setNX(key);if(flag){//消费}else{//忽略,重复消费}

方式二:redis的 Incr 原子操作:key自增,大于0 返回值大于0则说明消费过,(key可以是消息的md5取值, 或者如果消息id设计合理直接用id做key)

int num = jedis.incr(key);if(num == 1){//消费}else{//忽略,重复消费}

方式三:数据库去重表

设计一个去重表,某个字段使用Message的key做唯一索引,因为存在唯一索引,所以重复消费会失败CREATE TABLE message_record (id int(11) unsigned NOT NULL AUTO_INCREMENT,key varchar(128) DEFAULT NULL,create_time datetime DEFAULT NULL,PRIMARY KEY (id),UNIQUE KEY key (key) )ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

消息队列里面有没用过延迟消息,使用场景是怎样的

什么是延迟消息:
Producer 将消息发送到消息队列 broker服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费
使用场景一:通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
使用场景二:消息生产和消费有时间窗口要求,比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略

你用过消息队列,引入队列有啥优缺点,对比其他消息中间产品,选择这款的原因是啥?

优点:解耦系统、异步化、削峰
缺点: 系统可用性降低、复杂度增高、维护成本增高
主流消息队列Apache ActiveMQ、Kafka、RabbitMQ、RocketMQ

  • ActiveMQ:http://activemq.apache.org/
    Apache出品,历史悠久,支持多种语言的客户端和协议,支持多种语言Java, .NET, C++ 等,基于JMS Provider的实现
    缺点:吞吐量不高,多队列的时候性能下降,存在消息丢失的情况,比较少大规模使用

  • Kafka:http://kafka.apache.org/
    是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理大规模的网站中的所有动作流数据(网页浏览,搜索和其他用户的行动),副本集机制,实现数据冗余,保障数据尽量不丢失;支持多个生产者和消费者
    缺点:不支持批量和广播消息,运维难度大,文档比较少, 需要掌握Scala

  • RabbitMQ:http://www.rabbitmq.com/
    是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不错
    缺点:使用Erlang开发,阅读和修改源码难度大

  • RocketMQ:http://rocketmq.apache.org/
    阿里开源的一款的消息中间件, 纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点, 性能强劲(零拷贝技术),支持海量堆积, 支持指定次数和时间间隔的失败消息重发,支持consumer端tag过滤、延迟消息等,在阿里内部进行大规模使用,适合在电商,互联网金融等领域使用
    缺点:成熟的资料相对不多,社区处于新生状态但是热度高

消息队列三个场景:解耦、异步、削峰

解耦

A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E系统也要这个数据呢?那如果 C系统现在不需要了呢?A 系统负责崩溃在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来? 如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护 这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。

异步

再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200= 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快!

削峰

一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟
最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。

消息发送方式

发送方式一般分三种

  • SYNC 同步发送
    应用场景:重要通知邮件、报名短信通知、营销短信系统等
  • ASYNC 异步发送
    应用场景:对RT时间敏感,可以支持更高的并发,回调成功触发相对应的业务,比如注册成功后通知积分系统发放优惠券
  • ONEWAY 无需要等待响应
    应用场景:主要是日志收集,适用于某些耗时非常短,但对可靠性要求并不高的场景, 也就是LogServer, 只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求 不等待应答
    发送方式汇总对比
    发送方式 发送 TPS 发送结果反馈 可靠性
    同步发送 快 有 不丢失
    异步发送 快 有 不丢失
    单向发送 最快 无 可能丢失

什么是延迟消息?

Producer 将消息发送到消息队列broker服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费
使用场景一:通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
使用场景二:消息生产和消费有时间窗口要求,定时关闭订单。比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略

你用的队列是否支持顺序消息,是怎么实现顺序消息的?

什么是顺序消息:
消息的生产和消费顺序一致
全局顺序:topic下面全部消息都要有序(少用),性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景,并行度成为消息系统的瓶颈, 吞吐量不够
使用场景:在证券处理中,以人民币兑换美元为例子,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 FIFO 的方式进行发布和消费
局部顺序:只要保证一组消息被顺序消费即可,性能要求高
使用场景:电商的订单创建,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息、订单交易成功消息 都会按照先后顺序来发布和消费(阿里巴巴集团内部电商系统均使用局部顺序消息,既保证业务的顺序,同时又能保证业务的高性能)
下面是用RocketMQ举例(用kafka或rabbitmq类似)
一个topic下面有多个queue
顺序发布:对于指定的一个 Topic,客户端将按照一定的先后顺序发送消息
举例:订单的顺序流程是:创建、付款、物流、完成,订单号相同的消息会被先后发送到同一个队列中,
根据MessageQueueSelector里面自定义策略,根据同个业务id放置到同个queue里面,如订单号取模运算再放到selector中,同一个模的值都会投递到同一条queue

   public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {            //如果是订单号是字符串,则进行hash,得到一个hash值          Long id = (Long) arg;          long index = id % mqs.size();          return mqs.get((int)index);   }

顺序消费:对于指定的一个 Topic,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。
举例:消费端要在保证消费同个topic里的同个队列,不应该用MessageListenerConcurrently,
应该使用MessageListenerOrderly,自带单线程消费消息,不能在Consumer端再使用多线程去消费,消费端分配到的queue数量是固定的,集群消费会锁住当前正在消费的队列集合的消息,所以会保证顺序消费。
注意:
顺序消息暂不支持广播模式
顺序消息不支持异步发送方式,否则将无法严格保证顺序
不能再Consumer端再使用多线程去消费

你的业务系统有没做消息的重复消费处理,是怎么做的

幂等性:一个请求,不管重复来多少次,结果是不会改变的。
RabbitMQ、RocketMQ、Kafka等任何队列不保证消息不重复,如果业务需要消息不重复消费,则需要消费端处理业务消息要保持幂等性

  • 方式一:Redis的setNX() , 做消息id去重 java版本目前不支持设置过期时间
//Redis中操作,判断是否已经操作过 TODOboolean flag =  jedis.setNX(key);if(flag){        //消费}else{        //忽略,重复消费}
  • 方式二:redis的 Incr 原子操作:key自增,大于0 返回值大于0则说明消费过,(key可以是消息的md5取值, 或者如果消息id设计合理直接用id做key)
int num =  jedis.incr(key);if(num == 1){    //消费}else{    //忽略,重复消费}
  • 方式三:数据库去重表
    设计一个去重表,某个字段使用Message的key做唯一索引,因为存在唯一索引,所以重复消费会失败
    CREATE TABLE message_record ( id int(11) unsigned NOT NULL AUTO_INCREMENT, key varchar(128) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY key (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

消息队列如何保证消息的可靠性

消息可靠性传输,是非常重要,消息如果丢失,可能带来严重后果,一般从三个角度去分析

  1. producer端:
  • 不采用oneway发送,使用同步或者异步方式发送
  • 做好重试,但是重试的Message key必须唯一,投递的日志需要保存,关键字段,投递时间、投递状态、重试次数、请求体、响应体
  1. broker端:
  • 多主多从架构,需要多机房
  • 同步双写、异步刷盘 (同步刷盘则可靠性更高,但是性能差点,根据业务选择)
  • 机器断电重启:异步刷盘,消息丢失;同步刷盘消息不丢失
  • 硬件故障:可能存在丢失,看队列架构
  1. consumer端
  • 必须手动ack,消息队列一般都提供的ack机制,发送者为了保证消息肯定消费成功,只有消费者明确表示消费成功,队列才会认为消息消费成功,中途断电、抛出异常等都不会认为成功——即都会重新投递,每次在确保处理完这个消息之后,在代码里调用ack,告诉消息队列消费成功
  • 消费端务必做好幂等性处理
  • 消息消费务必保留日志,即消息的元数据和消息体,

消息堆积了10小时,有几千万条消息待处理,现在怎么办?

修复consumer, 然后慢慢消费?也需要几小时才可以消费完成,新的消息怎么办?
核心思想:紧急临时扩容,更快的速度去消费数据

  • 修复Consumer不消费问题,使其恢复正常消费,根据业务需要看是否要暂停
  • 临时topic队列扩容,并提高消费者能力,但是如果增加Consumer数量,但是堆积的topic里面的message queue数量固定,过多的consumer不能分配到message queue
  • 编写临时处理分发程序,从旧topic快速读取到临时新topic中,新topic的queue数量扩容多倍,然后再启动更多consumer进行在临时新的topic里消费
  • 直到堆积的消息处理完成,再还原到正常的机器数量

API网关

数据库扩展:分库分表、读写分离

分布式id

基于数据库的实现方案 数据库⾃增 id 这个就是说你的系统⾥每次得到⼀个 id,都是往⼀个库的⼀个表⾥插⼊⼀条没什么业务含义的数据,然后获取⼀个数据库⾃增的⼀个 id。拿到这个 id 之后再往对应的分库分表⾥去写⼊。这个⽅案的好处就是⽅便简单,谁都会用;缺点就是单库⽣成⾃增 id,要是⾼并发的话,就会有瓶颈的;如果你硬是要改进⼀下,那么就专⻔开⼀个服务出来,这个服务每次就拿到当前 id 最⼤值,然后⾃⼰递增⼏个 id,⼀次性返回⼀批 id,然后再把当前最⼤ id 值修改成递增⼏个 id 之后的⼀个值;但是⽆论如何都是基于单个数据库。
适合的场景:你分库分表就俩原因,要不就是单库并发太⾼,要不就是单库数据量太⼤;除⾮是你并发不⾼,但是数据量太⼤导致的分库分表扩容,你可以⽤这个⽅案,因为可能每秒最⾼并发最多就⼏百,那么就⾛单独的⼀个库和表⽣成⾃增主键即可。
设置数据库 sequence 或者表⾃增字段步⻓ 可以通过设置数据库 sequence 或者表的⾃增字段步⻓来进⾏⽔平伸缩。
⽐如说,现在有 8 个服务节点,每个服务节点使⽤⼀个 sequence 功能来产⽣ ID,每个 sequence 的起始 ID 不同,并且依次递增,步⻓都是 8。
适合的场景:在⽤户防⽌产⽣的 ID 重复时,这种⽅案实现起来⽐较简单,也能达到性能⽬标。但是服务
节点固定,步⻓也固定,将来如果还要增加服务节点,就不好搞了。

UUID 好处就是本地⽣成,不要基于数据库来了;不好之处就是,UUID 太⻓了、占⽤空间⼤,作为主键性能太差了;更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产⽣部分顺序写),还有,由于在写的时候不能产⽣有顺序的 append 操作,⽽需要进⾏ insert 操作,将会读取整个 B+ 树节点到内存,在插⼊这条记录后会将整个节点写回磁盘,这种操作在记录占⽤空间⽐较⼤的情况下,性能下降明显。
适合的场景:如果你是要随机⽣成个什么⽂件名、编号之类的,你可以⽤ UUID,但是作为主键是不能⽤ UUID 的。
UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf

获取系统当前时间 这个就是获取当前时间即可,但是问题是,并发很⾼的时候,⽐如⼀秒并发⼏千,会有重复的情况,这个是肯定不合适的。基本就不⽤考虑了。
适合的场景:⼀般如果⽤这个⽅案,是将当前时间跟很多其他的业务字段拼接起来,作为⼀个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成⼀个全局唯⼀的编号。

snowflake 算法 snowflake 算法是 twitter 开源的分布式 id ⽣成算法,采⽤ Scala 语⾔实现,是把⼀个 64 位的 long 型的 id,1 个 bit 是不⽤的,⽤其中的 41 bit 作为毫秒数,⽤ 10 bit 作为⼯作机器 id,12 bit 作为序列号。1 bit:不⽤,为啥呢?因为⼆进制⾥第⼀个 bit 为如果是 1,那么都是负数,但是我们⽣成的 id 都是正数,所以第⼀个 bit 统⼀都是 0。 41 bit:表示的是时间戳,单位是毫秒。41 bit 可以表示的数字多达2^41 - 1,也就是可以标识 2^41 - 1 个毫秒值,换算成年就是表示69年的时间。 10 bit:记录⼯作机器id,代表的是这个服务最多可以部署在 2^10台机器上哪,也就是1024台机器。但是 10 bit ⾥ 5 个 bit代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 25个机房(32个机房),每个机房⾥可以代表25 个机器(32台机器)。 12 bit:这个是⽤来记录同⼀个毫秒内产⽣的不同 id,12 bit 可以代表的最⼤正整数是 2^12 - 1 = 4096,也就是说可以⽤这个 12 bit 代表的数字来区分同⼀个毫秒内的 4096 个不同的 id。

分布式接口幂等性

分布式限流

微服务

SpringCloud

springcloud 服务发现原理?

a. 每 30s 发送⼼跳检测重新进行租约,如果客户端不能多次更新租约,它将在 90s 内从服务器注册中心移除。
a. 注册信息和更新会被复制到其他 Eureka 节点,来自任何区域的客户端可以查找到注册中心信息, 每 30s 发生⼀次复制来定位他 们的服务,并进行远程调用。
b. 客户端还可以缓存⼀些服务实例信息,所以即使 Eureka 全挂掉,客户端也是可以定位到服务地址的。

介绍下 springcloud 各个组件? springcloud 的注册中心除了eureka 还可以用什么?

springcloud 由以下几个核心组件构成:
Eureka:各个服务启动时, Eureka Client 都会将服务注册到 Eureka Server,并且 EurekaClient 还可以反过来从 Eureka Server拉取注册表,从而知道其他服务在哪⾥
Ribbon:服务间发起请求的时候,基于 Ribbon 做负载均衡,从⼀个服务的多台机器中选择⼀台
Feign:基于 Feign 的动态代理机制,根据注解和选择的机器,拼接请求 URL 地址,发起请求
Hystrix:发起请求是通过 Hystrix 的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
Zuul:如果前端、移动端要调用后端系统,统⼀从 Zuul网关进⼊,由 Zuul⽹关转发请求给对应的服务 注册中心还可以用zookeeper。

设计模式

单例模式

使用场景:单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象(比如数据源,session工厂),使用单例模式可以提高系统性能

单例设计模式八种方式:
饿汉式(静态常量):基于类加载机制,避免了多线程的同步问题,但是也导致了没有达到懒加载效果,从而造成性能浪费
饿汉式(静态代码块):同上

懒汉式(线程不安全):起到了懒加载的目的,但是不能保证多个线程同时进入if判断,所以线程不安全,不要使用这种方式

if(instance==null){instance = new Singleton();}

懒汉式(线程安全,同步方法):解决了线程安全问题,不过由于对静态方法加锁实际上是对整个类加锁,效率不高
懒汉式(同步代码块):不推荐使用
双重检查:延迟加载效率高,推荐使用
静态内部类:采用了类加载机制来保证实例时只有一个线程,JVM保证了线程的安全性
枚举:不仅避免多线程同步问题,而且还能防止反序列化重新创建新的对象

单例模式的线程安全性

首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境 下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全

手写饿汉式单例

public class Singleton {   // 必须使用volatile关键字   private static volatile Singleton instance = null;   //构造必须私有化   private Singleton(){}   public static Singleton getInstance() {    //双重检查    if (instance == null) { synchronized (Singleton.class) {  if (instance == null) {  instance = new Singleton(); } }   }    return instance;  }}

为什么要使用volatile关键字?
答:可以避免指令重排序,即使使用双重检锁,但是如果不用volatile关键字修饰,在运行过程中,可能因为指令重排序而导致系统产生多个实例

Linux

如何创建比普通用户权限高,又不能高于root用户权限?

工作中,从安全的角度上出发,时常不给使用超级管理员root用户,但是普通用户的权限又不能满足实际工作中使用,所以就有一种需求,使得一个用户的权限比普通用户高,而又不能比root用户权限高,可以创建一个普通用户赋予root组的权限,请写出创建的命令

创建一个user1用户,使他拥有root组的权限
useradd user1 && usermod -G root user1

巧用Linux的定时器

背景:数据库备份是件非常重要的事情,需要每时每刻备份,此时可以利用linux 的定时任务,轻松搞定
在3月份内,每天早晨6点到18点这个时间段,每隔2小时执行一次/usr/bin/mysql_backup.sh
0 6-18/2 * 3 * /usr/bin/mysql_backup.sh

监控系统告警磁盘空间不足,如何处理?

背景:服务器监控告警,某个文件系统磁盘空间使用率高达95%,此时必须清理出空间,否则面临着程序日志无法写入或者程序运行所需使用空间,将导致程序挂死,服务宕掉风险。若是用手工进入对应路径,进行查找一段时间前的没用日志并删除,效率极低,请解决这个问题?
请找出最后创建时间是10天前,后缀是*.log的文件并删除
答案:
find / -ctime +10 -a -type f -name “*.log” -exec rm -rf {} ;

有没了解 Docker, Docker 和虚拟机有什么区别?

(1)虚拟机:我们传统的虚拟机需要模拟整台机器包括硬件,每台虚拟机都需要有自己的操作系统,虚拟机⼀旦被开启,预分配给他的资源将全部被占用。,每⼀个虚拟机包括应用,必要的二进制和库,以及⼀个完整的用户操作系统。
(2)Docker:容器技术是和我们的宿主机共享硬件资源及操作系统可以实现资源的动态分配。 容器包含应用和其所有的依赖包,但是与其他容器共享内核。容器在宿主机操作系统中,在用户空间以分离的进程运行。
(3)对比:

  1. docker启动快速属于秒级别。虚拟机通常需要⼏分钟去启动。
  2. docker需要的资源更少, docker在操作系统级别进行虚拟化, docker容器和内核交互,几乎没有性能损耗,性能优于通过 Hypervisor层与内核层的虚拟化。;
  3. docker更轻量, docker的架构可以共公用⼀个内核与共享应用程序库,所占内存极小。同样的硬件环境, Docker运⾏的镜像数 远多于虚拟机数量。对系统的利用率非常高
  4. 与虚拟机相比, docker隔离性更弱, docker属于进程之间的隔离,虚拟机可实现系统级别隔离;
  5. 安全性: docker的安全性也更弱。 Docker的租户 root和宿主机 root等同,
    ⼀旦容器内的用户从普通用户权限提升为 root权限,它就直接具备了宿主机的root权限,进而可进进无限制的操作。虚拟机租户 root权限和宿主机的 root虚拟机权限是分离的,并且虚拟机 利⽤如 Intel的 VT-d和 VT-x的 ring-1硬件隔离技术,这种
    隔离技术可以防⽌虚拟机突破和彼此交互,⽽容器⾄今还没有任何形式的硬件隔离,这使得容器容易受到攻击。
  6. 可管理性: docker的集中化管理工具还不算成熟。各种虚拟化技术都有成熟的管理工具,例如 VMware vCenter提供完备的虚 拟机管理能⼒。
  7. 高可用和可恢复性: docker对业务的⾼可⽤⽀持是通过快速重新部署实现的。虚拟化具备负载均衡,高可用,容错,迁移和数 据保护等经过⽣产实践检验的成熟保障机制,VMware可承诺虚拟机 99.999%高可用,保证业务连续性。
  8. 快速创建、删除:虚拟化创建是分钟级别的, Docker容器创建是秒级别的,Docker的快速迭代性,决定了无论是开发、测试、 部署都可以节约⼤量时间。
  9. 交付、部署:虚拟机可以通过镜像实现环境交付的⼀致性,但镜像分发无法体系化;Docker在 Dockerfile中记录了容器构建过 程,可在集群中实现快速分发和快速部署;

同⼀个宿主机中多个 Docker 容器之间如何通信?多个宿主机中Docker 容器之间如何通信?

(1)这⾥同主机不同容器之间通信主要使⽤Docker 桥接(Bridge)模式。
(2)不同主机的容器之间的通信可以借助于 pipework 这个⼯具。


参考文章:https://blog.csdn.net/qq_48403611/article/details/126132188

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时候联系我们修改或删除,在此表示感谢。

特别提醒:

1、请用户自行保存原始数据,为确保安全网站使用完即被永久销毁,如何人将无法再次获取。

2、如果上次文件较大或者涉及到复杂运算的数据,可能需要一定的时间,请耐心等待一会。

3、请按照用户协议文明上网,如果发现用户存在恶意行为,包括但不限于发布不合适言论妄图

     获取用户隐私信息等行为,网站将根据掌握的情况对用户进行限制部分行为、永久封号等处罚。

4、如果文件下载失败可能是弹出窗口被浏览器拦截,点击允许弹出即可,一般在网址栏位置设置

5、欢迎将网站推荐给其他人,网站持续更新更多功能敬请期待,收藏网站高效办公不迷路。

      



登录后回复

共有0条评论