review



basic

面向对象和面向过程的区别

面向过程是具体化的,流程化的,解决一个问题,你需要一步一步的分析,一步一步的实现。

面向对象是模型化的,你只需抽象出一个类,这是一个封闭的盒子,在这里你拥有数据也拥有解决问题的方法。需要什么功能直接使用就可以了,不必去一步一步的实现,至于这个功能是如何实现的,管我们什么事?我们会用就可以了。

面向对象的底层其实还是面向过程,把面向过程抽象成类,然后封装,方便我们使用的就是面向对象了。

基本数据类型

在这里插入图片描述

String相关

String 底层就是一个 char 类型的数组,比如“你好” 就是长度为2的数组 char[] chars = {'你','好'};

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

形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符

什么是字符串常量池?

字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。

String有哪些特性

  • 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。
  • 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用。
  • final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性。

String真的是不可变的吗?

  • 我觉得如果别人问这个问题的话,回答不可变就可以了。 下面只是给大家看两个有代表性的例子:

1 String不可变但不代表引用不可以变

1
2
3
String str = "Hello";
str = str + " World";
System.out.println("str=" + str);
  • 结果:

    str=Hello World

  • 解析:

  • 实际上,原来String的内容是不变的,只是str由原来指向”Hello”的内存地址转为指向”Hello World”的内存地址而已,也就是说多开辟了一块内存区域给”Hello World”字符串。(实际上都是创建一个StringBuilder对象,并调用append()方法,最后调用toString()创建新String对象,以包含修改后的字符串内容。

2.通过反射是可以修改所谓的“不可变”对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建字符串"Hello World", 并赋给引用s
String s = "Hello World";

System.out.println("s = " + s); // Hello World

// 获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");

// 改变value属性的访问权限
valueFieldOfString.setAccessible(true);

// 获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);

// 改变value所引用的数组中的第5个字符
value[5] = '_';

System.out.println("s = " + s); // Hello_World

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

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

String str=new String(“i”) 两个对象,一个是静态区的”i”,一个是用new创建在堆上的对象。

在使用 HashMap 的时候,用 String 做 key 有什么好处?

HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。(String对象内部已经重写了equals和hashcode,避免了不必要的hash碰撞

String,StringBuffer, StringBuilder 的区别是什么?String为什么是不可变的?

  • String是字符串常量,创建的字符内容是不可改变。不可变是因为String类中定义的char数组是final
  • StringBuffer和StringBuilder是字符串变量,创建的字符内容是可以改变的。
  • StringBuffer是线程安全的,而StringBuilder是非线程安全的。

String常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
length() 字符串的长度
charAt() 截取一个字符
toCharArray() String转换成char数组
equals()和equalsIgnoreCase() 比较两个字符串
startsWith()和endsWith() startsWith()方法决定是否以特定字符串开始,endWith()方法决定是否以特定字符串结束
equals()和 ==: equals()方法比较字符串对象中的字符,==运算符比较两个对象是否引用同一实例。
substring()
concat() 连接两个字符串
replace() 替换
trim() 去掉起始和结尾的空格
valueOf() 转换为字符串
toLowerCase() 转换为小写
toUpperCase() 转换为大写

自动装箱与拆箱

Integer a= 127 与 Integer b = 127相等吗

  • 对于对象引用类型:==比较的是对象的内存地址。
  • 对于基本数据类型:==比较的是值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
如果整型字面量的值在-128127之间,那么自动装箱时不会new新的Integer对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false
public static void main(String[] args) {
Integer a = new Integer(3); // 这个是创建堆对象
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
System.out.println(b == c); // true

Integer a1 = 128; // 相当于 new Integer()
Integer b1 = 128;
System.out.println(a1 == b1); // false

Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true
}

变量

静态变量和实例变量区别

  • 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。
  • 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。

静态变量与普通变量区别

  • static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
  • 还有一点就是static成员变量的初始化顺序按照定义的顺序进行初始化。

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

静态方法和实例方法的区别主要体现在两个方面:

  • 在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
  • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制

值传递

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?

  • 值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的(可以改变传过来的对象的属性值,但是更改引用是不会影响调用者)

为什么 Java 中只有值传递

  • 首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。 它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。
  • Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;

swap(num1, num2);

System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}

public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;

System.out.println("a = " + a);
System.out.println("b = " + b);
}
  • 结果:

    a = 20 b = 10 num1 = 10 num2 = 20

  • 解析:

在这里插入图片描述

  • 在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身

举例2:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}

public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
  • 结果:

    1 0

  • 解析:

在这里插入图片描述

  • array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。

举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {

public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}

public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
}
  • 结果:依旧没有影响调用者的对象

    x:小李 y:小张 s1:小张 s2:小李

  • 解析:

  • 交换之前:

在这里插入图片描述

  • 交换之后:

在这里插入图片描述

  • 通过上面两张图可以很清晰的看出:方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝

值传递和引用传递有什么区别

  • 值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
  • 引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。

重写与重载

构造器(constructor)是否可被重写(override)

  • 构造器不能被继承,因此不能被重写,但可以被重载。

重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?

  • 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
  • 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
  • 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。

多态的实现

Java实现多态有三个必要条件:继承、重写、向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类。
  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
People p=new Stuent();

class People{
public void eat(){
System.out.println("吃饭");
}
}
class Stu extends People{
@Override
public void eat(){
System.out.println("吃水煮肉片");
}
}

构造方法

在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

  • 帮助子类做初始化工作。

一个类的构造方法的作用是什么?若一个类没有声明构造方法,改程序能正确执行吗?为什么?

  • 主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。

构造方法有哪些特性?

  • 名字与类名相同;
  • 没有返回值,但不能用void声明构造函数;
  • 生成类的对象时自动执行,无需调用。

反射

获取反射机制的三种方式:

1
2
3
4
5
6
7
8
9
10
11
12
// 方式一、new对象:getClass方法来获取
Student student = new Student();
Class classObj1 = student.getClass();
System.out.println(classObj1.getName());

// 方式二、Class.forName方法(路径-相对路径)
Class classObj2 = Class.forName("com.aop8.reflect.Student");
System.out.println(classObj2.getName());

// 方式三、类名.class
Class classObj3 = Student.class;
System.out.println(classObj3.getName());

Collection

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

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

1
2
3
4
5
List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());

多线程场景下如何使用 ArrayList?

ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

1
2
3
4
5
6
7
List<String> synchronizedList = Collections.synchronizedList(list);
synchronizedList.add("aaa");
synchronizedList.add("bbb");

for (int i = 0; i < synchronizedList.size(); i++) {
System.out.println(synchronizedList.get(i));
}

Java标准库自带的java.util包提供了集合类:Collection,它是除Map外所有其他集合类的根接口。Java的java.util包主要提供了以下三种类型的集合:

  • List:一种有序列表的集合,例如,按索引排列的StudentList
  • Set:一种保证没有重复元素的集合,例如,所有无重复名称的StudentSet
  • Map:一种通过键值(key-value)查找的映射表集合,例如,根据Studentname查找对应StudentMap

img

List 接口

List:有序的,可重复的

ArrayList

底层是数组实现的,数组没变,初始容量为10,(空间不够)创建新数组,复制原来的数组的值 ,增加新的值进去,变得是数组引用指向(指向了新的数组)

  • 特性:
    • 空间连续
    • 有序的
    • 数据可重复(只能存 引用型)
    • 对中间节点,查询快,但是做增删操作,效率低下。
  • 优点:单链表,查询效率快
    img
LinkedList

底层是双向链表
适合存储大量数据,

  • 优点:查询慢,但中间节点增删快,
Vector

类似于ArrayList ,但较之于它,Vector是线程安全的

Set 接口

img

HashSet

底层是Hash表,且其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75。
无序集合、不能重复
相比set接口,HashSet 多了一个clone()方法。

特点:

  • 不能保证元素的排列顺序,顺序有可能发生变化
  • 不是同步的
  • 集合元素可以是 null,但只能放入一个 null

