后端学习文档

Java基础

程序本质:代码是如何被执行的?CPU、操作系统、虚拟机各司何职?

基础语法:从CPU角度看变量、数组、类型、运算跳转、函数的那个语法

引用类型:同样都是存储地址,为何Java引用比C/C++指针更安全?

基本类型:既然Java一切皆对象,那为何又要保留int等基本类型?

位运算:>>>和>>有何区别?(源码/反码/补码、算数位移/逻辑位移)

浮点数:计算机如何用二进制表示浮点数?为何0.1+0.1不等于0.2?

接口和抽象类的区别

  1. 抽象类允许包含某些方法的实现,而接口是不允许的;从设计级别看,为了实现由抽象类定义的类型,一个类必须定义为一个抽象类的子类,这限制了它在类图中的层次,但是接口没有这个层次的限制。

  2. 在抽象类中提供成员方法的实现细节,该方法只能在接口中为public abstract修饰,也就是抽象方法。

  3. 抽象类中的成员变量可以实现多个权限 public private protected final等,接口中只能用 public static final修饰。

String字符串提取第N个值

  1. charAt()方法

    1
    2
    String str = "Hello World!";
    System.out.printLn(str.charAt(0)) // H // 为了得到第n个字符的字符串,只需调用charAt(n)上String,在那里n是你想检索字符的索引
  2. subString 截取字符串

    substring(0,2)这个只含开头不含结尾,因此截取是截取两个字符,从第一个到第二个字符,不包含第三个。

    substring(2)这个表示截掉前两个,得到后边的新字符串。

JDK、JRE、JVM之间的区别

  • JDK(Java SE Development Kit),Java标准开发包,它提供了编译运行Java程序所需的各种工具和资源,包括Java编译器Java运行时环境,以及常用的Java类库
  • JRE(Java Runtime Eviroment),Java运行环境,用于运行Java的字节码文件。JRE中包括了JVM以及JVM工作所需要的类库,普通用户而只需要安装JRE来运行JAVA程序,而程序开发者必须安装JDK来编译、调试程序。
  • JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心的部分,负责运行字节码文件。

我们写Java代码,用txt就可以写,但是写出来的Java代码,要想运行,需要先编译成字节码,那就需要编译器,而JDK中就包含了编译器javac,编译之后的字节码,想要运行,就需要一个可执行字节码的程序,这个程序就是JVM(Java虚拟机),专门用来执行Java字节码的。

如果我要开发Java程序,那就需要JDK,因为要编译Java源文件。

如果我们执行运行已经编译好的Java字节码文件,也就是*.class文件,那么就只需要JRE。

JDK中包含了JRE,JRE中包含了JVM。

另外,JVM在执行Java字节码时,需要把字节码解释为机器指令,而不同操作系统的机器指令是可能不一样的,所以就导致不同操作系统上的JVM是不一样的,所以我们在安装JDK时需要选择操作系统。

另外,JVM是用来执行Java字节码的,所以凡是某个代码编译之后是Java字节码,那就能在JVM上运行,比如Apache Groovy,Scala,Kotlin等等。

什么是字节码?采用字节码的好处是什么?

编译器(javac)将Java源文件(*.java)编译成为字节码文件(.class),可以做到一次编译到处运行,windows上编译好的class文件,可以直接在linux上运行,通过这种方式做到跨平台,不过Java的跨平台有一个前提条件,就是不同的操作系统上安装的JDK或JRE是不一样的,虽然字节码时通用的,但是需要把字节码解释成各个操作系统的机器码是需要不同的解释器的,所以针对各个操作系统需要有各自的JDK或JRE

采用字节码的好处,一方面实现了跨平台,另一方面也提高了代码执行的性能,编译器在编译源代码时可以做一些编译期的优化,比如锁消除、标量替换、方法内联等。

hashCode()与equals()之间的关系

在Java中,每个对象都可以调用自己的hashCode()方法得到自己的哈希值(hashCode),相当于对象的指纹信息,通常说世界上没有完全相同的两个指纹,但是在Java中做不到这么绝对,但是我们仍然可以利用hashCode来做一些提前的判断,比如:

* 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象
* 如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象。
* 如果两个对象相等,那么他们的hashCode就一定相同。

在Java的一些集合的实现中,在比较两个对象是否相等时,会根据上面的原则,会先调用对象的hashCode()方法得到hashCode进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同,如果hashCode相同,那么就会进一步调用equals()方法进行比较。而equals()方法,就是用来最终确定两个对象是不是相等的,通常equals()方法的实现会比较重,逻辑较多,而hashCode()主要就是得到一个哈希值,实际上就是一个数字,相对而言比较轻,所以在比较两个对象时,通常都会先根据hashCode相比较一下。

所以我们就要注意,如果我们重写了equals()方法,那么就要注意hashCode()方法。

重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同、方法返回值和访问修饰符可以不同,发生在编译时。

重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法范围修饰符为private则子类就不能重写该方法。

1
2
3
public int add(int a, String b)
public String add(int a, String b)
//编译报错

String、StringBuffer、StringBuilder的区别

  1. String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的。
  2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以单线程环境下StringBuilder效率会更高。

泛型中extends和super的区别

  1. <? extends T>表示包括T在内的任何T的子类
  2. <? super T>表示包括T在内的任何父类

==和equals方法的区别

  • ==:如果是基本数据类型,比较的是值,如果是引用类型,比较的是引用地址
  • equals:具体看各个类重写equals方法之后的比较逻辑,比如String类,虽然是引用类型,但是String类中重写了equals方法,方法内部比较的是字符串中的各个字符是否全部相等。

If和Switch的区别

现代 CPU 都支持分支预测 (branch prediction) 和指令流水线 (instruction pipeline),这两个结合可以极大提高 CPU 效率。对于像简单的 if 跳转,CPU 是可以比较好地做分支预测的。但是对于 switch 跳转,CPU 则没有太多的办法。 switch 本质上是根据索引,从地址数组里取地址再跳转。

List和Set的区别

  • List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素
  • Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,再逐一遍历各个元素

ArrayList和LinkedList区别

  1. 首先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList是基于链表实现的
  2. 由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同。
  3. 另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来适用。

ConcurrentHashMap的扩容机制

1.7版本

  1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
  2. 每个Segment相当一个小型的HashMap
  3. 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
  4. 先生成新的数组,然后转移元素到新数组中
  5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值。

1.8版本

  1. 1.8版本的ConcurrentHashMap不再基于Segment实现
  2. 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
  3. 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
  4. ConcurrentHashMap是支持多个线程同时扩容的
  5. 扩容之前也先生成一个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

HashMap在JDK1.7、1.8中发生了什么变化(底层)

  1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
  2. 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法
  3. 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源

HashMap的Put方法

HashMap的Put方法的流程:

  1. 根据Key通过哈希算法与运算得出数组下标

  2. 如果数组下标元素为空,则将key和value封装为Entry对象(1.7中是Entry对象,1.8中是Node对象)并放入该位置

  3. 如果数组下标位置元素不为空,则要分情况讨论

    a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中

    b. 如果是JDK1.8,则会先判断当前位置上的Node类型,看是红黑树Node还是链表Node

    ​ i . 如果是红黑树Node,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value

    ​ ii . 如果此配置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,那么则会将该链表转成红黑树

    ​ iii. 将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束PUT方法

深拷贝和浅拷贝

深拷贝和浅拷贝是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。

  1. 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
  2. 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象。

HashMap的扩容机制原理

1.7版本

  1. 先生成新数组
  2. 遍历老数组中的每个位置上的链表上的每个元素
  3. 取每个元素的Key,并基于新数组的长度,计算出每个元素在新数组中的下标
  4. 将元素添加到新数组中去
  5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性

1.8版本

  1. 先生成新数组

  2. 遍历老数组中的每个位置上的链表或红黑树

  3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去

  4. 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置

    ​ a. 统计每个下标位置的元素个数

    ​ b. 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置

    ​ c. 如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置

  5. 所有元素转移完成了之后,将新数组赋值给HashMap对象的table属性

CopyOnWriteArrayList的底层原理

  1. 首先CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
  2. 并且,写操作会加锁,防止出现并发写入丢失数据的问题
  3. 写操作结束之后会把原数组指向新数组
  4. CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的引用场景,但是CopyOnWriteArrayList会比较占用内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景

Java中的异常体系

  • Java中所有异常的顶级父类是Throwable
  • Throwable下有两个子类Exception、Error
  • Error表示非常严重的错误,比如Java.lang.StachOverFlowError,Java.lang.OutOfMemmoryError,通常这些错误出现时,仅仅想靠程序自己是解决不了的,可能是虚拟机、磁盘、操作系统层面出现了问题,所以通常也不建议在代码中去捕获这些Error,因为捕获的意义不大,因为程序可能已经根本运行不了了。
  • Exception表示异常,表示程序出现Exception时,是可以考程序自己来解决的,比如NullPointException、IllegalAccessException等,我们可以捕获这些异常来做特殊处理。
  • Exception的子类通常又可以分为RuntimeException和非RuntimeException两类
  • RuntimeException表示运行期异常,表示这个异常是在代码运行过程中抛出的,这些异常时非检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免出现这类异常,比如NullPointException、IndexOutOfBoundsException等。
  • 非RuntimeException表示运行期异常,也就是我们常说的检查异常,是必须进行处理的异常,如果不处理,程序就不能检查异常通过。如IOException、SQLException等以及用户自定义的Exception异常

在Java的异常处理机制中,什么时候应该抛出异常,什么时候捕获异常?

异常相当于一种提示,如果我们抛出异常,将相当于告诉上层方法,我抛了一个异常,交给你来处理,而对于上层方法来说,它也需要决定自己能不能处理这个异常,是否也需要交给它的上层。

所以我们在写方法时,我们需要考虑的就是,本方法能否合理的处理该异常,如果处理不了就继续向上抛出异常,包括本方法中在调用另外一个方法时,发现出现了异常,如果这个异常应该由自己来处理,那就捕获该异常并进行处理。

面向对象

封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项

内部细节对外部调用透明,外部调用无需修改或者关心内部实现

  1. javabean的属性私有,提供get set对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定。而不能由外部胡乱修改

    1
    2
    3
    4
    5
    private String name;
    public void setName(String name) {
    this.name = "tuling_"+name;
    }
    //该name由自己的命名规则,明显不能由外部直接赋值
  2. orm框架

    操作数据库,我们不需要关心链接是如何建立的、sql是如何执行的,只需要引入mybatis,调方法即可

继承:继承基类的方法,并作出自己的改变和/或扩展

子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需要扩展自己个性化的

多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同

继承,方法重写,父类引用指向子类对象

1
2
父类类型 变量名 = new 子类对象;
变量名.方法名();

无法调用子类特有的功能。

Java中有哪些类加载器

JDK自带的有三个类加载器:bootstrapClassLoader、ExtClassLoader、AppClassLoader

  • BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%/lib下的jar包和class文件
  • ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类
  • AppClassLoader是自定义加载器的父类,负责加载classpath下的类文件

说说类加载器双亲委派模型

JVM中存在三个默认的类加载器

  1. BootstrapClassLoader
  2. ExtClassLoader
  3. AppClassLoader

AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader

JVM在加载一个类时,会调用AppClassLoader的loadClass方法来加载这个类,不过在这个方法中,会先试用ExtClassLoader的loadClass方法来加载类,同样ExtClassLoader的loadClass方法中会先试用BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果BootstrapClassLoader没有加载到,那么ExtClassLoader就会自己尝试加载该类,如果没有加载到,那么则会有AppClassLoader来加载这个类。

所以双亲委派指得是,JVM在加载类时,会委派给Ext和BootStrap进行加载,如果没加载到才由自己进行加载。

JDK1.8 新特性

Lambda表达式

Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用 Lambda表达式可以使代码变的更加简洁紧凑。

方法引用

方法引用提供了非常有用的语法,可以直接引用已有Java类或对象(实例)的方法或构造器。与lambda联合使用,方法引用可以使语言的构造更紧凑简洁,减少冗余代码。

默认方法

默认方法就是一个在接口里面有了一个实现的方法。

新工具

新的编译工具,如:Nashorn引擎 jjs、 类依赖分析器jdeps。

StreamApi

新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。

DateTimeApi

加强对日期与时间的处理。

Optional类

Optional 类已经成为 Java 8 类库的一部分,用来解决空指针异常。

Nashorn,JavaScript引擎

JDK1.8提供了一个新的Nashorn javascript引擎,它允许我们在JVM上运行特定的javascript应用。

并发编程

Synchronized和Lock锁底层实现原理

  • Synchronized是语义级支持
    • JVM层面
    • 锁升级过程
  • LOCK锁是AQS
    • JDK层面
    • CAS(Compare and Swap - 比较并交换)
      • CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值V与预期原值A相匹配,那么处理器会自动将该位置值V更新为新值B,否则,处理器不做任何操作,整个操作保证了原子性,即在对比V==A后、设置V=B之前不会有其他线程修改V的值。

线程池

  • 工作流程
    • 线程先提交进来,进入核心池里面
    • 如果线程数大于coreSize,就会进入一个阻塞队列
    • 阻塞队列满了之后会新建一些线程,进入到最大的池
    • 当线程数maxSize超出了之后,会执行一个拒绝策略
      • JDK自带的拒绝策略有4种
        • 一种是直接丢弃
        • 一种是抛出异常
        • 一种是丢弃阻塞队列里面等待时间最长的一个线程
        • 一种是由调用者在调用者的线程里执行

实现多线程的两种方式

  • 继承Thread类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class MyThread extends Thread {
    String name;

    @Override
    public void run() {
    for (int i=1; i<10; i++) {
    System.out.println(name + 1);
    }
    }
    }

    public class Test{
    public static void main() {
    new MyThread("A").start();
    new MyThread("B").start();
    new MyThread("C").start();
    }
    }
  • 实现Runable接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class MyThread implements Runnable {
    @Override
    public void run() {
    ....
    }
    }

    public class Hello {
    public static void main() {
    Thread thread = new Thread(new MyThread);
    thread.start();
    }
    }
  • 通过Callable和Future创建线程

    (1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

    (2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

    (3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

    (4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    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
    public class CallableThreadTest implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
    int i =0;
    for(;i<100;i++) {
    System.out.println(Thread.currentThread().getName() + " " + i);
    }
    return i;
    }

    public static void main(String[] args) {
    CallableThreadTest ctt = new CallableThreadTest();
    FutureTask<Integer> ft = new FutureTask<>(ctt);
    for (int i = 0; i<100; i++) {
    System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
    if (i==20) {
    new Thread(ft, "有返回值的线程").start();
    }
    }
    try {
    System.out.println("子线程的返回值: " + ft.get());
    } catch (InterruptedException e) {
    e.printStackTrace();
    } catch (ExcutionException e) {
    e.printStackTrace();
    }
    }
    }

说说对线程安全的理解

线程安全指得是,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能够得到正常的结果,比如i++,i的初始化值为0,那么两个线程来同时执行这行代码,如果代码是线程安全的,那么最终的结果应该就是一个线程的结果为1,一个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码时线程不安全。

所以线程安全,主要指得是一段代码在多个线程同时执行的情况下,能否得到正确的结果。

对守护线程的理解

线程分为用户线程和守护线程,用户线程就是普通线程,守护线程就是JVM的后台线程,比如垃圾回收线程就是一个守护线程,守护线程会在其他普通线程都停止运行之后自动关闭。我们可以通过设置thread.setDaemon(true)来把一个线程设置为守护线程。

ThreadLocal的底层原理

  1. ThreadLocal是JAVA中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据

  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值

  3. 如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清楚Entry对象。

  4. ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接

并发、并行、串行之间的区别

  1. 串行:一个任务执行完,才能执行下一个任务
  2. 并行:两个任务同时执行
  3. 并发:两个任务整体看上去是同时执行,在底层,两个任务被拆分成了很多份,然后一个一个执行,站在更高的角度看来两个任务是同时执行的

Java死锁如何避免

造成死锁的几个原因:

  1. 一个资源每次只能被一个线程使用
  2. 一个线程在阻塞等待某个资源时,不释放已占有资源
  3. 一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
  4. 若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破谛哥条件,不出现循环等待锁的关系。

在开发过程中:

  1. 要注意加锁的顺序,保证每个线程按同样的顺讯进行加锁
  2. 要注意加锁的时限,可以针对所设置一个超时时间
  3. 要注意死锁检查,这是一种防御机制,确保在第一时间发现死锁并进行解决

乐观锁与悲观锁

乐观锁

CAS是乐观锁的一种实现方式,是一种轻量级锁,JUC中很多工具类的实现就是基于CAS的

CAS是如何实现线程安全的?

线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。

悲观锁

synchronized就是悲观锁的一种实现。

synchronized,代表这个方法加锁,相当于不管哪一个线程,运行到这个方法时,都要检查有没有其他线程正在用这个方法,有的话要等正在使用synchronized方法的线程运行完这个方法后再运行此线程,没有的话,锁定调用者,然后直接运行。

JAVA WEB

什么是Servlet

Servlet是Java Servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,具有独立于平台和协议的特性,是一个Java类。

JavaWeb中servlet主要功能是承载网络连接,业务逻辑处理,比如一些编码格式的转换、登录拦截等。

狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。Servlet运行于支持Java的应用服务器中。从原理上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

JSP与Sevlet的区别

Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容,Jsp中的Java脚本如度何镶嵌到一个类中,由Jsp容器完成。

1、jsp经编译后就变成了Servlet。

2、jsp更擅长表现于页面显示,servlet更擅长于逻辑控制。

3、Servlet中没有内置对象,Jsp中的内置对象都是必须通过HttpServletResponse对象以及HttpServlet对象得到。

4、而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应

5、Servlet的应用逻辑是在Java文件中,并且完全从表示层中的HTML里分离开来。

6、而JSP的情况是Java和HTML可以组合成一个扩展名为.jsp的文件。

7、JSP侧重于视图,Servlet主要用于控制逻辑

8、Servlet更多的是类似于一个Controller,用来做控制。

JVM

  1. jvm的位置
  2. JVM的体系结构
  3. 类加载器
  4. 双亲委派机制
  5. 沙箱安全机制
  6. Native
  7. PC寄存器
  8. 方法区
  9. 三种JVM
  10. 新生区、老年区
  11. 永久区
  12. 堆内存调优
  13. GC
    1. 常用算法
  14. JMM

JVM探究

  • 对JVM的理解?java8虚拟机和之前的变化、更新?
    • JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分,它是整个java实现跨平台的最核心的部分,负责运行字节码文件。
  • 什么是OOM?什么是栈溢出StackOverFlowError? 怎么分析
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取?怎么分析Dump文件?
  • 谈谈JVM中,类加载器你的认识?

JVM中哪些是线程共享区

堆区和方法区是所有线程共享的,栈、本地方法栈、程序计数器是每个线程独有的

![jvm运行时数据区.drawio (1)](C:\Users\oveng\Downloads\jvm运行时数据区.drawio (1).png)

如何排查JVM问题

对于还在正常运行的系统:

  1. 可以使用jmap来查看jvm中各个区域的使用情况
  2. 可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
  3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
  4. 通过各个命令的结果,或者jvisualvm等工具来进行分析
  5. 首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
  6. 同时,还可以找到占用cpu最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存

对于已经发生了OOM的系统:

  1. 一般生产系统中都会设置当前系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  2. 我们可以利用jsisualvm等工具来分析dump文件
  3. 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
  4. 然后在进行详细的分析和调试

总之,调优就不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题

一个对象从加载到JVM,再到被GC清除,都经历了什么过程?

  1. 首先把字节码文件内容加载到方法区
  2. 然后再根据类信息在堆区创建对象
  3. 对象首先会分配在堆区中年轻代的Eden区,经过一次Minor GC后,对象如果存活,就会进入Suvivor区。在后续的每次Minor GC中,如果对象一直存活,就会在Suvivor区来回拷贝,每移动一次,年龄加1
  4. 当年龄超过
  5. 如果经过FullGC,被标记为垃圾对象,那么就会被GC线程清理掉

怎么确定一个对象到底是不是垃圾?

  1. 引用计数算法:这种方式是给堆内存当中的每个对象记录一个引用个数,引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。
  2. 可达性算法:这种方式是在内存中,从根对象向下一只找引用,找到的对象就不是垃圾,没找到的对象就是垃圾。

JVM的垃圾回收算法

  1. 标记清除算法:

    a. 标记阶段:把垃圾内存标记出来

    b. 清除阶段:直接将垃圾内存回收

    c. 这种算法比较简单的,但是有一个很严重的问题,就是会产生大量的内存碎片

  2. 复制算法:为了解决标记清除算法的内存碎片问题,就产生了复制算法。复制算法将内存分为大小想等的两半,每次只使用其中的一般。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。这种算法没有内存碎片,但是他的问题就在于浪费空间。而且,他的效率跟存活对象的个数有关。

  3. 标记压缩算法:为了解决复制算法的缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的。但是在完成标记之后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将边界以外的所有内存直接清除。

什么是STW

STW: Stop-The-World,是在垃圾回收算法执行过程中,需要将JVM内存冻结的一种状态。在STW状态下,JAVA的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC公众算法优化的重点,就是减少STW,同时这也是JVM调优的重点。

常用的JVM参数

JVM参数大致可以分为三类:

  1. 标注指令: -开头,这些是所有的HotSpot都支持的参数。可以用java -help 打印出来
  2. 非标准指令: -X开头,这些指令通常试试跟特定的HotSpot版本对应的。可以用java -X打印出来。
  3. 不稳定参数: -XX开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。

JVM调优

JVM调优原则

JVM调优应该是调优的最后一步,应该在应用层、数据库层、框架层都进行调优后,系统还有性能问题时再进行JVM调优。

JVM调优的时机

  • Heap内存(老年代)持续上涨达到设置的最大内存值
  • Full GC次数频繁
  • GC停顿时间过长(超过1s)
  • 应该出现OutOfMemory等内存异常
  • 应用中有使用本地缓存且占用大量内存空间
  • 系统吞吐量与响应性能不高或下降

JVM调优的目标

吞吐量、延迟、内存占用三者类似CAP,构成了一个不可能三角,只能选择其中两个进行调优,不可三者兼得,

  • 延迟:GC低停顿和GC低频率
  • 低内存占用;
  • 高吞吐量

选择了其中两个,必然会以牺牲另一个为代价。

注意:不同应用的JVM调优量化目标是不一样的。

JVM调优的步骤

  • 分析系统运行情况:分析GC日志和dump文件,判断是否需要进行优化,确定瓶颈问题点;
  • 确定JVM调优量化目标
  • 确定JVM调优参数(根据历史JVM参数来调整)
  • 依次确定调优内存、延迟、吞吐量等指标
  • 对比观察调优前后的差异
  • 不断的分析和调整,知道找到合适的JVM参数配置;
  • 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪

一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求。

img

算法

LeetCode

链表

队列

平衡二叉树

B树

B+树

红黑树

Spring

什么是Spring框架

Spring是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。

Spring是很多模块的集合:核心容器、数据访问/集成、Web、AOP、工具、消息和测试模块

Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。

Spring的6个特征:

  • 核心技术: 依赖注入,AOP、事件(events)、资源、i18n、验证、数据绑定、类型转换、SpEL
  • 测试:模拟对象、TestContext框架、Spring MVC测试、WebTestClient
  • 数据访问:事务、DAO支持、JDBC、ORM、编组XML
  • Web支持:Spring MVC和Spring WebFlux Web框架
  • 集成:远程处理、JMS、JCA、JMX、电子邮件、任务、调度、缓存。
  • 语言:Kotlin、Groovy、动态语言

Spring的优点

IoC-Inversion of Control

控制翻转

控制反转就是把创建和管理 bean 的过程转移给了第三方。而这个第三方,就是 Spring IoC Container,对于 IoC 来说,最重要的就是容器

容器负责创建、配置和管理 bean,也就是它管理着 bean 的生命,控制着 bean 的依赖注入。

通俗点讲,因为项目中每次创建对象是很麻烦的,所以我们使用 Spring IoC 容器来管理这些对象,需要的时候你就直接用,不用管它是怎么来的、什么时候要销毁,只管用就好了。

Bean 其实就是包装了的 Object,无论是控制反转还是依赖注入,它们的主语都是 object,而 bean 就是由第三方包装好了的 object。(想一下别人送礼物给你的时候都是要包装一下的,自己造的就免了。

Bean 是 Spring 的主角,有种说法叫 Spring 就是面向 bean 的编程(Bean Oriented Programming, BOP)。

IOC容器

spring如何设计容器的?

使用 ApplicationContext,它是 BeanFactory 的子类,更好的补充并实现了 BeanFactory 的。

BeanFactory 简单粗暴,可以理解为 HashMap:

  • Key - bean name
  • Value - bean object

但它一般只有 get, put 两个功能,所以称之为“低级容器”。

ApplicationContext 多了很多功能,因为它继承了多个接口,可称之为“高级容器”。

ApplicationContext 的里面有两个具体的实现子类,用来读取配置配件的:

  • ClassPathXmlApplicationContext - 从 class path 中加载配置文件,更常用一些;

  • FileSystemXmlApplicationContext - 从本地文件中加载配置文件,不是很常用,如果再到 Linux 环境中,还要改路径,不是很方便。

    ClassPathXmlApplicationContext 并不是直接继承ApplicationContext的,它有很多层的依赖关系,每层的子类都是对父类的补充实现。

最上层的Class是BeanFactory

Spring中还有个FactoryBean,两者并没有特别的关系,只是名字比较接近。

深入理解IOC

使用class Rectangle举例:

  • 两个变量:长和宽
  • 自动生成set()方法和toString()方法

注意 ⚠️:一定要生成 set() 方法,因为 Spring IoC 就是通过这个 set() 方法注入的;toString() 方法是为了我们方便打印查看。

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
public class Rectangle {
private int width;
private int length;

public Rectangle() {
System.out.println("Hello World!");
}


public void setWidth(int widTth) {
this.width = widTth;
}

public void setLength(int length) {
this.length = length;
}

@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", length=" + length +
'}';
}
}

然后在test文件中手动用set()方法给变量赋值

其实就是解耦的过程

1
2
3
4
5
6
7
8
9
public class MyTest {
@Test
public void myTest() {
Rectangle rect = new Rectangle();
rect.setLength(2);
rect.setWidth(3);
System.out.println(rect);
}
}

其实这就是IoC给属性赋值的实现方法,我们把「创建对象的过程」转移给了set()方法,而不是靠自己去new,就不是自己创建的了。

这里自己创建,指的是直接在对象内部来new,是程序主动创建对象的正向的过程;这里使用set()方法,是别人(test)给我的;而IoC是用它的容器来创建、管理这些对象的,其实也是用的这个set()方法。

依赖注入 - dependency injection

什么是依赖注入?依赖什么?

程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。

什么是注入?注入什么?

配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系。

控制翻转是通过依赖注入实现的。

  • IoC是设计思想,DI是具体的实现方式
  • IOC是理论,DI是实践

为什么要用IoC这种思想?IoC能给我们带来什么好处

解耦

它把对象之间的依赖关系转成配置文件来管理,由Spring IoC Container来管理

在项目中,底层的实现都是由很多个对象组成的,对象之间彼此合作实现项目的业务逻辑。但是,很多很多对象紧密结合在一起,一旦有一方出问题了,必然会对其他对象有所影响,所以才有了解藕的这种设计思想。

AOP

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理

核心的概念:

  • 切面(Aspect):指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的列子。在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以@Aspect注解(@AspectJ注解方式)来实现
  • 连接点(Join point):在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点,在Spring AOP中,一个连接点总是代表一个方法执行。
  • 通知(Advice):在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“arround”,”before”,”after”等等。许多AOP框架,包括Spring在内,都是以拦截器来做通知模型的,并维护着一个以连接点为中心的拦截器链。
  • 切点(Pointcut):匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切点语义。
  • 引入(Introduction):声明二外的方法或者某个类型的字段。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现IsModified接口,以便简化存储机制(在AspectJ社区,引入也被称为内部类型声明(inter))。
  • 目标对象(Target object):被一个或者多个切面所通知的对象。也被乘坐被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象
  • AOP代理(AOP proxy): AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态dialing或CGLIB代理。
  • 织入(weaving):把切面连接到其他的应用程序类型或者对象上,并创建一个被通知的对象的过程。这个过程可以在编译时(例如使用AspectJ编译器)、类加载时或运行时中完成。Spring和其他纯Java AOP框架一样,是在运行时完成织入的。

任何一个系统都是由不同的组件组成的,每个组件负责一块特定的功能,当然会存在很多组件是跟业务无关的,例如日志、事务、权限等核心服务组件,这些核心服务组件经常融入到具体的业务逻辑中,如果我们为每个具体业务逻辑操作都添加这样的代码,很明显代码冗余太多,因此我们需要将这些公共的代码逻辑抽象出来变成一个切面,然后注入到目标对象(具体业务)中去,AOP正是基于这样的一个思路实现的,而不需要修改原有业务的逻辑代码,只需要在原来的业务逻辑基础之上做一些增强功能即可。

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 快很多。

Spring Bean的注入过程

Spring 中的 bean 的作用域有哪些?

  • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求都会创建一个新的 bean 实例。
  • request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
  • session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
  • global-session:全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话

Spring中的Bean创建的生命周期有哪些步骤

Spring中的一个Bean的创建大概分为以下几个步骤:

  1. 推断构造方法
  2. 实例化
  3. 填充属性,也就是依赖注入
  4. 处理Aware回调
  5. 初始化前,处理@PostConstruct注解
  6. 初始化,处理InitializingBean接口
  7. 初始化,进行AOP

TODO 生命周期图

Spring 中的单例 bean 的线程安全问题了解吗?

大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。

常见的有两种解决办法:

  1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。
  2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。

Spring事务

@Transactional注解

就是在方法上使用@Transcational,当该方法报异常时则回滚事务,没有发生异常则提交事务

开启事务最核心的两件事:

  1. 创建一个数据库连接
  2. 把AutoCommit属性改成false

@Transcational注解常用属性

  1. propagation

    该属性用于设置事务的传播行为。

    例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)

  2. value

    事务管理器

  3. readOnly

    用户设置当前事务是否为只读,设置为True的时候表示只读,false表示可读写,默认值为false

  4. rollbackFor

    用户设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:

    指定单一异常类:@Transcational(rollbackFor=RuntimeException.class)

    指定多个异常类:@Transcational(rollbackFor=RuntimeException.class,Exception.class)

  5. rollbackForClassName

    用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。

    指定单一异常异常类名:@Transcational(rollbackForClassName="RuntimeException")

    指定多个异常类名:@Transcational(rollbackForClassName={"RuntimeException","Exception"})

  6. noRollbackFor

    该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:

    指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)

    指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})

  7. noRollbackForClassName
    该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:

    指定单一异常类名称:@Transactional(noRollbackForClassName=“RuntimeException”)

    指定多个异常类名称:

    @Transactional(noRollbackForClassName={“RuntimeException”,“Exception”})

  8. isolation
    该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置

  9. timeout
    该属性用于设置事务的超时秒数,默认值为-1表示永不超时

    如果我们在一个业务类的前边加上@Transactional,那么这个类的所有方法需要事务管理,当执行每一个业务方法开始时都会打开一个事务。