一般操作 HashSet 还是调用 Collection 的 add / remove 等方法进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HashSetTest {

public static void main(String[] args) {
//增加
Set<String> hashSet = new HashSet<String>();
hashSet.add("1");
hashSet.add("2");
hashSet.add("3");
hashSet.add("4");
hashSet.add("5");

//删除
hashSet.remove("1");

//查询 无法获取某个元素
System.out.println("是否包含1元素:" + hashSet.contains("2"));

//迭代
Iterator<String> it = hashSet.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
}
}

HashSet 的实现原理?

当向 HashSet 结合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在 HashSet 中存储位置。根据这种方式可以看出,HashSet 的数据存取其实是通过哈希算法实现的,因为通过哈希算法可以极大的提高数据的读取速度。通过阅读 JDK 源码,我们知道 HashSet 是通过 HashMap 实现的,只不过是HashSet 的 value 上的值都是 null 而已

简单的说,HashSet 集合判断两个元素相等的标准是两个对象通过 equals() 方法比较相等,并且两个对象的hashCode() 方法返回值相等

注意,如果要把一个对象放入 HashSet 中,重写该对象对应类的 equals() 方法,也应该重写其 hashCode() 方法。其规则是如果两个对 象通过equals方法比较返回true时,其hashCode也应该相同。另外,对象中用作equals比较标准的属性,都应该用来计算hashCode的值。

HashSet如何检查重复?HashSet是如何保证数据不可重复的?

HashSet 中的add ()方法会使用HashMap 的put()方法

HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap 比较key是否相等是先比较hashcode 再比较equals )。

源码:

1
2
3
4
5
6
7
8
9
10
// value值统一默认
private static final Object PRESENT = new Object();

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
LinkedHashSet

LinkedHashSet 在迭代访问 Set 中的全部元素时,性能比 HashSet 好,但是插入时性能稍微逊色于HashSet(因为 HashSet 直接采用哈希算法,而 LinkedHashSet 还需要维护链表结构)。

TreeSet

SortedSet 接口的唯一实现类,TreeSet 可以确保集合元素处于排序状态,这也是 TreeSet最大的特征之一。

底层是最优二叉树
img

Map集合

  • Map中的元素是两个对象,一个对象作为键,一个对象作为值。键不可以重复,但是值可以重复。
  • Map存储元素使用put方法,Collection使用add方法
类型 说明
Map(顶层接口) 将键映射到值的对象。一个映射不能包含重复的键;每个键最多只能映射到一个值。
HashMap 采用哈希表实现,所以无序。底层是哈希表数据结构,线程是不同步的,可以存入null键,null值。
要保证键的唯一性,需要覆盖hashCode方法,和equals方法。
Hashtable 底层是哈希表数据结构,线程是同步的,不可以存入null键,null值。
效率较低,被HashMap 替代。
LinkedHashMap 该子类基于哈希表又融入了链表。可以Map集合进行增删提高效率。
TreeMap 底层是二叉树数据结构。可以对map集合中的键进行排序。需要使用Comparable或者Comparator 进行比较排序。return 0,来判断键的唯一性。
ConcurrentHashMap 线程安全。,在JDK8以前,ConcurrentHashMap都是基于Segment分段锁来实现的,在JDK8以后,就换成synchronized和CAS这套实现机制了。

因为在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。

我们再思考一下HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对。通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。

因此,正确使用Map必须保证:作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true;作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范:如果两个对象相等,则两个对象的hashCode()必须相等;如果两个对象不相等,则两个对象的hashCode()尽量不要相等。即对应两个实例a和b:如果a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode();如果a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。

HashMap

我们现在用的都是 JDK 1.8,底层是由“数组+链表+红黑树”组成,而在 JDK 1.8 之前是由“数组+链表”组成。改成“数组+链表+红黑树”,主要是为了提升在 hash 冲突严重时(链表过长)的查找性能,使用链表的查找性能是 O(n),而使用红黑树是 O(logn)。对于插入,默认情况下是使用链表节点。当同一个索引位置的节点在新增后达到9个(阈值8):如果此时数组长度大于等于 64,则会触发链表节点转红黑树节点(treeifyBin);而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容,因为此时的数据量还比较小。转回成链表节点是用的6而不是复用8,因为我们设置节点多于8个转红黑树,少于8个就马上转链表,当节点个数在8徘徊时,就会频繁进行红黑树和链表的转换,造成性能的损耗。

注意:红黑树节点大小约为链表节点的2倍,在节点太少时,红黑树的查找性能优势并不明显,付出2倍空间的代价作者觉得不值得。

img

总结:

  1. 转红黑树:节点>阈值8,且数组长度 >= 64
  2. JDK1.8后底层是由“数组+链表+红黑树”,之前是数组+链表
扩容机制

使用一个容量更大的数组来代替已有的容量小的数组,transfer()方法将原有Entry数组的元素拷贝到新的Entry数组里。

HashMap的扩容机制—resize()

HashMap学习

ConcurrentHashMap

JDK1.7 和 JDK1.8 对 ConcurrentHashMap 的实现有很大的不同!

ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment

1
2
3
4
5
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}

JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于8时,链表结构转为红黑二叉树)结构。

ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!

1
2
3
4
是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在
ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的
结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个
Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)

Java8中ConcurrentHashMap是如何保证线程安全的

面试必问之 ConcurrentHashMap 线程安全的具体实现方式

网络编程

网络模型

这个模型从上到下依次是:

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

常用协议

TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTP、SMTP等。

UDP协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为UDP协议在通信前不需要建立连接,因此它的传输效率比TCP高,而且UDP协议比TCP协议要简单得多。

选择UDP协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择UDP协议。

WebSocket 和HTTP的区别及原理

http协议是用在应用层的协议,他是基于tcp协议的,http协议建立链接也必须要有三次握手才能发送信息

http链接分为短链接,长链接,短链接是每次请求都要三次握手才能发送自己的信息。即每一个request对应一个response,在一个TCP连接上可以传输多个Request/Response消息对。长链接是在一定的期限内保持链接。保持TCP连接不断开。客户端与服务器通信,必须要有客户端发起然后服务器返回结果。客户端是主动的,服务器是被动的

WebSocket他是为了解决客户端发起多个http请求到服务器资源浏览器必须要经过长时间的轮询问题而生的,他实现了多路复用,他是全双工通信。在webSocket协议下客服端和浏览器可以同时发送信息

websocket的持久连接只需建立一次Request/Response消息对,之后都是TCP连接,避免了需要多次建立Request/Response消息对而产生的冗余头部信息。

WebSocket 和HTTP的区别及原理

三次握手过程理解

简单

详细

img

第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。

第二次握手:服务器收到syn包,必须确认客户端的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

TCP的三次握手与四次挥手理解及面试题

四次挥手 - 待补充

从浏览器发出请求到响应都经历了什么

多线程

死锁

如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:

死锁检测

1、Jstack命令:java虚拟机自带的一种堆栈跟踪工具。

2、JConsole工具:Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。

什么是自旋锁?

自旋锁是SMP架构中的一种low-level的同步机制

当线程A想要获取一把自选锁而该锁又被其它线程锁持有时,线程A会在一个循环中自选以检测锁是不是已经可用了。

目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyWaitNotify3{

MonitorObject myMonitorObject = new MonitorObject();
boolean wasSignalled = false;

public void doWait(){
synchronized(myMonitorObject){
while(!wasSignalled){
try{
myMonitorObject.wait();
} catch(InterruptedException e){...}
}
//clear signal and continue running.
wasSignalled = false;
}
}

public void doNotify(){
synchronized(myMonitorObject){
wasSignalled = true;
myMonitorObject.notify();
}
}
}

线程生命周期

新建状态(New):当线程对象创建后,即进入新建状态,如:Thread t = new MyThread();

就绪状态(Runnable):当调用线程对象的start()方法时,线程即进入就绪状态。处于就绪状态的线程只是说明此线程已经做好准备,随时等待CPU调度执行,并不是说执行了start()方法就立即执行。

运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。

阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。

死亡状态(dead):线程执行完毕或者是异常退出,该线程结束生命周期。

阻塞状态分类

  1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  2. 同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程占用),它会进入到同步阻塞状态;
  3. 其他阻塞:通过调用线程的sleep()或join()或发出I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

线程池

Java面试经典题:线程池专题