Spring中的事务是如何实现的

  1. Spring事务底层是基于数据库事务和AOP机制
  2. 首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean
  3. 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
  4. 如果加了,那么则会利用事务管理器创建一个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是Spring事务非常重要的一步
  6. 然后执行当前方法,方法中会执行SQL
  7. 执行完当前方法后,如果没有出现异常就直接提交事务
  8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
  9. Spring事务的隔离级别对应的就是数据库的隔离级别
  10. Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
  11. Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行SQL

Spring事务传播机制

多个事务方法互相调用时,事务如何在这些方法间传播,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。

  1. REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务

  2. SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行

  3. MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常

  4. REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。

  5. NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务

  6. NEVER:不使用事务,如果当前事务存在,则抛出异常

  7. NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)

Spring事务什么时候会失效

Spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!

常见情况有如下几种:

  1. 发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService对象本身
  2. 方法不是public的:@Transactional只能用于public的方法上,否则事务不会失效,如果要用来非public方法上,可以开启AspectJ代理模式。
  3. 数据库不支持事务
  4. 没有被Spring管理
  5. 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)

Spring中的循环依赖

什么是循环依赖

图片

如图

在代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class A {
@Autowired
private B b;
}

@Component
public class B {
@Autowired
private A a;
}

// 比较特殊的循环依赖
@Component
public class A {
@Autowired
private A a;
}

什么情况下循环依赖可以被处理

Spring解决循环依赖是有前置条件的:

  1. 出现循环依赖的Bean必选要是单例

  2. 依赖注入的方式不能全是构造器注入的方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Component
    public class A {
    public A(B b) {}
    }

    @Component
    public class B {
    public B(A a) {}
    }

    在上述代码中,A中注入B的方式是通过构造器,B中注入A的方式也是通过构造器,这个时候循环依赖是无法被解决的,如果项目中有两个这样相互依赖的Bean,在启动的时候就会报出以下错误

    Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

依赖情况 依赖注入方式 循环依赖是否被解决
AB相互依赖(循环依赖) 均采用setter方法注入
AB相互依赖(循环依赖) 均采用构造器注入
AB相互依赖(循环依赖) A中注入B的方式为setter方法,B中注入A的方式为构造器
AB相互依赖(循环依赖) B中注入A的方式为setter方法,A中注入B的方式为构造器

Spring是如何解决循环依赖的

关于循环依赖的解决方式应该要分两种情况来讨论:

1. 简单的循环依赖(没有AOP)
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class A {
// A中注入了B
@Autowired
private B b;
}

@Component
public class B {
// B中也注入了A
@Autowired
private A a;
}

Spring在创建Bean的时候默认是按照自然排序来进行创建的,所以第一步Spring会去创建A

与此同时,Spring在创建Bean的过程中分为三步:

  1. 实例化,对应方法:AbstractAutowireCapableBeanFactory中的createBeanInstance方法, 简单理解就是new了一个对象

  2. 属性注入,对应方法:AbstractAutowireCapableBeanFactorypopulateBean方法, 为实例化中new出来的对象填充属性

  3. 初始化,对应方法:AbstractAutowireCapableBeanFactoryinitializeBean, 执行aware接口中的方法,初始化方法,完成AOP代理

    图片

    解读整个循环依赖处理的过程,整个流程应该以A的创建为起点。

    创建A的过程实际上就是调用getBean方法,这个方法有两层含义:

    1. 创建一个新的Bean
    2. 从缓存中获取已经被创建的对象

    我们现在分析的是第一层含义,因为这个时候缓存中还没有A。

    首先是调用getSingleton(a)方法,这个方法又会调用getSingleton(beanName, true)

    1
    2
    3
    public Object getSingleton(String beanName) {
    return getSingleton(beanName, true);
    }
    true)```这个方法实际上就是到缓存中尝试去获取Bean,整个缓存分为三级:
    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
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53

    1. ```singletonObject```,一级缓存,存储的是所有创建好了的单例Bean
    2. ```earlySingletonObjects```,完成实例化,但是还未进行属性注入及初始化的对象
    3. ```singletonFactories```,提前暴露的一个单例工厂,二级缓存中存储的就是从这个工厂中获取到的对象

    因为A是第一次被创建,所以不管哪个缓存中必然都是没有的,因此会进入```getSingleton```的另外一个重载方法```getSingleton(beanName, singletonFactory)```

    这个方法就是用来创建Bean的,起源码如下

    ```java
    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(beanName, "Bean name must not be null");
    synchronized (this.singletonObjects) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null) {

    // ....
    // 省略异常处理及日志
    // ....

    // 在单例对象创建前先做一个标记
    // 将beanName放入到singletonsCurrentlyInCreation这个集合中
    // 标志着这个单例Bean正在创建
    // 如果同一个单例Bean多次被创建,这里会抛出异常
    beforeSingletonCreation(beanName);
    boolean newSingleton = false;
    boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
    if (recordSuppressedExceptions) {
    this.suppressedExceptions = new LinkedHashSet<>();
    }
    try {
    // 上游传入的lambda在这里会被执行,调用createBean方法创建一个Bean后返回
    singletonObject = singletonFactory.getObject();
    newSingleton = true;
    }
    // ...
    // 省略catch异常处理
    // ...
    finally {
    if (recordSuppressedExceptions) {
    this.suppressedExceptions = null;
    }
    // 创建完成后将对应的beanName从singletonsCurrentlyInCreation移除
    afterSingletonCreation(beanName);
    }
    if (newSingleton) {
    // 添加到一级缓存singletonObjects中
    addSingleton(beanName, singletonObject);
    }
    }
    return singletonObject;
    }
    }

    上面的代码我们主要抓住一点,通过createBean方法返回的Bean最终被放到了一级缓存,也就是单例池中。

    那么到这里就可以得出一个结论:一级缓存中存储的是已经完全创建好了的单例Bean

    调用addSingletonFactory方法

    图片

    在完成Bean的实例化后,属性注入之前Spring将Bean包装成一个工厂添加进了三级缓存中,对应代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 这里传入的参数也是一个lambda表达式,() -> getEarlyBeanReference(beanName, mbd, bean)
    protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
    if (!this.singletonObjects.containsKey(beanName)) {
    // 添加到三级缓存中
    this.singletonFactories.put(beanName, singletonFactory);
    this.earlySingletonObjects.remove(beanName);
    this.registeredSingletons.add(beanName);
    }
    }
    }

    这里只是添加了一个工厂,通过这个工厂(ObjectFactory)的getObject方法可以得到一个对象,而这个对象实际上就是通过getEarlyBeanReference这个方法创建的。那么,什么时候会去调用这个工厂的getObject方法呢?这个时候就要创建B的流程了

    当A完成了实例化并添加进了三级缓存后,就要开始为A进行属性注入了,在注入时发现A依赖了B,那么这个时候Spring又会去图片

    因为B需要注入A,所以在创建B的时候,又会去调用getBean(a),这个时候就又回到了之前的流程了,但是不同的是,之前的getBean是为了创建Bean,而此时在调用getBean不是为了创建了,而是要从缓存中获取,因为之前A在实例化后已经将其放入了三级缓存singletonFactories中,所以此时getBean(a)的流程就是这样子了

    图片

    从这里我们可以看出,注入到B中的A是通过getEarlyBeanReference方法提前暴露出去的一个对象,还不是一个完整的Bean,那么getEarlyBeanReference到底干了啥,我们看下它的源码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    for (BeanPostProcessor bp : getBeanPostProcessors()) {
    if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
    SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
    exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
    }
    }
    }
    return exposedObject;
    }

    它实际上就是调用了后置处理器的getEarlyBeanReference,而真正实现了这个方法的后置处理器只有一个,就是通过@EnableAspectJAutoProxy注解导入的AnnotationAwareAspectJAutoProxyCreator.也就是说如果在不考虑AOP的情况下,上面的代码等价于:

    1
    2
    3
    4
    protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    return exposedObject;
    }

    也就是说这个工厂啥都没干,直接将实例化阶段创建的对象返回了!所以说在不考虑AOP的情况下三级缓存没有用

    B中提前注入了一个没有经过初始化的A类型对象不会有问题吗?

    不会

    创建A这个Bean的流程图:

    图片

    从上图中我们可以看到,虽然在创建B时会提前给B注入一个还未初始化的A对象,但是在创建A的流程中一直使用的是注入到B中的A对象的引用,之后会根据这个引用对A进行初始化,所以没有问题。

2. 结合了AOP的循环依赖

在普通的循环依赖的情况下,三级缓存没有任何作用。三级缓存实际上跟Spring中AOP相关

getEarlyBeanReference的代码:

1
2
3
4
5
6
7
8
9
10
11
12
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
return exposedObject;
}

如果在开启AOP的情况下,那么就是调用到AnnotationAwareAspectJAutoProxyCreatorgetEarlyBeanReference方法,对应代码:

1
2
3
4
5
6
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
// 如果需要代理,返回一个代理对象,不需要代理,直接返回当前传入的这个bean对象
return wrapIfNecessary(bean, beanName, cacheKey);
}

我们对A进行了AOP代理的话,那么此时getEarlyBeanReference将返回一个代理后的对象,而不是实例化阶段创建的对象,这样就意味着B中注入的A僵尸一个代理对象而不是A的实例化阶段创建后的对象。

图片

  1. 在给B注入的时候为什么需要注入一个代理对象?

    当我们对A进行了AOP代理时,说明我们希望从容器中获取到的就是A代理后的对象而不是A本身,因此把A当做依赖进行注入时也要注入它的代理对象

  2. 明明初始化的时候是A对象,那么Spring是在哪里讲代理对象放入到容器中的呢?

    图片

    在完成初始化后,Spring又调用了一次getSingleton方法,这一次传入的参数又不一样了,false可以理解为禁用三级缓存,前面途中已经提到过了,在为B中注入A时已经将三级缓存中的工厂取出,并从工厂中获取到了一个对象放入到了二级缓存中,所以这里getSingleton方法做的时间就是从二级缓存中获取到这个代理后的A对象。exposedObject == bean可以认为是必定成立的,除非你非要在初始化阶段的后置处理器中替换掉正常流程中的Bean,例如增加一个后置处理器:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    public class MyPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    if (beanName.equals("a")) {
    return new A();
    }
    return bean;
    }
    }
  3. 初始化的时候是对A对象本身进行初始化,而容器中以及注入到B中的都是代理对象,这样不会有问题吗?

    不对,这是因为不管是cglib代理还是jdk动态代理生成的代理类,内部都持有一个目标类的引用,当调用代理对象的方法时,实际回去调用目标对象的方法,A完成初始化相当于代理对象自身也完成了初始化。

  4. 三级缓存为什么要使用工厂而不是直接使用引用?换言之,为什么需要这个三级缓存,直接通过二级缓存暴露一个引用不行吗?

    这个工厂的目的在于延迟对实例化阶段生成的对象的代理,只有真正发生以来循环的时候,才回提前生成代理对象,否则只会创建一个工厂并将其放入到三级缓存中,但是不会去通过这个工厂去真正创建对象

    以单独创建A为例,假设AB之间现在没有以来关系,但是A被代理了,这个时候A完成实例化后还是会进入下面这段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // A是单例的,mbd.isSingleton()条件满足
    // allowCircularReferences:这个变量代表是否允许循环依赖,默认是开启的,条件也满足
    // isSingletonCurrentlyInCreation:正在在创建A,也满足
    // 所以earlySingletonExposure=true
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
    isSingletonCurrentlyInCreation(beanName));
    // 还是会进入到这段代码中
    if (earlySingletonExposure) {
    // 还是会通过三级缓存提前暴露一个工厂对象
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    即使没有循环依赖,也会将其添加到三级缓存中,而且是不得不添加到三级缓存中,因为到目前为止Spring也不能确定这个Bean有没有跟别的Bean出现循环依赖。

    假设我们在这里直接使用二级缓存的话,那么就意味着所有的Bean在这一步都要完成AOP代理,这不仅没必要,而且违背了Spring在结合AOP跟Bean的声明周期的设计!Spring结合AOP跟Bean的生命周期本身就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器完成的,在这个后置处理器的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那么没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在声明周期的最后一步完成代理而不是在实例化后就立马完成代理。

    3. 三级缓存真的提高了效率吗?
    1. 没有进行AOP的Bean间的循环依赖

      从上文分析可以看出,这种情况下三级缓存根本没用,所以不会存在提高了效率的说法

    2. 进行了AOP的Bean间的循环依赖

      如果A被AOP代理,那么我们先分析下使用了三级缓存的情况下,A、B的创建流程

      图片

      假设不适用三级缓存,直接放在二级缓存中

      图片

      上面两个流程的唯一区别在于为A对象创建代理的时机不同,在使用了三级缓存的情况下为A创建代理的时机是在B中需要注入A的时候,而不使用三级缓存的话再A实例化后就需要马上为A创建代理然后放入到二级缓存中去。

      对于A、B的整个创建过程而言,消耗的时间是一样的。

    所以,三级缓存提高了效率这种说法是错误的。

3. 总结

  1. Spring如何解决循环依赖?

Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。

当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。

当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取:

第一步,先获取到三级缓存中的工厂;

第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。

当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。至此,循环依赖结束!

  1. 为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?

如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。

SpringMVC

Spring MVC的工作流程

  1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet(中央处理器)捕获;

  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回给DispatcherServlet(中央处理器);

    1. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(…)方法)
    2. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
      • HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息数据转换:对请求消息进行数据转换。如String转换成Integer、Double等数据根式化:对请求消息进行数据格式化。
      • 如将字符串转换成格式化数字或格式化日期等数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中.
    3. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
    4. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet ;
    5. ViewResolver 结合Model和View,来渲染视图
    6. 将渲染结果返回给客户端。

Spring Boot

Spring Boot常用注解及底层实现

  1. @SpringBootApplication 注解: 这个注解标识了一个SpringBoot工程,他实际上是另外三个注解的组合:

    a. @SpringBootConfiguration: 这个注解实际上就是一个@Configuration,表示启动类也是一个配置类

    b. @EnableAutoConfiguration: 向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean

    c. @ComponentScan: 标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录

  2. @Bean注解: 用来定义Bean,类似XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字作为BeanName,并通过执行方法得到bean对象

  3. @Controller、@Service、@RequestBody、@AutoWired、@Resource