线程池

常见知识

switch

switch是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上

在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从Java 7开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

float f=3.4;是否正确

  • 不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成 float f =3.4F;。

short运算类型转换

short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗

  • 对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。
  • 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换

总结:运算结果是 int型,所以第一个需要通过s1 = (short) (s1 + 1)强转成short类型

final finally finalize区别

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

抽象类和接口

抽象类只能被继承,一个抽象方法可以被子类们继承不断增强,但是实现接口只能一次性完成功能

抽象类

1
2
3
4
5
6
7
8
9
10
11
a、抽象类不能通过构造方法被实例化只能被继承,一个抽象方法可以被子类们继承不断增强

b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;

c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public

d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;

e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。

f、非抽象方法需要方法体,抽象方法(abstract)没有方法体

接口

1
2
3
4
5
6
7
a、接口可以包含变量、方法;变量被隐士指定为public static final,方法被隐士指定为public abstract(JDK1.8之前);

b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;

c、一个类可以实现多个接口;

d、JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰

== 和 equals 的区别是什么

== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  • 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  • 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

为什么要有 hashCode

1
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
  • 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

BIO,NIO,AIO 有什么区别?

  • 简答
    • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
    • NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
    • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非阻塞 IO ,异步 IO 的操作基于事件和回调机制。

字符串常量Java内部加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Main {
public static void main(String[] args) {
/*
(1).str1
str1 会有4个对象
一个StringBuilder、
一个coder ldc、
一个blue ldc、
String
这个时候常量池中没有coderblue这个ldc在
str1.intern():在jdk7后,会看常量池中是否存在,如果没有,它不会创建一个对象,
如果堆中已经这个字符串,那么会将堆中的引用地址赋给它
所以这个时候str1.intern()是获取的堆中的
* */
String str1 = new StringBuilder("coder").append("blue").toString();
System.out.println(str1);
System.out.println(str1.intern());
System.out.println(str1 == str1.intern());//true
System.out.println();

/*
sum.misc.Version类会在JDK类库的初始化中被加载并初始化,而在初始化时它需要对静态常量字
段根据指定的常量值(ConstantValue)做默认初始化,此时sum.misc.Version.launcher静态常
量字段所引用的"java"字符串字面量就被intern到HotSpot VM的字符串常量池 - StringTable
里了
str2对象是堆中的
str.intern()是返回的是JDK出娘胎自带的,在加载sum.misc.version这个类的时候进入常量池
*/
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2);
System.out.println(str2.intern());
// false
System.out.println(str2 == str2.intern());//false
}
}

根据以上代码判断输出结果。

1
2
3
4
5
6
7
coderblue
coderblue
true

java
java
false

原因分析

intern()作用说明: intern() 是一个本地方法。如果字符串常量池中已经包含了一个等于此String对象的字符串,则返回代表池中这个字符串的String对象的引用;否则,会将此String对象包含的字符串添加到常量池中,然后返回此String对象的引用。
简单来说就是:如果常量池中已经存在了这个字符串,则直接返回池中的引用,如果常量池中没有这个字符串,则新建然后返回池中的引用。

那么问题来了,为什么同样的操作,字符串coderblue返回的结果是true,而java返回的记过的false呢?

原因: 这是因为在JDK初始化的时候,有一个初始化的”java“字符串(JDK出娘胎自带的),在加载sun.musc.Version这个类的时候进入了字符串常量池。

源码 sun.misc.Version:

1
2
3
4
5
6
7
8
9
10
package sun.misc;

import java.io.PrintStream;

public class Version {
private static final String launcher_name = "java";
private static final String java_version = "1.8.0_201";

...
}

可重入锁的代码验证

1、ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @author Lauy
* @date 2021/1/29
*/
public class ReentrantLockMain {
public static void main(String[] args) {
new Thread(new SynchronizedTest2()).start();
}
}

class SynchronizedTest2 implements Runnable {

private final Lock lock = new ReentrantLock();

public void helloA() { //方法1,调用方法2
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " helloA()");
helloB();
} finally {
lock.unlock();
}
}

public void helloB() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " helloB()");
} finally {
lock.unlock();
}
}

@Override
public void run() {
helloA();
}
}

2、synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SynchronizedMain {
public static void main(String[] args) {
new Thread(new SynchronizedTest()).start();
}
}

class SynchronizedTest implements Runnable {
private final Object obj = new Object();

public void helloA() { //方法1,调用方法2
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + " helloA()");
helloB();
}
}

public void helloB() {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + " helloB()");
}
}

@Override
public void run() {
helloA(); //调用helloA方法
}
}

均输出:

Thread-0 helloA()
Thread-0 helloB()

==&equals&final

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
String s1 = new String("blue");
String s2 = new String("blue");
System.out.println(s1 == s2); // false
System.out.println(Objects.equals(s1, s2)); // true
String s3 = "blue";
String s4 = "blue";
System.out.println(s3 == s4); // true
System.out.println(s3 == s1); // false
String s5 = "blueblue";
String s11 = "blue" + "blue";
String s6 = s3 + s4;
System.out.println(s5 == s6); // false
final String s7 = "blue";
final String s8 = "blue";
String s9 = s7 + s8;
System.out.println(s5 == s9); // final修饰,true
final String s10 = s3 + s4;
System.out.println(s5 == s10); // false
}
}
    1. new (“blue”)不相等,会各自在堆空间创建
    1. 常量比较相等
    1. 常量 + 常量还是在常量池
    1. 对象 + 对象 != 对象,但是重写equals值相等 或者 用final修饰

ThreadLocal

ThreadLocal提供 getset等接口或方法,这些方法为每一个使用这个变量的线程都存有一份独立的副本,因此 get总是返回由当前线程在调用 set时设置的最新值。

为什么wait必须写在同步(synchroized)代码块中