SpringBoot 自动装配原理

  • .springboot自动装配主要是基于注解编程,和预定优于配置的思想来进行设计的

    自动装配就是自动地把其他组件中的Bean装载到IOC容器中,不需要开发人员再去配置文件中添加大量的配置,

    我们只需要在springboot的启动类上添加一个SptingBootApplication的一个注解,这样就可以开启自动装配

  • @SptingBootApplication这个注解是暴露给用户使用的一个入口,它的底层其实是由@EnableAutoConfiguration这个注解来实现的,

    自动装配的实现,归纳为以下三个核心的步骤:

    第一步:

    启动依赖组件的时候

    组件中必须要包含@Configuration的配置类,在这个配置类里面声明为Bean注解,然后将方法的返回值或者是属性注入到IOC容器中

    第二部:

    第三方jar包,SpringBoot会采用SPI机制,在/META-INF/目录下增加spring.factories文件,然后SpringBoot会自动根据约定,自动使用SpringFactoriesLoader来加载配置文件中的内容

    第三步:

    Spring获取到第三方jar中的配置以后会调用ImportSelector接口来完成动态加载,

    这样设计的好处,在于大幅度减少了臃肿的配置文件,而各模块之间的依赖,也深度的解耦,

    比如我们使用Spring创建Web程序的时候需要引用非常多的Maven依赖,而SpringBoot中只需要引用一个Maven依赖就可以来创建Web程序

    并且SpringBoot把我们常用的依赖都放在了一起,,我们只需要去引入spring-boot-starter-web这个依赖就可以去完成一个简单的Web应用

    以前我们使用Spring的时候需要xml文件来配置开启一些功能,现在使用SpringBoot就不需要xml文件了,

    只需要一个加了@Configuration注解的类,或者是实现了对因接口的配置类就可以了

    SpringBoot自动装配是Spring的完善和扩展,就是为了我们便捷开发,方便测试和部署,提高效率而诞生的框架技术。

SpringBoot的starter

  • spring-boot-starter
  • spring-boot-starter-web
  • spring-boot-starter-aop
  • spring-boot-starter-amqp
  • spring-boot-starter-cache
  • spring-boot-starter-data-elasticsearch
  • spring-boot-starter-data-jpa
  • spring-boot-starter-data-mongodb
  • spring-boot-starter-data-solr
  • spring-boot-starter-jdbc
  • spring-boot-starter-security
  • spring-boot-starter-test
  • spring-boot-starter-log4j
  • spring-boot-starter-tomcat

自定义注解

创建一个注解叫做MyAnnotation,在类的上面有四个源注解。

①、枚举类:enum,指的是常量的集合

②、注解类

  1. 最基本的注解

    1
    2
    3
    4
    5
    public @interface MyAnotation {
    public string name();
    int age();
    String sex() default "女";
    }
  2. 常用的元注解

    元注解:专门修饰注解的注解
    @Target
    @Target是专门用来限定某个自定义注解(如上面的@interface MyAnnotation)能够被应用在哪些Java元素上面的。枚举类型。
    因此,我们可以在使用@Target时指定注解的使用范围

    1
    2
    3
    4
    5
    6
    7
    //@MyAnotation 被限定只能使用在(类、接口)Type或(方法)METHOD上面
    @Target(value = {ElementType.Method, ElementType.TYPE})
    public @interface MyAnotation {
    public string name();
    int age();
    String sex() default "女";
    }

    @Retention注解,用来修饰自定义注解的生命力。
    a.如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;
      b.如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到,是默认的;
      c.如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME。

    使用此注解修饰自定义注解生命力的示例如下:

    1
    2
    3
    4
    5
    6
    7
    //设置注解的生命力在运行期
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnotation {
    public string name();
    int age();
    String sex() default "女";
    }

    @Inherited注解

    是指定某个自定义注解如果写在了父类的声明部分,那么子类(继承关系)的声明部分也能自动拥有该注解。该注解只对@Target被定义为ElementType.TYPE的自定义注解起作用。

    @Documented

    该注解表明被注解信息是否被添加在Javadoc中。

Struts2与SpiringMVC的区别

1
2
3
4
5
6
7
8
9
(1)联系
1)它们都是表现层框架,都是基于mvc模型编写的
2)它们的底层实现都离不开servletAPI
3)它们的处理请求机制都是一个核心控制器
(2)区别
1)springMVC的入口是servlet,Struts的入口是filter;
2)springMVC是基于方法的,Struts是基于类的,Struts每次执行都会创建一个动作
3)springMVC使用更加简洁,同时还支持JSR303,处理ajax请求更方便
4)Struts2的OGNL表达式使页面的开发效率相对于springMVC要更高一些,但是执行的效率没有比JSTL提升,尤其是Struts的表单远没有html执行效率高。

Spring Cloud

设计模式

单例模式

单例有如下几个特点:

  • 在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在
  • 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
  • 没有公开的set方法,外部类无法调用set方法创建该实例
  • 提供一个公开的get方法获取唯一的这个实例

那单例模式有什么好处呢?

  • 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
  • 省去了new操作符,降低了系统内存的使用频率,减轻GC压力
  • 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
  • 避免了对资源的重复占用

饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
// 创建一个实例对象
private static Singleton instance = new Singleton();
/**
* 私有构造方法,防止被实例化
*/
private Singleton(){}
/**
* 静态get方法
*/
public static Singleton getInstance(){
return instance;
}
}

提前把对象new出来,这样别人哪怕是第一次获取这个类对象的时候直接就存在这个类了,省去了创建类这一步的开销。

懒汉式

线程不安全的模式

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {  
private static Singleton instance;
private Singleton (){}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

别人第一次调用的时候他发现自己的实例是空的,然后去初始化了,再赋值,后面的调用就和饿汉没区别了。

图片

在运行过程中可能存在这种情况:多个线程去调用getInstance方法来获取Singleton的实例,那么就有可能发生当第一个线程在执行if(instance==null)时,此时instance是为null的进入语句,在还没有执行instance = new Singleton()时(此时instance为null),第二个语句也进入了if(instance == null)这个语句,因为之前进入这个语句的线程中还没有执行instance=new Singleton(),所以它会执行instance = new Singleton()来实例化Singleton对象,因为第二个线程也进入了if语句所以它会实例化Singleton对象。

这就导致了实例化了两个对象,可以使用加锁进行解决

加锁后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static Singleton instance = null;
/**
* 私有构造方法,防止被实例化
*/
private Singleton(){}
/**
* 静态get方法
*/
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}

这种加锁方法是时间换空间的写法,严重降低了系统的处理速度,可以使用双检锁做两次判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized (Singleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。

但是该代码instance = new Singleton()不是原子操作,会有问题:

  1. A、B线程同时进入了第一个if判断

  2. A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();

  3. 由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

    图片image-20201212010622553

  4. B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

  5. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。

解决办法:加上volatile修饰Singleton:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized (Singleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}

但是volatile关键字可能会屏蔽掉虚拟机中有一些必要的代码优化,可以通过静态内部类来优化:

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

/* 私有构造方法,防止被实例化 */
private Singleton() {
}

/* 此处使用一个内部类来维护单例 */
private static class SingletonFactory {
private static Singleton instance = new Singleton();
}

/* 获取实例 */
public static Singleton getInstance() {
return SingletonFactory.instance;
}

/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
public Object readResolve() {
return getInstance();
}
}

使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。

这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕, 这样我们就不用担心上面的问题。

同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式。

还有更完美的写法吗,通过枚举:

1
2
3
4
5
6
public enum Singleton {
/**
* 定义一个枚举的元素,它就代表了Singleton的一个实例。
*/
Instance;
}

使用枚举来实现单实例控制会更加简洁,而且JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

工厂模式

工厂模式主要可以分为三大类:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工程模式

简单工厂模式

工厂模式主要是用于对实现逻辑的封装,并且通过对公共的接口提供对象的实列画的服务,在我添加新的类时不需大动干戈,只要修改一点点就好。

举例:在电商平台创建商品时:

图片

在这个简单工厂里面,如果要创建活动商品1 以及活动商品2,我们要创建商品的时候只要调用简单工厂里面的创建商品方法,根据类型创建出不同的商品然后实列化返回就可以了。

简单工厂几种实现方式:

静态工厂模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SimpleFactory {
public static Share createProduce(EnumProductType type) {
if (EnumProductType.activityOne.equals(type)) {
return new ProDuctOne();
} else if (EnumProductType.activityTwo.equals(type)) {
return new ProDuctTwo();
}
return null;
}

public enum EnumProductType {
activityOne, activityTwo;
}
}

这种模式在我每增加一种类型都要去修改一次createProduct方法,违背了开闭原则

可以使用以下两种方法进行优化:

  • 使用反射机制
  • 直接注册商品对象,添加一个Type类型方法,根据type类型返回自身相同类型的方法

反射方法的事项:

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
39
40
41
42
43
44
45
public class SimpleFactoryReflection {
private static final Map<EnumProductType, Class> activityIdMap = new HashMap<>();
public static void addProductKey(EnumProductType EnumProduct, Class product) {
activityIdMap.put(EnumProduct, product);
}

public static activityOne product(EnumProductType type) throws IllegalAccessException, InstantiationException {
Class productClass = activityIdMap.get(type);
return (activityOne) productClass.newInstance();
}

public static void main(String[] args) throw InstantiationException, IllegalAccessException {
addProductKey(EnumProductType.activityOne, activityOne.class);
activityOne product = product(EnumProductType.activityOne);
System.out.println(product.toString());
}

public static class Product {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

public static class activityOne extends Product {
private String stock;
@Override
public String toString() {
return "activityOne{" +
"stock='" + stock + '\'' +
'}';
}
}

public class activityTwo extends Product {
private String stock;
}

public enum EnumProductType {
activityOne, activityTwo;
}
}

在一些特定的情况下,并不适用,而且在某些特定的情况下是无法实现的,而且反射机制也会降低程序的运行效果,在对性能要求很高的场景下应该避免这种实现。

这里还有一个问题适用反射不当是容易导致线上机器出问题的,因为我们反射创建的对象属性是被SoftReference软引用的,所以当-XX:SoftRefLRUPolicyMSPerMB 没有设置好的话会一直让机器CPU很高。

当然他的默认值是1000,也就根据大家的情况而定吧,反正就是注意一下这点。

直接注册商品对象

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
39
40
41
42
43
44
45
46
public class SimpleFactoryReflection {
private static final Map<EnumProductType, Product> activityIdMap = new HashMap<>();

public static void addProductKey(EnumProductType EnumProduct, Product product) {
activityIdMap.put(EnumProduct, product);
}

public static activityOne product(EnumProductType type) throws IllegalAccessException, InstantiationException {
Product product = activityIdMap.get(type);
return (activityOne) product;
}

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
addProductKey(EnumProductType.activityOne, new activityOne());
activityOne product = product(EnumProductType.activityOne);
System.out.println(product.toString());
}

public static class Product {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

public static class activityOne extends Product {
private String stock;
@Override
public String toString() {
return "activityOne{" +
"stock='" + stock + '\'' +
'}';
}
}

public class activityTwo extends Product {
private String stock;
}

public enum EnumProductType {
activityOne, activityTwo;
}
}

工厂方法模式

工厂方法模式是对静态工厂模式的上的一种改进,我们的工厂类直接被抽象化,需要具体特定化的逻辑代码转移到实现抽象方法的子类中,这样我们就不要再去修改工厂类(即:不用再去做什么if else 修改)这也是我们当前比较常用的一种方式。

以创建商品为例:

图片

代码:

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
39
40
public abstract class FactoryMethod {
protected abstract Product createProduct(String name);

public Product product(String activity, String name) {
Product product = createProduct(activity);
product.setProductName(name);
return product;
}

public static class Product {
public String productName;

public String getProductName() {
return productName;
}

public void setProductName(String productName) {
this.productName = productName;
}

}

public enum EnumProductType {
activityOne("one"),
activityTwo("two");

private String name;

EnumProductType(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

先创建一个抽象工厂方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ProductFactory extends FactoryMethod{
@Override
protected Product createProduct(String activityId) {
if (EnumProductType.activityOne.getName().equals(activityId)) {
// 这里可以处理我们自己的想要的业务逻辑代码
return new OneProduct();
} else if (EnumProductType.activityTwo.getName().equals(activityId)) {
return new OneProduct();
}
return null;
}

public static class OneProduct extends Product {}

public static class TwoProduct extends Product {}

public static void main(String[] args) {
FactoryMethod factoryMothod = new ProductFactory();
Product product = factoryMothod.Product("one", "one");
System.out.println(product.getProductName());
}
}

再创建一个商品工厂去继承抽象工厂方法。

抽象工厂模式

抽象工厂模式是工厂模式的一个延伸。工厂方法类中只有一个抽象方法,要想实现多种不同的类对象,只能去创建不同的具体工厂方法的子类来实例化,而抽象工厂则是让一个工厂负责创建多个不同类型的对象。

图片

抽象工厂的组成:

  • 抽象工厂类
  • 具体工厂类
  • 抽象类

代理模式

定义

代理模式可以分为多种类型

  • 远程代理:就是将工作委托给远程对象(不同的服务器,或者不同的进程)来完成。常见的是用在web Service中。还有就是我们的RPC调用也可以理解为一种远程代理。
  • 保护代理:该模式主要进行安全/权限检查。(接触很少)
  • 缓存代理:这个很好理解,就是通过存储来加速调用,比如Sping中的@Cacheable方法,缓存特定的参数获取到的结果,当下次相同参数调用该方法,直接从缓存中返回数据。
  • 虚拟代理:这种代理主要是为方法增加功能,比如记录一些性能指标等,或进行延迟初始化

图片

  • Subject(共同接口): 客户端使用的现有接口
  • RealSubject(真实对象):真实对象的类
  • ProxySubject(代理对象):代理类

目的:提供一个实际代理对象,以便更好的控制实际对象。

代码举例
1
2
3
4
public interface Subject {
// 共同的接口
void doSomething();
}

定义一个共同的接口:

1
2
3
4
5
6
7
public class RealSubject implements Subject {
// 真实对象
@Override
public void doSomething() {
System.out.println(".....")
}
}

构建一个真实对象:

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
public class ProxySubject implements Subject {
private RealSubject realSubject;

public ProxySubject(RealSubject realSubject) {
this.realSubject = realSubject;
}

public ProxySubject() throws ClassNotfoundException, IllegalAccessException, InstantiationException {
this.realSubject = (RealSubject) this.getClass().getClassLoader().loadClass("com.ao.bing.demo.proxyPattern.RealSubject").newInstance();
}

@Override
public void doSomething() {
realSubject.doSomething();
}

public static void main(String[] args) {
try {
// 第一种方式
new ProxySubject().doSomething();
// 打印结果
} catch (Exception e) {
// 异常情况,代理失败
}

// 第二种方式
new ProxySubject(new RealSubject()).doSomething();
}
}
  • 第一种:采用类加载器形式,去加载实列对象,这样我们就不同关心到底什么时候需要真实的实列化对象

  • 第二种:通过传值的形式,把实列化对象传过来。(理解为装饰器模式了)

    这里大家要区别一下,代理模式是提供完全相同的接口,而装饰器模式是为了增强接口

静态代理、动态代理和cglib代理分析
静态代理

上面的例子就是静态代理。缺点比较明显,静态代理需要为每一个对象都创建一个代理类,增加了维护成本以及开发成本。

动态代理

动态代理合理的避免了静态代理的那种方式,不用事先为代理的类而构建好代理类,而是在运行时通过反射机制创建。

在写动态代理时需要理解两个东西:Proxy可以理解为就是调度器,InvocationHandler增强服务接口可以理解为代理器。

可以理解为:动态代理其实就是一种行为的监听

代码举例:螳螂捕蝉,通过螳螂监听到蝉的动作

1
2
3
4
5
6
7
8
9
10
public interface BaseService {
void mainService();
}

public class Cicada implements BaseService {
@Override
public void mainService() {
System.out.println("。。。。。");
}
}

创建共同接口,以及真实对象蝉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PrayingMantis implements IvocationHandler {
private BaseService baseService;

// 这里采用的是构建传参,可以用反射
publci PrayingMantis(BaseService baseService) {
this.baseService = baseService;
}

// 螳螂主要业务,也就是监听对象
@Override
public Object invoke(Object listener, Method method, Object[] args) throws Throwable {
method.invoke(baseService, args);
secondaryMain();
return null;
}

// 这里理解增强业务,即我们可以在实现InvocationHandler里面添加其他的业务,比如日志等等
private void secondaryMain() {
System.out.println("");
}
}

创建螳螂类,监听着蝉的类的动作www

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BeanFactory {
public static BaseService newInstanc(Class classFile) {
// 1.创建蝉,真实类对象
BaseService trueCicada = new Cicada();
// 2.创建代理类 螳螂
InvocationHanler prayingManties = new PrayingMantis(trueCicada);
// 3.向jvm索要代理对象,其实就是监听对象
Class classArray[] = {BaseService.class};
BaseService baseService = (BaseService) Proxy.newProxyInstance(classFile.getClassLoader(), classArray, prayingMantis);
return baseService;
}

public static void main(String[] args) {
BaseService baseService = new Instanc(Cicada.class);
baseService.mainService();

}
}

代理模式的组成:

  • 接口:声明需要被监听行为
  • 代理实现类:次要业务,次要业务和主要业务绑定执行
  • 代理对象(监听对象)
cglib动态代理

cglib动态代理其实和jdk的动态代理是很相似的,都是要去实现代理器接口完成。

具体代买如下:

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 ParyingMantis implements MethodInterceptor {
private Cicada cicada; // 代理对象

public Cicada getInstance(Cicada cicada) {
this.cicada = cicada;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.cicada.getClass());
enhancer.setCallback(this);
return (Cicada) enhancer.create();
}

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object object = methodProxy.invokeSuper(o, object);
secondaryMain();
return object;
}

private void secondaryMain() {
System.out.println("");
}

public static void main(String[] args) {

}

}

cglib无需通过接口来实现,它是通过实现子类的方式来完成调用的。

Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因为是采用继承方式,所以代理类不能加final修饰,否则会报错

final类:类不能被继承,内部的方法和变量都变成final类型

JDK和cglib的区别

jdk动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前嗲用InvokeHanlder来处理

cglib动态代理时利用ASM开源包,对呗代理对象类的class文件加载进来,通过修改其字节码生成子类来处理

ASM:一个Java字节码操控框架。它能被用来动态生成类或者增强既有

多级动态代理实现
总结

Linux

说说常用的Linux基本操作命令

  1. cd

  2. ls

    1
    2
    3
    4
    5
    -l 列出长数据串,包含文件的属性与权限数据等
    -a 列出全部文件,连同隐藏文件
    -d 仅列出目录本身,而不是列出目录的文件数据
    -h:将文件容量以较易读的方式(GB,kB等)列出来
    -R:连同子目录的内容一起列出(递归列出),等于该目录下的所有文件都会显示出来
  3. grep

  4. find

  5. cp

  6. mv

  7. rm

  8. ps

  9. kill

  10. file

  11. tar

  12. cat

  13. chgrp

  14. chown

  15. chmod

数据库

SQL注入

  1. 对输入进行严格的转义和过滤

    可以使用正则表达式匹配安全的字符串

    在拼接 SQL 语句之前就要进行校验,验证其有效性。比如对于某个传入的值,如果可以确定是整型,则要判断它是否为整型,在浏览器端(客户端)和服务器端都需要进行验证

  2. 使用参数化(Parameterized):目前有很多ORM框架会自动使用参数化解决注入问题,但其也提供了”拼接”的方式,所以使用时需要慎重!

    Mybatis中要使用#作为参数,$不会有类型限制

  3. PDO预处理 (Java、PHP防范推荐方法:)

MySQL索引类型

存储方式区分

1) B-树索引

B-树索引又称为 BTREE 索引,目前大部分的索引都是采用 B-树索引来存储的。

B-树索引是一个典型的数据结构,其包含的组件主要有以下几个:

  • 叶子节点:包含的条目直接指向表里的数据行。叶子节点之间彼此相连,一个叶子节点有一个指向下一个叶子节点的指针。
  • 分支节点:包含的条目指向索引里其他的分支节点或者叶子节点。
  • 根节点:一个 B-树索引只有一个根节点,实际上就是位于树的最顶端的分支节点。

基于这种树形数据结构,表中的每一行都会在索引上有一个对应值。因此,在表中进行数据查询时,可以根据索引值一步一步定位到数据所在的行。

B-树索引可以进行全键值、键值范围和键值前缀查询,也可以对查询结果进行 ORDER BY 排序。但 B-树索引必须遵循左边前缀原则,要考虑以下几点约束:

  • 查询必须从索引的最左边的列开始。
  • 查询不能跳过某一索引列,必须按照从左到右的顺序进行匹配。
  • 存储引擎不能使用索引中范围条件右边的列。
2) 哈希索引

哈希(Hash)一般翻译为“散列”,也有直接音译成“哈希”的,就是把任意长度的输入(又叫作预映射,pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。

哈希索引也称为散列索引或 HASH 索引。MySQL 目前仅有 MEMORY 存储引擎和 HEAP 存储引擎支持这类索引。其中,MEMORY 存储引擎可以支持 B-树索引和 HASH 索引,且将 HASH 当成默认索引。

HASH 索引不是基于树形的数据结构查找数据,而是根据索引列对应的哈希值的方法获取表的记录行。哈希索引的最大特点是访问速度快,但也存在下面的一些缺点:

  • MySQL 需要读取表中索引列的值来参与散列计算,散列计算是一个比较耗时的操作。也就是说,相对于 B-树索引来说,建立哈希索引会耗费更多的时间。
  • 不能使用 HASH 索引排序。
  • HASH 索引只支持等值比较,如“=”“IN()”或“<=>”。
  • HASH 索引不支持键的部分匹配,因为在计算 HASH 值的时候是通过整个索引值来计算的。

逻辑区分

1) 普通索引

普通索引是 MySQL 中最基本的索引类型,它没有任何限制,唯一任务就是加快系统对数据的访问速度。

普通索引允许在定义索引的列中插入重复值和空值。

创建普通索引时,通常使用的关键字是 INDEX 或 KEY。

例 1

下面在 tb_student 表中的 id 字段上建立名为 index_id 的索引。

CREATE INDEX index_id ON tb_student(id);

2) 唯一索引

唯一索引与普通索引类似,不同的是创建唯一性索引的目的不是为了提高访问速度,而是为了避免数据出现重复。

唯一索引列的值必须唯一,允许有空值。如果是组合索引,则列值的组合必须唯一。

创建唯一索引通常使用 UNIQUE 关键字。

例 2

下面在 tb_student 表中的 id 字段上建立名为 index_id 的索引,SQL 语句如下:

CREATE UNIQUE INDEX index_id ON tb_student(id);

其中,id 字段可以有唯一性约束,也可以没有。

3) 主键索引

顾名思义,主键索引就是专门为主键字段创建的索引,也属于索引的一种。

主键索引是一种特殊的唯一索引,不允许值重复或者值为空。