![https://img-blog.csdnimg.cn/20210226003548328.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQzNDc2NDY1,size_16,color_FFFFFF,t_70)

创建线程的方式

  • 继承Thread
  • 实现Runable接口
  • 实现Callable接口(可以获取线程执行之后的返回值)

https://zhuanlan.zhihu.com/p/58821827

JUC

JVM由哪些部分组成

在这里插入图片描述

  • JVM包含两个子系统和两个组件: 两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
    • Class loader(类装载):根据给定的全限定名类名(如:java.lang.Object)来装载class文件到Runtime data area中的method area。
    • Execution engine(执行引擎):执行classes中的指令。
    • Native Interface(本地接口):与native libraries交互,是其它编程语言交互的接口。
    • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。

Java 8从永久代到元空间

方法区是JVM 的规范,永久代(PermGen space)是HotSpot对这种规范的实现。

在 JDK 1.8 中, HotSpot 已经没有 “PermGen space”这个区间了,取而代之的是 Metaspace(元空间)。

并发和并行

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

运行流程是什么

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

说一下 JVM 运行时数据区

Java 虚拟机在执行 Java 程序的过程中会把它所管理的内存区域划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有些区域随着虚拟机进程的启动而存在,有些区域则是依赖线程的启动和结束而建立和销毁。Java 虚拟机所管理的内存被划分为如下几个区域:

简单的说就是我们java运行时的东西是放在那里的

在这里插入图片描述

程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器,字节码解析器的工作是通过改变这个计数器的值,来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能,都需要依赖这个计数器来完成;

为什么要线程计数器?因为线程是不具备记忆功能

Java 虚拟机栈(Java Virtual Machine Stacks):每个方法在执行的同时都会在Java 虚拟机栈中创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;

栈帧就是Java虚拟机栈中的下一个单位

本地方法栈(Native Method Stack):与虚拟机栈的作用是一样的,只不过虚拟机栈是服务 Java 方法的,而本地方法栈是为虚拟机调用 Native 方法服务的;

Native 关键字修饰的方法是看不到的,Native 方法的源码大部分都是 C和C++ 的代码

Java 堆(Java Heap):Java 虚拟机中内存最大的一块,是被所有线程共享的,几乎所有的对象实例都在这里分配内存;

方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

栈先进后出,堆先进先出

详细的介绍下程序计数器?(重点理解)

  1. 程序计数器是一块较小的内存空间,它可以看作是:保存当前线程所正在执行的字节码指令的地址(行号)
  2. 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

详细介绍下Java虚拟机栈?(重点理解)

  1. Java虚拟机是线程私有的,它的生命周期和线程相同。
  2. 虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
  • 解释:虚拟机栈中是有单位的,单位就是栈帧,一个方法一个栈帧。一个栈帧中他又要存储,局部变量,操作数栈,动态链接,出口等。

在这里插入图片描述

解析栈帧:

  1. 局部变量表:是用来存储我们临时8个基本数据类型、对象引用地址、returnAddress类型。(returnAddress中保存的是return后要执行的字节码的指令地址。)
  2. 操作数栈:操作数栈就是用来操作的,例如代码中有个 i = 6*6,他在一开始的时候就会进行操作,读取我们的代码,进行计算后再放入局部变量表中去
  3. 动态链接:假如我方法中,有个 service.add()方法,要链接到别的方法中去,这就是动态链接,存储链接的地方。
  4. 方法出口:方法出口是什么呢,方法出口正常的话就是return 不正常的话就是抛出异常咯

一个方法调用另一个方法,会创建很多栈帧吗?

  • 答:会创建。如果一个栈中有动态链接调用别的方法,就会去创建新的栈帧,栈中是由顺序的,一个栈帧调用另一个栈帧,另一个栈帧就会排在调用者下面(说明被调用者先进去)

栈指向堆是什么意思?

  • 栈指向堆是什么意思,就是栈中要使用成员变量怎么办,栈中不会存储成员变量,只会存储一个应用地址

递归的调用自己会创建很多栈帧吗?

  • 答:递归的话也会创建多个栈帧,就是在栈中一直从上往下排下去

详细的介绍Java堆吗?(重点理解)

  • java堆(Java Heap)是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例。
  • 在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
  • java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”。
  • 从内存回收角度来看java堆可分为:新生代和老年代
  • 从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
  • 无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。
  • 根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存可以完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
  • 堆的大小可通过参数–Xms(堆的初始容量)、-Xmx(堆的最大容量) 来指定

深拷贝和浅拷贝

  • 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址,
  • 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存,
  • 浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
  • 深复制:在计算机中开辟一块新的内存地址用于存放复制的对象。

JVM如何判断一个对象是否存活

判断方式

  • 垃圾收集器对 Java堆里的对象 是否进行回收的判断准则:Java对象是存活 or 死亡

判断对象为死亡才会进行回收

  • Java虚拟机中,判断对象是否存活有2种方法:
    1. 引用计数法
    2. 引用链法(可达性分析法)

引用计数法

方式描述

  • Java 对象添加一个引用计数器
  • 每当有一个地方引用它时,计数器 +1;引用失效则 -1;

判断对象存活准则

当计数器不为 0 时,判断该对象存活;否则判断为死亡(计数器 = 0)。

优点

  • 实现简单
  • 判断高效

缺点

  • 无法解决 对象间相互循环引用 的问题

即该算法存在判断逻辑的漏洞

  • 具体描述
1
2
3
4
5
6
7
8
9
<-- 背景 -->
// 对象objA 和 objB 都有字段 name
// 两个对象相互进行引用,除此之外这两个人对象没有任何引用
objA.name = objB;
objB.name = objA;

<-- 问题 -->
// 实际上这两个对象已经不可能再被访问,应该要被垃圾收集器进行回收
// 但因为他们相互引用,所以导致计数器不为0,这导致引用计数算法无法通知垃圾收集器回收该两个对象

正由于该算法存在判断逻辑漏洞,所以 Java虚拟机没有采用该算法判断Java是否存活。

引用链法(可达性分析法)

img

垃圾收集器

天上飞的理念,要有落地的实现(垃圾收集器就是GC垃圾回收算法的实现)

GC算法是内存回收的方法论,垃圾收集器就是算法的落地实现

GC算法主要有以下几种

  • 引用计数(几乎不用,无法解决循环引用的问题)
  • 复制拷贝(用于新生代)
  • 标记清除(用于老年代)
  • 标记整理(用于老年代)

简述Java垃圾回收机制

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

垃圾回收器的原理是什么?有什么办法手动进行垃圾回收?

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

虚拟机类加载机制

JAVA类加载器包括几种?

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

简述java类加载机制?

  • 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的java类型。

类加载的机制及过程

  • 程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这个3个步骤统称为类加载或类初始化。

在这里插入图片描述

1、加载
  • 加载指的是将类的class文件读入到内存,并将这些静态数据转换成方法区中的运行时数据结构,并在堆中生成一个代表这个类的java.lang.Class对象,作为方法区类数据的访问入口,这个过程需要类加载器参与。
  • Java类加载器由JVM提供,是所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
  • 类加载器,可以从不同来源加载类的二进制数据,比如:本地Class文件、Jar包Class文件、网络Class文件等等等。
  • 类加载的最终产物就是位于堆中的Class对象(注意不是目标类对象),该对象封装了类在方法区中的数据结构,并且向用户提供了访问方法区数据结构的接口,即Java反射的接口
2、连接过程
  • 当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中(意思就是将java类的二进制代码合并到JVM的运行状态之中)。类连接又可分为如下3个阶段。
  1. 验证:确保加载的类信息符合JVM规范,没有安全方面的问题。主要验证是否符合Class文件格式规范,并且是否能被当前的虚拟机加载处理。
  2. 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配
  3. 解析:虚拟机常量池的符号引用替换为字节引用过程
3、初始化
  • 初始化阶段是执行类构造器<clinit>() 方法的过程。类构造器<clinit>()方法是由编译器自动收藏类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生,代码从上往下执行。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  • 虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确加锁和同步

初始化的总结就是:初始化是为类的静态变量赋予正确的初始值

双亲委托机制

(1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

(2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

(3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

jvm类加载器,类加载机制详解,看这一篇就够了

https://juejin.cn/post/6844904041131016200

https://zhuanlan.zhihu.com/p/142141937

Volatile

Volatile是Java虚拟机提供的轻量级的同步机制(三大特性)

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

Spring

Spring是一个轻量级的IoC和AOP容器框架,是为Java应用程序提供基础性服务的一套框架。

优点

(1)spring属于低侵入式设计,代码的污染极低;

(2)spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;

(3)Spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。

(4)spring对于主流的应用框架提供了集成支持。

常用注解


Spring

@component :标准一个普通的spring Bean类。可以代替@Repository、@Service、@Controller,因为这三个注解是被@Component标注的。

@Repository:标注一个DAO组件类。

@Service、@Controller实质上属于同一类注解,用法相同,功能相同,区别在于标识组件的类型。

@Autowired是Spring的注解,默认按类型装配,默认情况下必须要求依赖对象存在,如果要允许null值,可以设置它的required属性为false。如果想使用名称装配可以结合@Qualifier注解进行使用。

@Resource默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行名称查找。

@Configuration 声明当前类为配置类,相当于xml形式的Spring配置(类上)

@Bean声明当前方法的返回值为一个bean,替代xml中的方式(方法上)


SpringMVC

@RequestMapping:请求路径映射,

@PathVariable:用于将请求URL中的模板变量映射到功能处理方法的参数上,即取出uri模板中的变量作为参数。

@RequestParam:主要用于在SpringMVC后台控制层获取参数。

@ResponseBody:该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。

@RequestBody:该注解用于读取Request请求的body部分数据,使用系统默认配置的HttpMessageConverter进行解析,然后把相应的数据绑定到要返回的对象上,再把HttpMessageConverter返回的对象数据绑定到controller中方法的参数上。


SpringBoot

@RestController :@ResponseBody和@Controller的合集。

@EnableAutoConfiguration:开启自动配置

@ComponentScan:表示将该类自动发现(扫描)并注册为Bean,可以自动收集所有的Spring组件,包括@Configuration类。

@SpringBootApplication:相当于@EnableAutoConfiguration、@ComponentScan和@Configuration的合集。

@EnableScheduling 在配置类上使用,开启计划任务的支持(类上)

Spring的IoC理解

(1)IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系、对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。

(2)最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。

(3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入

依赖注入DI和控制反转IOC

https://www.jianshu.com/p/07af9dbbbc4b 控制反转(IoC)与依赖注入(DI)
https://zhuanlan.zhihu.com/p/67032669 理解依赖注入(DI – Dependency Injection)

依赖注入是种设计思想

Spring的AOP理解

OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。

AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。

AOP实现的关键在于 代理模式,AOP代理主要分为静态代理动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP和cglib为代表。

(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)硬编码到Java字节码中,运行的时候就是增强之后的AOP对象

(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理

  • JDK动态代理只提供接口的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
  • 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

对比:

静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

img

Spirng4和Spring5下的区别

Spring5 2.3.3.后

正常

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:5.2.8.RELEASE / d07228cc-7d9d-41a7-b345-97208a848829
@After在切点方法后,return前执行
我是环绕通知后
我进入了业务层的queryUser方法

异常

方法前

1
2
3
4
5
6
7
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterThrowing方法执行
@After在切点方法后,return前执行
2021-02-02 22:54:24.878 ERROR 66232 --- [nio-8089-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

方法后

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.3.3.RELEASE / SpringVersion5.2.8.RELEASE
进入Controller类的queryUser方法
@AfterThrowing方法执行
@After在切点方法后,return前执行
我进入了业务层的queryUser方法
2021-02-02 22:53:26.961 ERROR 66232 --- [nio-8089-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
Spring5

正常

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
我是环绕通知后
@After在切点方法后,return前执行
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:5.1.5.RELEASE / e973d3ff-8764-46e3-9f1a-d9862fb750b5
我进入了业务层的queryUser方法

异常

调用业务层方法queryUser前:对比可见没有执行service的queryUser方法

1
2
3
4
5
6
7
8
9
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:17:13.164 ERROR 31916 --- [nio-8089-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

调用业务层方法queryUser后:

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
我是SpringBootVersion:2.2.5.RELEASE / SpringVersion5.2.4.RELEASE
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
我进入了业务层的queryUser方法
2021-02-02 14:16:30.185 ERROR 31916 --- [nio-8089-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero
Spring4

正常

1
2
3
4
5
6
7
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
我是环绕通知后
@After在切点方法后,return前执行
@AfterReturning方法执行 / 访问的类方法:execution(String cn.lauy.controller.UserController.queryUser(String)) / 调用业务层方法的返回:我是SpringBootVersion:4.3.6.RELEASE / fabcfd3c-7a9a-4290-a7a9-190f60bbf1ca
我进入了业务层的queryUser方法

异常

调用业务层方法queryUser后:对比可见没有执行service的queryUser方法

1
2
3
4
5
6
7
8
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:31:56.791 ERROR 59308 --- [nio-8089-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

调用业务层方法queryUser后:

1
2
3
4
5
6
7
8
9
10
我是环绕通知前
@Before切点方法执行之前,输出日志
进入Controller类的queryUser方法
@After在切点方法后,return前执行
@AfterThrowing方法执行
2021-02-02 14:35:40.903 ERROR 34152 --- [nio-8089-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

java.lang.ArithmeticException: / by zero

我进入了业务层的queryUser方法

Spring容器的启动流程

(1)初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中:

  • 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象

(2)将配置类的BeanDefinition注册到容器中:

(3)调用refresh()方法刷新容器:

BeanFactory和ApplicationContext有什么区别?

BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。

(1)BeanFactory是Spring里面最底层的接口,是IOC的核心,定义了IOC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。

  • BeanFactroy采用的是延迟加载形式来注入Bean时(调用getBean()),才对该Bean进行加载实例化。
  • BeanFactory通常以编程的方式被创建

(2)ApplicationContext接口作为BeanFactory的子类

  • 它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入

  • 相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。

  • 通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

  • 框架功能:

    • 继承MessageSource,因此支持国际化。
  • 资源文件访问,如URL和文件(ResourceLoader)。

    • 载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
  • 提供在监听器中注册bean的事件。

AbstractApplicationcontext

AbstractApplicationContextApplicationContext 的抽象实现类,该抽象类实现应用上下文的一些具体操作,也是我们加载xml配置文件的入口类

AbstractApplicationContext是Spring应用上下文中最重要的一个类,这个抽象类中提供了几乎ApplicationContext的所有操作。主要有容器工厂的处理,事件的发送广播,监听器添加,容器初始化操作refresh方法,然后就是bean的生成获取方法接口等。主要还是提供了一些方法,复杂的操作也是没有太多。

Spring Bean的生命周期

简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation –> 属性赋值 Populate –> 初始化 Initialization –> 销毁 Destruction

img

(1)实例化Bean:

对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。

对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

(2)设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。

(3)处理Aware接口:Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:

  • ①如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字;
  • ②如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • ②如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
  • ③如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;

(4)BeanPostProcessor前置处理:如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean:如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。

(6)init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。

(7)BeanPostProcessor后置处理:如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

(8)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

(9)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

Spring中bean的作用域:

(1)singleton:默认作用域,单例bean,每个容器中只有一个bean的实例。

(2)prototype:为每一个bean请求创建一个实例。

(3)request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。

(4)session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。

(5)global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。

Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?

(1)对于prototype作用域的Bean,每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。

(2)对于singleton作用域的Bean,所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的(对于有状态的Bean)。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。

1
2
3
有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。

无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。

对于有状态的bean(比如Model和View),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。

也可以采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。

Spring如何解决循环依赖问题

循环依赖问题在Spring中主要有三种情况:

  • (1)通过构造方法进行依赖注入时产生的循环依赖问题。

    在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。

  • (2)通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。

    每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。

  • (3)通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

Spring在单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过二级缓存和三级缓存来解决的,其中三级缓存是主要功臣。解决的核心原理就是:在对象实例化之后,依赖注入之前,Spring提前暴露的Bean实例的引用在第三级缓存中进行存储。

Spring事务的实现方式和实现原理

spring支持编程式事务管理和声明式事务管理两种方式:

①编程式事务管理使用TransactionTemplate。

②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

启动类开启@EnableTransactionManager

(2)spring的事务传播机制:

spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。

① PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。

② PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。

③ PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘

④ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

⑤ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。

⑥ PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。

⑦ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

Spring Boot

什么是 CSRF 攻击?

CSRF 代表跨站请求伪造。这是一种攻击,迫使最终用户在当前通过身份验证的Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。

SpringBoot与SpringCloud 区别

SpringBoot是快速开发的Spring框架,SpringCloud是完整的微服务框架,SpringCloud依赖于SpringBoot。

Spring Boot 有哪些优点?

Spring Boot 主要有如下优点:

  1. 容易上手,提升开发效率,为 Spring 开发提供一个更快、更简单的开发框架。
  2. 开箱即用,远离繁琐的配置。
  3. 提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。
  4. SpringBoot总结就是使编码变简单、配置变简单、部署变简单、监控变简单等等

Spring Boot 的核心注解是哪个?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项, 例如:java 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
  • @ComponentScan:Spring组件扫描。

SpringBoot Starter的工作原理

我个人理解SpringBoot就是由各种Starter组合起来的,我们自己也可以开发Starter

在sprinBoot启动时由@SpringBootApplication注解会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean,并且进行自动配置把bean注入SpringContext中 //(SpringContext是Spring的配置文件)

SpringBoot的自动配置原理是什么

主要是Spring Boot的启动类上的核心注解SpringBootApplication注解主配置类,有了这个主配置类启动时就会为SpringBoot开启一个@EnableAutoConfiguration注解自动配置功能。

有了这个EnableAutoConfiguration的话就会:

  1. 从配置文件META_INF/Spring.factories加载可能用到的自动配置类
  2. 去重,并将exclude和excludeName属性携带的类排除
  3. 过滤,将满足条件(@Conditional)的自动配置类返回

SpringBoot事物的使用

SpringBoot的事物很简单,首先使用注解@EnableTransactionManagement开启事物之后,然后在Service方法上添加注解@Transactional便可。

Async异步调用方法

在SpringBoot中使用异步调用是很简单的,只需要在方法上使用@Async注解即可实现方法的异步调用。 注意:需要在启动类加入@EnableAsync使异步调用@Async注解生效。

定时任务的使用

在SpringBoot中使用定时任务是很简单的,只需要在方法上使用@EnableScheduling注解即可实现。@Scheduled配合cron表达式使用

Spring Boot 有哪几种读取配置的方式?

Spring Boot 可以通过 @PropertySource, @Value, @Environment, @ConfigurationPropertie注解来绑定变量

Spring Boot读取配置文件的几种方式

Spring Boot 是否可以使用 XML 配置 ?

Spring Boot 推荐使用 Java 配置而非 XML 配置,但是 Spring Boot 中也可以使用 XML 配置,通过 @ImportResource 注解可以引入一个 XML 配置。

SpringBoot多数据源拆分的思路

先在properties配置文件中配置两个数据源,创建分包mapper,使用@ConfigurationProperties读取properties中的配置,使用@MapperScan注册到对应的mapper包中

在一个项目中多数据源如何划分:1.分包名(业务) 2.注解方式
cn.li.test01—datasource1
cn.li.test02—datasource2

SpringBoot多数据源事务如何管理

  • 第一种方式是在service层的@TransactionManager中使用transactionManager指定DataSourceConfig中配置的事务
  • 第二种是使用jta-atomikos实现分布式事务管理

如何实现 Spring Boot 应用程序的安全性?

为了实现 Spring Boot 的安全性,我们使用 spring-boot-starter-security 依赖项,并且必须添加安全配置。它只需要很少的代码。配置类将必须扩展WebSecurityConfigurerAdapter 并覆盖其方法。

Spring Boot 中如何解决跨域问题 ?

  1. 因此我们推荐在后端通过 (CORS,Cross-origin resource sharing) 来解决跨域问题。
  2. 通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}

}

SpringBoot性能如何优化

  • 如果项目比较大,类比较多,不使用@SpringBootApplication,采用@Compoment指定扫包范围

  • 在项目启动时设置JVM初始内存和最大内存相同

    vm option中设置

  • 将springboot内置服务器由tomcat设置为undertow

SpringBoot微服务中如何实现 session 共享 ?

常见的方案就是 Spring Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对 session 有相关的读写操作时,都去操作 Redis 上的 session 。

spring-boot-starter-parent 有什么用 ?

我们都知道,新创建一个 Spring Boot 项目,默认都是有 parent 的,这个 parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用:

  1. 定义了 Java 编译版本为 1.8 。
  2. 使用 UTF-8 格式编码。
  3. 继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号。
  4. 执行打包操作的配置。
  5. 自动化的资源过滤。
  6. 自动化的插件配置。
  7. 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的配置文件,例如 application-dev.properties 和 application-dev.yml。

总结就是打包用的

SpringBoot如何实现打包

  • 进入项目目录在控制台输入mvn clean package,clean是清空已存在的项目包,package进行打包
  • 或者点击左边选项栏中的Maven,先点击clean在点击package

SpringCloud

SpringCloud学习

SpringCloud技术栈使用

CAP原则

QPS、TPS

Redis

redis入门到进阶

什么是Redis?

Redis 是一个使用 C 语言写成的,开源的高性能key-value非关系缓存数据库。它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合)和hash(哈希类型)。Redis的数据都基于缓存的,所以很快,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。Redis也可以实现数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。

Redis有哪些优缺点?

优点

  • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有

持久化

什么是Redis持久化? 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 的持久化机制是什么?各自的优缺点?

Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

RDB:是Redis DataBase缩写快照

RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

在这里插入图片描述

优点:

1、只有一个文件 dump.rdb,方便持久化。

2、容灾性好,一个文件可以保存到安全的磁盘。

3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能

4.相对于数据集大时,比 AOF 的启动效率更高。

缺点:数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)

AOF:持久化

AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复

在这里插入图片描述

优点:

1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。

2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。

3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点:

1、AOF 文件比 RDB 文件大,且恢复速度慢。

2、数据集大的时候,比 rdb 启动效率低。

Redis的过期键的删除策略

我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

  • 过期策略通常有以下三种:
  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。 (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了惰性过期和定期过期两种过期策略。

Redis的内存

Redis的内存淘汰策略有哪些

MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

全局的键空间选择性移除

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。

设置过期时间的键空间选择性移除

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

Redis主要消耗什么物理资源?

  • 内存。

Redis的内存用完了会发生什么?

  • 如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

  • 可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hash),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面

Redis事务

Redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令

Redis 的事务是总是带有隔离性的。

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务的三个阶段

  1. 事务开始 MULTI
  2. 命令入队
  3. 事务执行 EXEC

事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队

Redis会将一个事务中的所有命令序列化,然后按顺序执行。

  1. redis 不支持回滚,“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
  2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行
  3. 如果在一个事务中出现运行错误,那么正确的命令会被执行
  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。

  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。

  • UNWATCH命令可以取消watch对所有key的监控。

如何解决 Redis 的并发竞争 Key 问题

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

redis缓存穿透,缓存击穿,缓存雪崩

缓存穿透

缓存和数据库中都没有的数据,可用户还是源源不断的发起请求,导致每次请求都会到数据库,从而压垮数据库。

key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。

解决办法

  ①、业务层校验

  用户发过来的请求,根据请求参数进行校验,对于明显错误的参数,直接拦截返回。

  比如,请求参数为主键自增id,那么对于请求小于0的id参数,明显不符合,可以直接返回错误请求。

  ②、不存在数据设置短过期时间

  对于某个查询为空的数据,可以将这个空结果进行Redis缓存,但是设置很短的过期时间,比如30s,可以根据实际业务设定。注意一定不要影响正常业务。

  ③、布隆过滤器**

  采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

  对于缓存穿透,我们可以将查询的数据条件都哈希到一个足够大的布隆过滤器中,用户发送的请求会先被布隆过滤器拦截,一定不存在的数据就直接拦截返回了,从而避免下一步对数据库的压力。

缓存击穿

Redis中一个热点key在失效的同时,大量的请求过来,从而会全部到达数据库,压垮数据库。

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

​ ①、设置热点数据永不过期

  对于某个需要频繁获取的信息,缓存在Redis中,并设置其永不过期。当然这种方式比较粗暴,对于某些业务场景是不适合的。

  ②、定时更新

  比如这个热点数据的过期时间是1h,那么每到59minutes时,通过定时任务去更新这个热点key,并重新设置其过期时间。

  ③、互斥锁

  这是解决缓存击穿比较常用的方法。

  互斥锁简单来说就是在Redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则睡眠一段时间(比如100ms)后重试。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

​ ④分级缓存(缓存两份数据,第二份数据生存时间长一点作为备份,第一份数据用于被请求命中,如果第二份数据被命中说明第一份数据已经过期,要去mysql请求数据重新缓存两份数据)

缓存雪崩

Redis中缓存的数据大面积同时失效,或者Redis宕机,从而会导致大量请求直接到数据库,压垮数据库。

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

解决方法:

  1. 保证Redis服务高可用:redis集群,将原来一个人干的工作,分发给多个人干
  2. 缓存预热:对于即将来临的大量请求,我们可以提前走一遍系统,将数据提前缓存在Redis中,并设置不同的过期时间。
  3. 避免缓存设置相近的有效期,我们可以在设置有效期时增加随机值,或者统一规划有效期,使得过期时间均匀分布。

缓存预热

  • 缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
  • 解决方案
    1. 直接写个缓存刷新页面,上线时手工操作一下;
    2. 数据量不大,可以在项目启动的时候自动进行加载;
    3. 定时刷新缓存;

缓存降级

  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
  • 缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
  • 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

  • 热点数据,缓存才有价值
  • 对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存
  • 对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
  • 数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
  • 那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

缓存热点key

  • 缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

  • 对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

如何保证缓存与数据库双写时的数据一致性?

问题场景 描述 解决
先写缓存,再写数据库,缓存写成功,数据库写失败 缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读 这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;读取数据的时候,如果缓存不存在,则读取数据库再写缓存
先写数据库,再写缓存,数据库写成功,缓存写失败 写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据 缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现
需要缓存异步刷新 指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候 确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔

一个字符串类型的值能存储最大容量是多少?

  • 512M

哨兵模式

RabbitMQ

MQ就是消息队列。是软件和软件进行通信的中间件产品

优点有以下几个:

异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。

应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。

流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。

日志处理 - 解决大量日志传输。

消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。

缺点有以下几个:

  1. 系统可用性降低

    本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低;

  2. 系统复杂度提高

    加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。

  3. 一致性问题

    A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。

基本概念

Broker: 简单来说就是消息队列服务器实体

Exchange: 消息交换机,它指定消息按什么规则,路由到哪个队列

Queue: 消息队列载体,每个消息都会被投入到一个或多个队列

Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来

Routing Key: 路由关键字,exchange根据这个关键字进行消息投递

VHost: vhost 可以理解为虚拟 broker ,即 mini-RabbitMQ server。其内部均含有独立的 queue、exchange 和 binding 等,但最最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段(一个典型的例子就是不同的应用可以跑在不同的 vhost 中)。

Producer: 消息生产者,就是投递消息的程序

Consumer: 消息消费者,就是接受消息的程序

Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务

由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。

Work Queues (工作队列): - 补充

https://www.coderblue.cn/3491549389/#常用工作模式

也叫公平队列,能者多劳的消息队列模型。队列必须接收到来自消费者的手动ack 才可以继续往消费者发送消息。默认自动应答。

ACK机制

为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。

如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。

1
2
// 返回确认状态:第二个参数值为false代表关闭RabbitMQ的自动应答机制,改为手动应答。
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

但是这样还是不够的,如果rabbitMQ-Server突然挂掉了,那么还没有被读取的消息还是会丢失 ,所以我们可以让消息持久化。 只需要在定义Queue时,设置持久化消息就可以了,方法如下:

1
2
boolean durable = true;
channel.queueDeclare(channelName, durable, false, false, null);

这样设置之后,服务器收到消息后就会立刻将消息写入到硬盘,就可以防止突然服务器挂掉,而引起的数据丢失了。 但是服务器如果刚收到消息,还没来得及写入到硬盘,就挂掉了,这样还是无法避免消息的丢失。

消息的重复问题

消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。

如何保证RabbitMQ消息的顺序性?

拆分多个 queue(消息队列),每个 queue(消息队列) 一个 consumer(消费者),就是多一些 queue (消息队列)而已,确实是麻烦点;

或者就一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

消息如何分发?

若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。通过路由可实现多消费的功能

消息基于什么传输?

由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。

如何保证消息不被重复消费?或者说,如何保证消息消费时的幂等性?

针对以上问题,一个解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;

在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;

如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?

发送方确认模式

  • 将信道设置成confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID
  • 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。
  • 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。
  • 发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。

接收方确认机制

  • 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
  • 这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性;

下面罗列几种特殊情况

  • 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)
  • 如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。

如何保证队列的消息不被重复消费?

这个需要灵活作答,考察的是思考力,因为消费的场景有很多,有数据库、有缓存、有第三方接口

  • 1.比如针对数据库,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键(或者UUID),那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
  • 2.再比如redis缓存,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
  • 3.再比如第三方接口,需要确定两点,第三方接口程序是有去重能力的,那么脏一点直接丢数据过去,如果没有去重能力,还是需要我们来写程序去重,就是第2点的办法。

如何解决消息队列的延时以及过期失效问题?

其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢,造成消息堆积了,MQ存储快要爆了,甚至开始过期失效删除数据了。

针对这个问题可以有事前、事中、事后三种处理

  • 事前:开发预警程序,监控最大的可堆积消息数,超过就发预警消息(比如短信),不要等出生产事故了再处理。
  • 事中:看看消费端是不是故障停止了,紧急重启。
  • 事后:中华石杉老师就是说的这一种(https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/mq-time-delay-and-expired-failure.md),需要对消费端紧急扩容 ,增加处理消费者进程,如扩充10倍处理,但其实这也有个问题,即数据库的吞吐是有限制的,如果是消费到数据库也是没办法巨量扩容的,所以还是要在吞吐能力支持下老老实实的泄洪消 费。所以事前预防还是最重要的。否则出发删除过期数据,那就需要再重写生产消息的程序,重新产生消息。

RabbitMQ 的高可用性如何保证?

RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式

队列

死信队列?

DLX,全称为 Dead-Letter-Exchange,死信交换器,死信邮箱。当消息在一个队列中变成死信 (dead message) 之后,它能被重新被发送到另一个交换器中,这个交换器就是 DLX,绑定 DLX 的队列就称之为死信队列。

导致的死信的几种原因?

  • 消息被拒(Basic.Reject /Basic.Nack) 且 requeue = false。
  • 消息TTL过期。
  • 队列满了,无法再添加。

优先级队列?

优先级高的队列会先被消费。

可以通过x-max-priority参数来实现。

当消费速度大于生产速度且Broker没有堆积的情况下,优先级显得没有意义。

延迟队列?

存储对应的延迟消息,指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。

RabbitMQ面试题必知必会29道(附答案)

Springboot整合RabbitMQ

MySQL

MyISAM索引与InnoDB索引的区别?

  • InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
  • InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
  • MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
  • InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。

存储引擎选择

  • 如果没有特别的需求,使用默认的Innodb即可。
  • MyISAM:以读写插入为主的应用程序,比如博客系统、新闻门户网站。
  • Innodb:更新(删除)操作频率也高,或者要保证数据的完整性;并发量高,支持事务和外键。比如OA自动化办公系统。

主键索引与唯一索引的区别

  1. 主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。
  2. 主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。
  3. 唯一性索引列允许空值,而主键列不允许为空值。
  4. 主键列在创建时,已经默认为空值 ++ 唯一索引了。
  5. 一个表最多只能创建一个主键,但可以创建多个唯一索引。
  6. 主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。
  7. 主键可以被其他表引用为外键,而唯一索引不能。

索引有哪几种类型?

主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。

唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。

  • 可以通过 ALTER TABLE table_name ADD UNIQUE (column); 创建唯一索引
  • 可以通过 ALTER TABLE table_name ADD UNIQUE (column1,column2); 创建唯一组合索引

普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。

  • 可以通过ALTER TABLE table_name ADD INDEX index_name (column);创建普通索引
  • 可以通过ALTER TABLE table_name ADD INDEX index_name(column1, column2, column3);创建组合索引

全文索引: 是目前搜索引擎使用的一种关键技术。

索引的数据结构(b树,hash)

  • 索引的数据结构和具体存储引擎的实现有关,在MySQL中使用较多的索引有Hash索引B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。

1、B树索引

  • mysql通过存储引擎取数据,基本上90%的人用的就是InnoDB了,按照实现方式分,InnoDB的索引类型目前只有两种:BTREE(B树)索引和HASH索引。B树索引是Mysql数据库中使用最频繁的索引类型,基本所有存储引擎都支持BTree索引。通常我们说的索引不出意外指的就是(B树)索引(实际是用B+树实现的,因为在查看表索引时,mysql一律打印BTREE,所以简称为B树索引)

在这里插入图片描述

  • 查询方式:
    • 主键索引区:PI(关联保存的时数据的地址)按主键查询,
    • 普通索引区:si(关联的id的地址,然后再到达上面的地址)。所以按主键查询,速度最快
  • B+tree性质:
    1. n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。
    2. 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
    3. 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
    4. B+ 树中,数据对象的插入和删除仅在叶节点上进行。
    5. B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。

2、哈希索引

  • 简要说下,类似于数据结构中简单实现的HASH表(散列表)一样,当我们在mysql中用哈希索引时,主要就是通过Hash算法(常见的Hash算法有直接定址法、平方取中法、折叠法、除数取余法、随机数法),将数据库字段数据转换成定长的Hash值,与这条数据的行指针一并存入Hash表的对应位置;如果发生Hash碰撞(两个不同关键字的Hash值相同),则在对应Hash键下以链表形式存储。当然这只是简略模拟图。

在这里插入图片描述

创建索引的三种方式

第一种方式:在执行CREATE TABLE时创建索引

1
CREATE TABLE user_index2 ( id INT auto_increment PRIMARY KEY, first_name VARCHAR (16), last_name VARCHAR (16), id_card VARCHAR (18), information text, KEY name (first_name, last_name), FULLTEXT KEY (information), UNIQUE KEY (id_card) );

第二种方式:使用ALTER TABLE命令去增加索引

ALTER TABLE table_name ADD INDEX index_name (column_list);

  • ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。
  • 其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。
  • 索引名index_name可自己命名,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。

第三种方式:使用CREATE INDEX命令创建

CREATE INDEX index_name ON table_name (column_list);

  • CREATE INDEX可对表增加普通索引或UNIQUE索引。(但是,不能创建PRIMARY KEY索引)

B树和B+树的区别

  • 在B树中,你可以将键和值存放在内部节点和叶子节点;但在B+树中,内部节点都是键,没有值,叶子节点同时存放键和值。
  • B+树的叶子节点有一条链相连,而B树的叶子节点各自独立。

在这里插入图片描述

使用B树的好处

  • B树可以在内部节点同时存储键和值,因此,把频繁访问的数据放在靠近根节点的地方将会大大提高热点数据的查询效率。这种特性使得B树在特定数据重复多次查询的场景中更加高效。

使用B+树的好处

  • 由于B+树的内部节点只存放键,不存放值,因此,一次读取,可以在内存页中获取更多的键,有利于更快地缩小查找范围。 B+树的叶节点由一条链相连,因此,当需要进行一次全数据遍历的时候,B+树只需要使用O(logN)时间复杂度找到最小的一个节点,然后通过链进行O(N)的顺序遍历即可。而B树则需要对树的每一层进行遍历,这会需要更多的内存置换次数,因此也就需要花费更多的时间

事物的四大特性(ACID)介绍一下?

  • 关系性数据库需要遵循ACID规则,具体内容如下:
  1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

什么是脏读?幻读?不可重复读?

  • 脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
  • 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
  • 幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

从锁的类别上分MySQL都有哪些锁呢?

从锁的类别上来讲,有共享锁和排他锁。

  • 共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁就是让多个线程同时获取一个锁。
  • 排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排它锁也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥,其它事务可以读取,但不能进行更新和插入操作

数据库的乐观锁和悲观锁是什么?怎么实现的?

  • 数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

    1
    2
    // 核心SQL,主要靠for update
    select status from t_goods where id=1 for update;
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:一般会使用版本号机制或CAS算法实现。

    1
    2
    //核心SQL
    update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

悲观锁、乐观锁的区别及使用场景

定义:

悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。

乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。

适用场景:

悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。

像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适,之所以用悲观锁就是因为两个用户更新同一条数据的概率高,也就是冲突比较严重的情况下,所以才用悲观锁.

悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。

两种锁的使用场景

  • 从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
  • 但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

行锁和表锁的含义及区别

记录完整的死锁日志show engine innodb status 时,显示的信息不全。

行级锁(引擎INNODB):开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

表级锁(引擎MyISAM):开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

显式加锁:

上共享锁(读锁)的写法:lock in share mode,例如:

1
select math from zje where math>60 lock in share mode

上排它锁(写锁)的写法:for update,例如:

1
select math from zje where math >60 for update

表锁

不会出现死锁,发生锁冲突几率高,并发低。

MyISAM在执行查询语句(select)前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。

有两种模式:

  • 表共享读锁
  • 表独占写锁

行锁

会出现死锁,发生锁冲突几率低,并发高。

在MySQL的InnoDB引擎支持行锁,与Oracle不同,MySQL的行锁是通过索引加载的,也就是说,行锁是加在索引响应的行上的,要是对应的SQL语句没有走索引,则会全表扫描,行锁则无法实现,取而代之的是表锁,此时其它事务无法对当前表进行更新或插入操作。

事务的隔离级别

SQL执行顺序

1
2
3
4
5
6
7
8
9
SELECT DISTINCT <select_list>
FROM <left_table>
<join_type> JOIN <right_table>
ON <join_condition>
WHERE <where_condition>
GROUP BY <group_by_list>
HAVING <having_condition>
ORDER BY <order_by_condition>
LIMIT <limit_number>

sql优化

MySQL面试题(总结最全面的面试题)

MySQL进阶学习

Mybatis

MyBatis是什么?

Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。

MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

Mybatis的一级、二级缓存

一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置<cache/>

对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。

MyBatis编程步骤是什么样的?

  • 1、 创建SqlSessionFactory
  • 2、 通过SqlSessionFactory创建SqlSession
  • 3、 通过sqlsession执行数据库操作
  • 4、 调用session.commit()提交事务
  • 5、 调用session.close()关闭会话

请说说MyBatis的工作原理

img

浅谈 MyBatis 三级缓存

会先查找二级缓存,未命中然后查询一级缓存。清空策略看配置,commit时会清空一级缓存,更新二级缓存。

Linux

常用命令

1
2
3
4
5
6
mkdir 用于新建一个新目录
rm— Remove 会删除给定的文件
cp— Copy 命令对文件进行复制
tail 默认在标准输出上显示给定文件的最后10行内容,可以使用tail -n N 指定在标准输出上显示文件的最后N行内容。
grep 在给定的文件中搜寻指定的字符串。
su 用于切换不同的用户。

linux的chmod赋权限命令相关

1
2
3
4
5
6
7
8
chmod 777 file
-rw------- (600) -- 只有属主有读写权限。
-rw-r--r-- (644) -- 只有属主有读写权限;而属组用户和其他用户只有读权限。
-rwx------ (700) -- 只有属主有读、写、执行权限。
-rwxr-xr-x (755) -- 属主有读、写、执行权限;而属组用户和其他用户只有读、执行权限。
-rwx--x--x (711) -- 属主有读、写、执行权限;而属组用户和其他用户只有执行权限。
-rw-rw-rw- (666) -- 所有用户都有文件读、写权限。这种做法不可取。
-rwxrwxrwx (777) -- 所有用户都有读、写、执行权限。更不可取的做法。

nginx操作命令

1、重启

1
2
service nginx restart
systemctl restart nginx

2、刷新配置文件

1
nginx -s reload

3、上传文件大小限制

1
2
3
4
5
6
7
8
9
10
可以选择在http{ }中设置:client_max_body_size   1m; //默认值为1m,推荐值500M

也可以选择在server{ }中设置:client_max_body_size 20m;

还可以选择在location{ }中设置:client_max_body_size 20m;

三者到区别是:
http{} 中控制着所有nginx收到的请求。
报文大小限制设置在server{}中,则控制该server收到的请求报文大小。
配置在location中,则报文大小限制,只对匹配了location 路由规则的请求生效。

docker常用操作命令

1、使用命令进入对应容器的目录

1
docker exec -it a5f02a3e6dde(启动的tomcat容器的容器id) /bin/bash

2、查看正在/所有容器

1
2
docker ps //查看正在运行的容器
docker ps -a //查看所有的容器(包括run、stop、exited状态的)

docker入门

Java8新特性

去重

1、集合对象:根据字段去重

1
2
3
4
5
6
7
8
9
// 根据 projectCode去重 , collectingAndThen:Collectors操作后附加的整理步骤
List<ProjectStageDto> dataList = projectStageDtoList.stream().collect(
Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(o -> o.getProjectcode()))), ArrayList::new));

// 不能是Map<String, Object>
List<Map<String, String>> filterList = quickWayList.stream().collect(
Collectors.collectingAndThen(Collectors.toCollection(
() -> new TreeSet<>(Comparator.comparing(item -> item.get("id")))), ArrayList::new));

2、集合元素,也可针对String类型的去重

1
2
3
4
5
6
7
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 5, 5, 5, 6, 7);

List<Integer> distinctNumbers =
numbers.stream().distinct()
.collect(Collectors.toList());

System.out.println(distinctNumbers);//1, 2, 3, 4, 5, 6, 7

Java8Stream流Api学习

ElasticSearch

https://zhuanlan.zhihu.com/p/102500311

https://zhuanlan.zhihu.com/p/139762008

Dubbo

算法

https://github.com/CoderBleu/leetcode/tree/master/Java

设计模式

设计模式深入学习

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  1. © 2020-2021 Lauy    湘ICP备20003709号

请我喝杯咖啡吧~

支付宝
微信