创建主键索引通常使用 PRIMARY KEY 关键字。不能使用 CREATE INDEX 语句创建主键索引。

4) 空间索引

空间索引是对空间数据类型的字段建立的索引,使用 SPATIAL 关键字进行扩展。

创建空间索引的列必须将其声明为 NOT NULL,空间索引只能在存储引擎为 MyISAM 的表中创建。

空间索引主要用于地理空间数据类型 GEOMETRY。对于初学者来说,这类索引很少会用到。

例 3

下面在 tb_student 表中的 line 字段上建立名为 index_line 的索引,SQL 语句如下:

CREATE SPATIAL INDEX index_line ON tb_student(line);

其中,tb_student 表的存储引擎必须是 MyISAM,line 字段必须为空间数据类型,而且是非空的。

5) 全文索引

全文索引主要用来查找文本中的关键字,只能在 CHAR、VARCHAR 或 TEXT 类型的列上创建。在 MySQL 中只有 MyISAM 存储引擎支持全文索引。

全文索引允许在索引列中插入重复值和空值。

不过对于大容量的数据表,生成全文索引非常消耗时间和硬盘空间。

创建全文索引使用 FULLTEXT 关键字。

例 4

在 tb_student 表中的 info 字段上建立名为 index_info 的全文索引,SQL 语句如下:

CREATE FULLTEXT INDEX index_info ON tb_student(info);

其中,index_info 的存储引擎必须是 MyISAM,info 字段必须是 CHAR、VARCHAR 和 TEXT 等类型。

什么字段创建索引比较好

主键自动建立唯一索引

频繁作为查询条件的字段

外键关系字段

参与排序的字段

分组依据的字段

组合查询的字段

不创建索引:

在表中出现次数少的字段

经常增删改的字段

where几乎不参与的字段

过滤性不好的字段

索引命中

最左匹配原则

1、先定位该sql的查询条件,有哪些,那些是等值的,那些是范围的条件。

2、等值的条件去命中索引最左边的一个字段,然后依次从左往右命中,范围的放在最后。

mysql索引失效

  1. like查询以“%”开头;当like前缀没有%,后缀有%时,索引有效

    如果要查询的字段都建立过索引,那么引擎会直接在索引表中查询而不会访问原始数据(否则只要有一个字段没有建立索引就会做全表扫描),这叫索引覆盖,因此我们需要尽可能的在select后只写必要的查询字段,以增加索引覆盖的几率。

    因为,LIKE查询以%开头使用了索引的原因就是使用了索引覆盖。
    针对二级索引MySQL提供了一个优化技术。即从辅助索引中就可以得到查询的记录,就不需要回表再根据聚集索引查询一次完整记录。使用索引覆盖的一个好处是辅助索引不包含整行记录的所有信息,故其大小要远小于聚集索引,因此可以减少大量的IO操作,但是前提是要查询的所有列必须都加了索引。

    LIKE查询以%开头会导致全索引扫描或者全表扫描,如果没有索引覆盖的话,查询到的数据会回表,多了一次IO操作,当MySQL预估全表扫描或全索引扫描的时间比走索引花费的时间更少时,就不会走索引。有了索引覆盖就不需要回表了,减少了IO操作,花费的时间更少,所以就使用了索引。

    总结:就是说如果查询的结果中只包含主键和索引字段则会使用索引,反之则不会。
    那有人就会说了,为什么我在设计表的时候,不把所有的字段全设置为索引呢,*这里值得注意的是不要想着为每个字段建立索引,因为优先使用索引的优势就在于其体积小 *

  2. or语句前后没有同时使用索引;

  3. 组合索引中不是使用第一列索引;

  4. 在索引列上使用“IS NULL”或“IS NOT NULL”操作;

  5. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引

  6. 在索引字段上使用“not”,“<>”,“!=”等等。

  7. 对索引字段进行计算操作、字段上使用函数。

  8. 当全表扫描速度比索引速度快时,mysql会使用全表扫描,此时索引失效

Mysql调优

  1. 排除缓存干扰
    • MySQL 8.0 之前数据库是存在索引的情况
    • 因为SQL有缓存,所以线下执行会很快,但是线上会遇到索引失效的问题
    • 所以需要我们测试的时候加上SQL NOCACHE去跑SQL
    • 缓存失效的原因就是一对表进行更新,那这个表的所有缓存都会被清空
    • MySQL 8.0不用担心这个问题
  2. Explain
  3. 覆盖索引
  4. 联合索引
  5. 最左匹配原则
  6. 索引下推
  7. 唯一索引普通索引难题
  8. 前缀索引
  9. 条件字段函数操作
  10. 隐式字符编码转换
  11. flush

Mysql索引有哪几种类型

  1. hash

  2. B+树

    1. 为什么用B+树

      1. 不用平衡二叉树是因为树高太高了,树高会导致IO变多

      2. 为什么不用B树

        1)B树的每个结点都存储了key和data,B+树的data存储在叶子节点上。

        节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。

        2)树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录

        由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

MyISAM和InnoDB的区别

  • MyISAM

    • 是非聚集索引
    • 他的索引直接指向的是数据项
    • MyISAM只支持表锁
  • InnoDB

    • 用的是追溯索引
    • 他的索引是指向他的主键索引,他主键索引采取相同的数据项
    • InnoDB支持redo log日志
    • InnoDB支持事务
    • InnoDB支持行锁

Mysql最多可以支持16个索引

MySQL和Oracle的区别

本质区别

  • Oracle数据库是一个对象关系数据库管理系统(收费)
  • MySQL是一个开源的关系数据库管理系统(免费)

数据库的安全性

  • MySQL使用三个参数来验证用户,即用户名、密码和位置
  • Oracle使用了更多的安全功能,如用户名、密码、配置文件、本地身份验证、外部身份验证、高级安全增强功能

权限

MySQL的权限系统是通过继承形式的分层结构。权限授于高层时,其他低层隐式继承被授予的权限,当然低层也可改写这些权限

按授权范围不同,MySQL有以下种授权方式:

  1. 全局
  2. 基于每个主机;
  3. 基于表
  4. 基于表列

每一级在数据库中都有一个授权表。当进行权限检查时,MySQL从高到低检查每一张表,低范围授权优于高范围授权。

与Oracle不同,MySQL没有角色的概念。也就是说,如果对一组用户授予同样的权限,需要对每一个用户分别授权。

模式迁移

模式包含表、视图、索引、用户、约束、存储过程、触发器和其他数据库相关的概念。多数关系型数据库都有相似的概念。

包含内容如下:

  1. 模式对象的相似性;
  2. 模式对象的名称;
  3. 表设计时的关注点;
  4. 多数据库整合;
  5. MySQL模式整合的关注点

模式对象的相似性

就模式对象,Oracle和MySQL存储诸多的相似,但也有一些不同

img

模式对象的名称

Oracle是大小写不敏感的,并且模式对象是以在写时进行存储。在Oracle的世界中,列、索引、存储过程、触发器以及列别名都是大小写不敏感,并且在所有平台都是如此。MySQL是大小写敏感的,如数据库相对的存储路径、表对应的文件都是如此

当把关键字用引号引起来时,Oracle和MySQL都允许把这些关键字用于模式对象。但MySQL对于一些关键字,不加引号也行。

表设计的关注点

  1. 字符数据的类型;
  2. 列默认值

多数据库迁移

如果多个MySQL数据库位于同一个数据库服务商,支持迁移

数据存储概念

MySQL的数据库对于应用于服务器上数据目录内的了目录,这一数据存储方式与多数据数据库不同,也包括Oracle。数据库中的表对应一个或者多个数据库目录下的文件,并取表存储时的存储引擎。

一个Oracle数据库包含一个或者多个表空间。表空间对应数据在磁盘上的物理存储。表空间是从一个或者多个数据文件开始构建的。数据文件是文件系统中的文件或者原始存储的一块空间。

语法上的区别

主键:
  • MySQL一般使用自动增长类型,在创建表的时候指定表的主键为auto increment,主键就会自动增长。
  • Oracle中没有自动增长,主键一般使用序列,插值时一次赋值即可
引号问题:
  • Oracle不使用双引号,会报错
  • MySQL对双引号没有限制
分页查询:
  • MySQL分页查询使用关键字Limit来实现
  • Oracle没有实现分页查询的关键字,实现起来比较复杂,在每个结果集中只有一个rownum字段标明它的位置,并且只能用rownum<=某个数字,不能用rownum>=某个数,因为ROWNUM是伪列,在使用时所以需要为ROWNUM取一个别名,变成逻辑列,然后来操作

数据类型

  • MySQL中的整型:int() 字符类型: varchar()
  • Oracle中的整型: number() 字符类型: varchar2()

Mybatis

Mybatis #和$的区别

#传入的参数在SQL中显示为字符串,$传入的参数在SqL中直接显示为传入的值.

例:使用以下SQL

select id,name,age from student where id =#{id}
当我们传递的参数id为 “1” 时,上述 sql 的解析为:

select id,name,age from student where id ="1"
$传入的参数在SqL中直接显示为传入的值

例:使用以下SQL

select id,name,age from student where id =${id}
当我们传递的参数id为 “1” 时,上述 sql 的解析为:

select id,name,age from student where id =1
2、#可以防止SQL注入的风险(语句的拼接);但$无法防止Sql注入。

3、$方式一般用于传入数据库对象,例如传入表名。

4、大多数情况下还是经常使用#,一般能用#的就别用$;但有些情况下必须使用$,例:MyBatis排序时使用order by 动态参数时需要注意,用$而不是#。

Redis

Redis如何进行数据存取的

redis的最底层存储结构实质上就是HashMap,也就是数组和链表实现的.

他通过哈希函数来确定插入位置,但数组就这么大,键值对总会有冲突,因此我们通过链表的形式来解决,实现纵向拓展。随着数据更大,不断增长的链表会拖慢我们的速度,这个时候就需要扩容,也就有了我们rehash的方式,我们会重新申请一个是原来两倍的空间把数据复制过去。

我们的数据存储与数据类型没有关系,所有的数据都存放在value里面,那么数据类型影响什么呢,会影响我们在value里面的存储方式,这也就是不同数据类型的数据结构带来的不同。但他们本质上都是在hashmap上存储。

开发选型为什么选型Redis

  1. 复杂数据结构,选择Redis更合适

    value是哈希、列表、集合、有序集合这类复杂的数据结构时,会选择redis,因此memcache无法满足这些需求

  2. 持久化,选择redis更合适

    mc无法满足持久化的需求

  3. 高可用

    redis天然支持集群功能,可以实现主动复制,读写分离。

  4. 存储的内容比较大,选择redis更合适

    mc的value存储最大为1m。

Redis单线程为什么执行速度这么快

(1):纯内存操作,避免大量访问数据库,减少直接读取磁盘数据,redis将数据存储在内存里,读写数据的时候都不会硬盘I/O速度的限制,所以速度快

(2):单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗cpu,不用去考虑各种锁的问题,不存在加锁释放锁的操作,没有因为可能出现死锁而导致的性能消耗

(3):采用了非阻塞I/O多路复用机制

Redis数据结构底层实现

String:

(1)Simple dynamic string (SDS)的数据结构

1
2
3
4
5
6
7
8
9
struct sdshdr {
// 记录buf数组 中已使用字节的数量
// 等于SDS保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buff[];
}

它的优点:

(1)不会出现字符串变更造成的内存溢出问题

(2)获取字符串长度时间复杂度为1

(3)空间预分配, 惰性空间释放free字段,会默认留够一定的空间防止多次重分配内存

应用场景:String 缓存结构体用户信息,计数

Hash:

数组+链表的基础上,进行了一些rehash优化;1.Reids的Hash采用链地址法来处理冲突,然后它没有使用红黑树优化。

2.哈希表节点采用单链表结构。

3.rehash优化 (采用分而治之的思想,将庞大的迁移工作量划分到每一次CURD中,避免了服务繁忙)

应用场景:保存结构体信息可部分获取不用序列化所有字段

List:

应用场景:(1):比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现

(2):list的实现为一个双向链表,即可以支持反向查找和遍历

Set:

内部实现是一个 value为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员 是否在集合内的原因。应用场景:去重的场景,交集(sinter)、并集(sunion)、差集(sdiff),实现如共同关注、共同喜好、二度好友等功能

Zset:

内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。跳表:每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的 应用场景:实现延时队列

Spring Redis是如何存储session的

img

用户访问服务器端,服务器端会判断请求对象cookie中是否包含session的键值对

  1. 如果 cookie 不包含 Session 键值对会创建 Session 对象,对象中包含:

    1. creationTime创建时间、
    2. lastAccessedTime 最后一次访问时间、
    3. maxInactiveInterval 有效时间。
  2. 如果 cookie 包含 Session 键值对,根据 session 键值对的值拼接 spring:session:value

  3. 从redis 中取出 session 对象。首先会判断 Session 对象是否过期。如果过期了,需要重新创建。执行步骤 2

  4. 如果没有过期,刷新 lastAccessedTime。刷新后重新放入到 redis 中。

Redis事务

(1):Multi开启事务

(2):Exec执行事务块内命令

(3):Discard 取消事务

(4):Watch 监视一个或多个key,如果事务执行前key被改动,事务将打断

Redis事务的实现特征

(1):所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行

(2):Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行

(3):在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行

(4):当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。

然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。

Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了

缓存雪崩以及处理办法

原理:同一时刻大量缓存失效;

处理方法:

(1):缓存数据增加过期标记

(2):设置不同的缓存失效时间

(3):双层缓存策略C1为短期,C2为长期

(4):定时更新策略

缓存击穿原因以及处理办法

原理:频繁请求查询系统中不存在的数据导致;

处理方法:

(1):cache null策略,查询反馈结果为null仍然缓存这个null结果,设置不超过5分钟过期时间

(2):布隆过滤器,所有可能存在的数据映射到足够大的bitmap中 google布隆过滤器:基于内存,重启失效不支持大数据量,无法在分布式场景 redis布隆过滤器:可扩展性,不存在重启失效问题,需要网络io,性能低于google

Redis如何做持久化

bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据 ,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来 实 现完整恢复重启之前的状态。

bgsave的原理是什么?

fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写进的页面数据会逐渐和子进程分离开来。

RDB与AOF区别

(1):R文件格式紧凑,方便数据恢复,保存rdb文件时父进程会fork出子进程由其完成具体持久化工作,最大化redis性能,恢复大数据集速度更快,只有手动提交save命令或关闭命令时才触发备份操作;

(2):A记录对服务器的每次写操作(默认1s写入一次),保存数据更完整,在redis重启是会重放这些命令来恢复数据,操作效率高,故障丢失数据更少,但是文件体积更大;

主从、Cluster和哨兵的区别

  1. 主从模式 可以实现读写分离,数据备份。但是并不是「高可用」的
  2. 哨兵模式 可以看做是主从模式的「高可用」版本,其引入了 Sentinel 对整个 Redis 服务集群进行监控。但是由于只有一个主节点,因此仍然有写入瓶颈。
  3. Cluster 模式 不仅提供了高可用的手段,同时数据是分片保存在各个节点中的,可以支持高并发的写入与读取。当然实现也是其中最复杂的。

ELK

ES常用API

索引操作

  1. 查看索引健康状态: GET _cluster/health?level=-indices
  2. 创建索引:PUT users
  3. 删除索引: DELETE users
  4. 查看索引设置: GET users/_settings

文档操作

  1. 创建文档

    1
    2
    3
    4
    5
    6
    PUT users/user/1
    {
    "name": "zhangsan",
    "age": 20,
    "sex": 1
    }
  2. 批量插入文档

    1
    2
    3
    4
    5
    POST books/name/_bulk
    {"index": {"_id":1}}
    {"name": "《Java编程思想》"}
    {"index": {"_id": 2}}
    {"name": "《代码简洁之道》"}
  3. 查看一个索引的所有文档

    1
    GET books/_search
  4. 查看一个索引的所有文档

    1
    GET books/_search
  5. 查看指定id的文档

    1
    GET books/book/2
  6. 修改文档

    1. POST方式:

      1
      2
      3
      POST books/name/_bulk
      {"index": {"_id": "3"}}
      {"update": "hello"}
    2. PUT方式:

      1
      2
      PUT books/name/3
      {"name": "java"}
  7. 删除文档

    1
    DELETE books/name/3
  8. 文档查询

    单条件搜索:

    1
    GET bank/account/_search?q=firstname:Virginia

    多条件搜索:

    1
    2
    3
    4
    5
    6
    7
    8
    GET student/_search 
    {
    "query": {
    "match": {
    "sex": "女"
    }
    }
    }

    ES的倒排索引是什么

传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。

而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。

有了倒排索引,就能实现O(1)时间复杂度的效率检索文章了,极大的提高了检索效率。

倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。

FST有两个优点:

1、 空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;

2、 查询速度快。O(len(str))的查询时间复杂度。

ES在部署时,对Linux的设置有哪些优化

  1. 关闭缓存swap
  2. 堆内存设置:Min(节点内存/2,32GB);
  3. 设置最大文件句柄数
  4. 线程池+队列大小根据业务需要做调整
  5. 磁盘存储raid方式–存储有条件使用RAID10,增减单节点性能以及避免单节点存储故障

详细描述一下ES索引文档过程

这里的索引文档应该理解为文档写入ES,创建索引的过程。

文档写入包含:单文档写入和批量bulk写入。

单文档写入流程

img

  1. 第一步:客户写集群某节点写入数据,发送请求

  2. 第二部:节点1接受到请求后,使用文档_id来确定文档属于分片0.请求会被转到另外的节点,假定节点3,。因此分片0的主分片分配到节点3上

    文档获取分片过程?

    会借助路由算法获取,路由算法就是根据路由和文档id计算目标的分片id的过程

    shard = hash(_routing) % (num_of_primary_shards)

  3. 第三部:节点3在主分片上执行写操作,如果成功,则将请求并行转发到节点1和节点2的副本分片上,等待结果返回。所有的副本分片都报告成功,节点3将想协调节点(节点1)报告成功,节点1箱请求客户端报告写入成功

批量bulk写入流程

MQ

Kafka

Kafka是什么

Kafka是一种高吞吐量、分布式、基于发布/订阅的消息系统,最初由LinkedIn公司开发,使用Scala语言编写,目前是Apache的开源项目。

broker: Kafka服务器,负责消息存储和转发

topic:消息类别,Kafka按照topic来分类消息

partition: topic的分区,一个topic可以包含多个partition, topic 消息保存在各个partition上.

offset:消息在日志中的位置,可以理解是消息在partition上的偏移量,也是代表该消息的唯一序号

Producer:消息生产者

Consumer:消息消费者

Consumer Group:消费者分组,每个Consumer必须属于一个group

Zookeeper:保存着集群 broker、 topic、 partition等meta 数据;另外,还负责broker故障发现, partition leader选举,负载均衡等功能

zookeeper对于kafka的作用是什么?

Zookeeper 主要用于在集群中不同节点之间进行通信,在 Kafka 中,它被用于提交偏移量,因此如果节点在任何情况下都失败了,它都可以从之前提交的偏移量中获取,除此之外,它还执行其他活动,如: leader 检测、分布式同步、配置管理、识别新节点何时离开或连接、集群、节点实时状态等等。

Kafka的哪些设计让它有如此高的性能?

  1. kafka是分布式的消息队列
  2. 对log文件进行了segment,并对segment创建了索引
  3. (对于单节点)使用了顺序读写,速度能够达到600m/s
  4. 使用了zeror拷贝,在os系统就完成了读写操作

Java发送消息给kafka

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
39
40
public class ProducerTest {

public static void main(String[] args) {
Properties props = new Properties();
// 必须
props.put("bootstrap.servers","121.5.240.148:9092");
// 被发送到broker的任何消息的格式都必须是字节数组
props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 非必须参数配置
// acks=0表明producer完全不管发送结果;
// acks=all或-1表明producer会等待ISR所有节点均写入后的响应结果;
// acks=1,表明producer会等待leader写入后的响应结果
props.put("acks","-1");
// 发生可重试异常时的重试次数
props.put("retries",3);
// producer会将发往同一分区的多条消息封装进一个batch中,
// 当batch满了的时候,发送其中的所有消息,不过并不总是等待batch满了才发送消息;
props.put("batch.size",323840);
// 控制消息发送延时,默认为0,即立即发送,无需关心batch是否已被填满。
props.put("linger.ms",10);
// 指定了producer用于缓存消息的缓冲区大小,单位字节,默认32MB
// producer启动时会首先创建一块内存缓冲区用于保存待发送的消息,然后由另一个专属线程负责从缓冲区中读取消息执行真正的发送
props.put("buffer.memory",33554432);
// 设置producer能发送的最大消息大小
props.put("max.request.size",10485760);
// 设置是否压缩消息,默认none
props.put("compression.type","lz4");
// 设置消息发送后,等待响应的最大时间
props.put("request.timeout.ms",30);

Producer<String,String> producer = new KafkaProducer<String, String>(props);
for(int i = 0;i<5;i++){
producer.send(new ProducerRecord<>("my-replicated-topic","key"+i,"value"+i));
}

producer.close();

}
}

Web基础

如何保持会话一致性

  1. 客户端存储法:

    将session存储到浏览器cookie中,每个端只要存储一个用户的数据

    优点:服务端不需要存储

    缺点:

    • 每次HTTP请求都携带Session,占外网带宽
    • 数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患
    • session存储的数据大小受cookie限制
  1. 后端统一存储:

    将session存储在服务器后端的存储层,数据库或者缓存

    优点:

    • 没有安全隐患
    • 可以水平扩展,数据库/缓存水平切分即可
    • web-server重启或者扩容都不会有session丢失

问题排查

线上问题如何排查?排查思路?

磁盘空间不足问题
CPU 与内存使用率过高问题

JPS命令查询java进程的pid,再用top -p pid命令查看这个java进程的情况。

VIRT 表示使用的虚拟内存数量,RES 表示使用的物理内存数量,SHR 表示使用的共享内存数量,这三者可以从内存角度看该进程的资源占用情况。

S 表示进程的状态,下面的值 S 表示睡眠,D 表示不可中断睡眠,R 表示运行,基本知道这三个就够了。

后面两个值是百分比,%CPU 自然就是 CPU 使用率,%MEM 自然就是内存使用率,看这俩值可以一目了然看谁占用的资源过高了。

TIME 表示累计 cpu 使用时长。

如果再细化到线程,可以加个 -H 参数,top -p 19063 -H

当然,top 命令已经可以分析内存了,如果想单独分析下内存,可以用小而美的命令,free -h

可以用 swapon 命令来看下交换区的使用情况。

网络延迟
Java 程序的问题分析

jmap -dump 分析堆内存中的快照

jmap -heap 查看堆内存设置与当前使用情况

jstack 查看 jvm 线程运行信息,上传到 fastthread.io 这个网站,直观地看一下,一看线程有点多。