陈乾坤面试总结

BAT大佬总结面试题

  • 2021面试题:https://mp.weixin.qq.com/s/PlVL7eZm4QZ-HEPN9EMCyw

  • 线程池:https://mp.weixin.qq.com/s/v3eClGAgC7iDW09MoDKiEA

  • Spring:https://mp.weixin.qq.com/s/7V92KCuLfAhWRTcek6t26A

JavaSE

1 面向对象的特点

面向对象是利于语言对现实事物进行抽象。面向对象具有以下特征:

**继承:**继承是从已有类得到继承信息创建新类的过程

**封装:**通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。

**多态性:**多态性是指允许不同子类型的对象对同一消息作出不同的响应。

2 你是怎样理解多态的?

同一个行为具有多个不同表现形式或形态的能力。

父类引用指向子类对象,list就是典型的一种多态的体现形式。
List<String> list = new ArrayList<String>();

3 重载与重写(上海)

**重载:**在同一个类中,重载的方法名必须相同,重载的参数列表不同,与返回值无关。

**重写:**子类继承父类。重写的参数列表必须相同,重写的方法名相同且返回值类型必须相同。重写的访问权限比父类高,构造方法不能被重写。抛出的异常范围小于等于父类

4 接口与抽象类(上海)

接口抽象类
extend,单继承implements,多实现
没有构造器有构造器
只能是public有public、protected和default修饰符
只能声明常量可以有成员变量
除了不能实例化抽象类之外,它和普通Java类没有任何区别

从上到下说:关键字,实现方式,构造方法,成员变量,修饰符

5 深拷贝与浅拷贝(上海)

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

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

  • 深拷贝:既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

总结

​ 深拷贝:地址不同,多个对象。

​ 浅拷贝:引用有多个,地址相同。

//浅拷贝 
A a = new A();
A b = a;

//深拷贝
方法一 构造函数
方法二 重载clone()方法,必须实现Cloneable
方法三 Serializable序列化

6 sleep和wait区别(上海)

  • sleep方法:属于Thread类中的方法;会导致程序暂停执行指定的时间,让出cpu执行权给其他线程,但是他的监控状态依然保持着,当指定时间到了之后,又会自动恢复运行状态;在调用sleep方法的过程中,线程不会释放对象锁。(只会让出CPU,不会导致锁行为的改变)

  • wait方法:属于Object类中的方法;在调用wait方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify方法后本线程才进入对象锁定池准备。获取对象锁进入运行状态。(不仅让出CPU,还释放已经占有的同步资源锁)

总结:

​ sleep:thread类,有时间参数,不释放锁,释放线程执行权。时间到就醒。
​ wait:object类,释放锁,释放,必须通过notify或notifyAll唤醒。

7 自动拆装箱(北京)

基本数据类型:byte 、short、int、long、float、double、char、boolean。不具备对象的特征,不能调用方法。

**装箱:**将基本类型转换成包装类对象

**拆箱:**将包装类对象转换成基本类型的值

java为什么要引入自动装箱和拆箱的功能?主要是用于java集合中,List list=new ArrayList();

list集合如果要放整数的话,只能放对象,不能放基本类型,因此需要将整数自动装箱成对象。

**实现原理:javac编译器的语法糖,底层是通过Integer.valueOf()和Integer.intValue()**方法实现。

区别:

(1)Integer是int的包装类,int则是java的一种基本数据类型

(2)Integer变量必须实例化后才能使用,而int变量不需要

(3)Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值

(4)Integer的默认值是null,int的默认值是0

public static void main(String[] args) {
//在-128~127之间,Integer有缓存,所有地址相同,有Integer.valueof()方法和Integer.intValue()
//包装类型重写了hashCode和equals,只要比较equals,都相同。因为比较的是内容
        Integer integer = 127;

        Integer a1 = 127;
        Integer a2 = Integer.valueOf(127);
        Integer a3 = new Integer(127);
    	//true,缓存中有-128到127,调用Integer.valueOf(127)和自动装箱相同
        System.out.println(a1 == a2); //true
    
        System.out.println(a1 == a3); //false,因为a3,new对象了。所有地址不同

        Integer b1 = 128;
        Integer b2 = Integer.valueOf(128);
        Integer b3 = new Integer(128);
        System.out.println(b1 == b2); //false
        System.out.println(b1 == b3); //false
        
        System.out.println(b1.equals(b2));//true
        System.out.println(b1.equals(b3));//true
}

8 ==和equals区别

  • **==:**基本类型比较的是值,引用类型比较地址。

  • equals:object方法,比较的是内容。native关键字:调用系统本地资源的。

  • hashCode: 是native方法

    //equals比较方式:object先比较内容,然后比较类型,最后比较长度。源码如下:
    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
    }
    

9 String(北京)

**问题:**String能被继承吗?为什么用final修饰?

回答:不能被继承,因为String类有final修饰符,而final修饰的类是不能被继承的。String 类是最常用的类之一,为了效率,禁止被继承和重写。

为了安全。String 类中有native关键字修饰的调用系统级别的本地方法,调用了操作系统的 API,如果方法可以重写,可能被植入恶意代码,破坏程序。Java 的安全性也体现在这里。

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
// 常量池中的信息,都会被加载到运行时常量池中,这时 a b ab 都是常量池中的符号,还没有变为java字符串对象
// ldc #2 会把 a 符号变为 "a" 字符串对象
// ldc #3 会把 b 符号变为 "b" 字符串对象
// ldc #4 会把 ab 符号变为 "ab" 字符串对象

    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() 相当于 new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为"ab"

        //false,==比较的是地址。StringTable存储的是字符串对象的引用,字符串对象本身存在堆中
        System.out.println(s3 == s4);
        
        System.out.println(s3.equals(s4));//true

        //String s3 = "ab";和String s5 = "a" + "b"; 是相同的。
        // javac 在编译期间的优化,结果已经在编译期确定为"ab"
        System.out.println(s3 == s5);//true
}

10 String buffer和String builder区别

  • StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,

  • 只是StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。

  • 在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer则每次都需要判断锁,效率相对更低

11 final、finally、finalize

  • final:修饰符(关键字)有三种用法:修饰类、变量和方法。修饰类时,意味着它不能再派生出新的子类,即不能被继承,因此它和abstract是反义词。修饰变量时,该变量使用中不被改变,必须在声明时给定初值,在引用中只能读取不可修改,即为常量。修饰方法时,也同样只能使用,不能在子类中被重写。

  • finally:通常放在try…catch的后面构造最终执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要JVM不关闭都能执行,可以将释放外部资源的代码写在finally块中。

  • finalize:Object类中定义的方法,Java中允许使用finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写finalize() 方法可以整理系统资源或者执行其他清理工作。

12 集合区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qoq8pJHB-1676791663319)(img\javase-集合1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9TaAbEhn-1676791663320)(img\javase-集合2.png)]

13 Object中有哪些方法

  • protected Object clone()—>创建并返回此对象的一个副本。
  • boolean equals(Object obj)—>指示某个其他对象是否与此对象“相等”。
  • protected void finalize()—>当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  • Class<? extendsObject> getClass()—>返回一个对象的运行时类。
  • int hashCode()—>返回该对象的哈希码值。
  • void notify()—>唤醒在此对象监视器上等待的单个线程。
  • void notifyAll()—>唤醒在此对象监视器上等待的所有线程。
  • String toString()—>返回该对象的字符串表示。
  • void wait()—>导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
    • void wait(long timeout)—>导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll()方法,或者超过指定的时间量。
    • void wait(long timeout, int nanos)—>导致当前的线程等待,直到其他线程调用此对象的 notify()

14 ArrarList和LinkedList

javase-集合图

区别:

LinkedList
	1. 基于双向链表,无需连续内存
	2. 随机访问慢(要沿着链表遍历)
	3. 头尾插入删除性能高
	4. 占用内存多
			
ArrayList
	1. 基于数组,需要连续内存
	2. 随机访问快(指根据下标访问)
	3. 尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低
	4. 可以利用 cpu 缓存,局部性原理。		
    局部性原理:当cpu读取数据是,先将相邻的一些数组读取到cpu缓存中。cpu与cpu缓存交互。这种效率很快,比直接从数组中读取数据快几百倍。但是链表不行。

局部性原理javase-局部性原理

15 ArrayList原理

构造函数构造函数什么都不传,会使用长度为零的数组
ArrayList(int initialCapacity)构造函数会使用指定容量的数组
public ArrayList(Collection<? extends E> c)传入一个集合、会使用 c 的大小作为数组容量
add(Object o)首次扩容为 10,再次扩容为上次容量的 1.5 倍
addAll(Collection c)没有元素时,扩容为 Math.max(10, 实际元素个数)
有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)

其中第 4 点必须知道,其它几点视个人情况而定

  • ArrayList 的底层是动态数组实现的。

  • ArrayList 扩容的真正计算是在一个**grow()**里面,新数组大小是旧数组的1.5倍,如果扩容后的新数组大小还是小于最小容量,那新数组的大小就是最小容量的大小,后面会调用一个Arrays.copyof方法,这个方法是真正实现扩容的步骤

    //源码如下
    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
    

15 HashMap 和 HashTable

HashMapHashTable
安全性不安全安全,synchronized修饰得
null可以作为key?可以不可以
初始容量初始容量为16,负载因子0.75初始容量为11,负载因子0.75
扩容大小旧容量*2旧容量*2+1,调用rehash方法
hash计算对key的hashcode进行了二次hashhashcode对table数组取模

16 HashMap原理

  • 基本数据结构:

    • jdk1.7 数组 + 链表,1.7采用头插法扩容死链(1.7 会存在)。

    • jdk1.8 数组node<K,V>数组 + (链表 | 红黑树),1.8 是尾插法

    • HashMap初始容量16,扩容阈值0.75,每次扩容2倍。

      扩容(加载)因子为何默认是 0.75f1、在空间占用与查询时间之间取得较好的权衡
      	2、大于这个值,空间节省了,但链表就会比较长影响性能
      	3、小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多
      
      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //数组初始化容量
      
      static final int MAXIMUM_CAPACITY = 1 << 30; //数组最大容量
      
      static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子
      
      static final int TREEIFY_THRESHOLD = 8; //链表是否要树化的条件
      
      static final int UNTREEIFY_THRESHOLD = 6; //红黑树转化为链表的条件
      
      //链表树化的条件,没有达到这个条件只会进行数组扩容
      static final int MIN_TREEIFY_CAPACITY = 64;
      
  • put方法流程

    //put方法,
    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
    }
    
    //putVal方法,hash就是计算的哈希值,key, value,
    //onlyIfAbsent为true表示不改变evict的值,flase代表创建。evict返回之前的值,否则返回null
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
        	//判断node节点是否存在,不存在直接resize()创建默认的扩容机制
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
        	//判断hash运算的数组位置是否为null,为null直接在数组下面新建链表,插入数据
        	//不为null,在链表下面一个个equals比较,不同则插入链表尾部,相同则覆盖
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
    		else {
            	.........
                //树化,退化,equals比较等等..
        	}
    }
    
    //hash运算,hashCode值 异或 hashCode值右移16位
    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

javase-put流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MVM5lHY4-1676791663321)(C:\Users\cqk\Desktop\面试总结\img\javase-put流程2.jpg)]

  • 扩容。
  • hash碰撞:形成链表的条件
  • 什么时候变成树:数组长度大于64并且链表长度大于8(为9时转化)
  • 转链表:树的高度小于6 转链表。
  • 为什么要转树? 当链表长度大于8 查询效率低于红黑树了。

17 Iterator

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

ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,一旦修改会报错

CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

18 常见异常

  • java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
  • java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
  • java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
  • java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
  • java.lang.IllegalArgumentException 方法传递参数错误。
  • java.lang.ClassCastException 数据类型转换异常。

19 对反射的理解

  • 反射机制

    • 所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底的了解自身的情况为下一步的动作做准备。
    • Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method
    • 其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组 成部分。
  • Java反射的作用

    • 在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。
  • Java 反射机制提供功能

    • 在运行时判断任意一个对象所属的类。
    • 在运行时构造任意一个类的对象。
    • 在运行时判断任意一个类所具有的成员变量和方法。
    • 在运行时调用任意一个对象的方法
  • 反射的三种方式

    • Class c = Class.forName(“完整类名必须带有包名”);
    • Class c = 对象.getClass();
    • Class c = 任何类型.class();
  • 反射理解

    • java是个大美女,但大美女有很多事情是规定不让你做的.反射就是把枪,有枪在手,你想让大美女做什么事就做什么事,脱光了都没问题.
    • 正常的解释:Java的反射是指程序在运行期可以拿到一个对象的所有信息。是一种动态获取对象信息以及动态调用对象的方法。最常见的场景就是在动态代理。而动态代理应用最广的地方就是各种框架,比如:Spring。

20 序列化

  • 序列化:就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。

  • 序 列 化 的 实 现 : 将 需 要 被 序 列 化 的 类 实 现 Serializable 接 口 , 该 接 口 没 有 需 要 实 现 的 方 法 , implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。

21 ConcurrentHashMap

Hashtable 对比 ConcurrentHashMap

  • Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
  • Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
  • ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突

ConcurrentHashMap 1.7

  • 数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
  • 并发度:Segment 数组大小即并发度,决定了同一时刻最多能有多少个线程并发访问。Segment 数组不能扩容,意味着并发度在 ConcurrentHashMap 创建时就固定了
  • 索引计算
    • 假设大数组长度是 2 m 2^m 2m,key 在大数组内的索引是 key 的二次 hash 值的高 m 位
    • 假设小数组长度是 2 n 2^n 2n,key 在小数组内的索引是 key 的二次 hash 值的低 n 位
  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子时会触发扩容,每次扩容翻倍
  • Segment[0] 原型:首次创建其它小数组时,会以此原型为依据,数组长度,扩容因子都会以原型为准

ConcurrentHashMap 1.8

  • 数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能

  • 并发度:Node 数组有多大,并发度就有多大,与 1.7 不同,Node 数组可以扩容

  • 扩容条件:Node 数组满 3/4 时就会扩容

  • 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode

  • 扩容时并发 get

    • 根据是否为 ForwardingNode 来决定是在新数组查找还是在旧数组查找,不会阻塞
    • 如果链表长度超过 1,则需要对节点进行复制(创建新节点),怕的是节点迁移后 next 指针改变
    • 如果链表最后几个元素扩容后索引不变,则节点无需复制
  • 扩容时并发 put

    • 如果 put 的线程与扩容线程操作的链表是同一个,put 线程会阻塞
    • 如果 put 的线程操作的链表还未迁移完成,即头节点不是 ForwardingNode,则可以并发执行
    • 如果 put 的线程操作的链表已经迁移完成,即头结点是 ForwardingNode,则可以协助扩容
  • 与 1.7 相比是懒惰初始化

  • capacity 代表预估的元素个数,capacity / factory 来计算出初始数组大小,需要贴近 2 n 2^n 2n

  • loadFactor 只在计算初始数组大小时被使用,之后扩容固定为 3/4

  • 超过树化阈值时的扩容问题,如果容量已经是 64,直接树化,否则在原来容量基础上做 3 轮扩容

  • 类似HashMap,初始容量16,主要利用cas实现

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Ct6EGgj-1676791663321)(img\javase-concurrentHahsMap.png)]

final V putVal(K key, V value, boolean onlyIfAbsent) {
    //判断key或者value是否存在,不存在抛出null
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode()); //计算hash值
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { //死循环
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)  //判断tab是否存在,不存在,则创建新的数组
            tab = initTable();
        //判断hash槽是否为空,为空表示没有hash冲突,通过cas插入元素
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;//如果成功,跳出循环,不成功说明并发问题,进入下一次循环
        }
        else if ((fh = f.hash) == MOVED) //判断是否进行数据迁移
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null; //拿到表头元的锁进行操作
            synchronized (f) {
                if (tabAt(tab, i) == f) {  //判断当前元素是否改变
                    if (fh >= 0) { //说明当前槽为链表,遍历链表
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //判断值是否重复,如果重复,用新值替换旧值
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                              value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

22 死锁问题

  • 死锁介绍:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

  • 命令:可以通过jstack命令来进行查看,jstack命令中会显示发生了死锁的线程

  • 数据库:或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况

    • 查询是否锁表:show OPEN TABLES wtere In_use > 0;
    • 查询进程:show processlist;
    • 查看正在锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
    • 查看等待锁的事务:SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
  • 死锁的四个必要条件,缺一个会打破死锁

    • 互斥条件:一个资源每次只能被一个进程使用;
    • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
    • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
    • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;
  • 死锁避免条件,前三个是锁的必要条件,只能打破最后一个

    • 要注意加锁顺序,保证每个线程按同样的顺序进行加锁
    • 要注意加锁时限,可以针对锁设置一个超时时间
    • 要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
static MyTest myTest1 = new MyTest();
static MyTest myTest2 = new MyTest();
public static void main(String[] args) throws InterruptedException{
    new Thread(() -> {
        synchronized (myTest1){
            try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
            synchronized (myTest2){
                System.out.println("A");
            }
        }
    }).start();


    new Thread(() -> {
        synchronized (myTest2){
            synchronized (myTest1){
                System.out.println("B");
            }
        }
    }).start();
}

23 集合排序

1、实现implements Comparable、重写compareTo(Vip v)方法比较排序规则

2、implements Comparator、重写compare(wuGui o1, wuGui o2)比较规则

Comparable和comparator怎么选择呢?
	当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
	如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用comparator接口。

线程池

详见BAT大佬笔记:https://mp.weixin.qq.com/s/v3eClGAgC7iDW09MoDKiEA

1 线程状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JaPhDZ9d-1676791663322)(img\多线程-运行状态.png)]

  • 运行态:分到 cpu 时间,能真正执行线程内代码的
  • 就绪态:有资格分到 cpu 时间,但还未轮到它的
  • 阻塞态:没资格分到 cpu 时间的
    • 涵盖了 java 状态中提到的阻塞等待有时限等待
    • 多出了阻塞 I/O,指线程在调用阻塞 I/O 时,实际活由 I/O 设备完成,此时线程无事可做,只能干等
  • 新建与终结态:与 java 中同名状态类似,不再啰嗦

2 线程池和拒绝策略

多线程-七大参数

七大参数

  1. corePoolSize 核心线程数目:池中会保留的最多线程数

  2. maximumPoolSize 最大线程数目:核心线程+救急线程的最大数目

  3. keepAliveTime 生存时间:救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  4. unit 时间单位:救急线程的生存时间单位,如秒、毫秒等

  5. workQueue队列:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  6. threadFactory 线程工厂:可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

  7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略

    1. 抛异常 java.util.concurrent.ThreadPoolExecutor.AbortPolicy

    2. 由调用者执行任务 java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy

    3. 丢弃任务 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy

    4. 丢弃最早排队任务 java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy

线程池的拒绝策略有哪些

  1. AbortPolicy:丢弃任务抛出 RejectedExecutionException 异常打断当前执行流程(默认)

    场景: ExecutorService 接口的系列 ThreadPoolExecutor 因为都没有显示的设置拒绝策略,所以默认的都是这个。 ExecutorService 中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。 当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程

  2. CallerRunsPolicy:只要线程池没有关闭,就由提交任务的当前线程处理

    场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。

  3. DiscardPolicy:直接静悄悄的丢弃这个任务,不触发任何动作

    使用场景:如果提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了

  4. DiscardOldestPolicy:丢弃队列最前面的任务,重新提交被拒绝的任务

    场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,我能想到的场景就是,发布消息,和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较。

注意: 以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

3 为什么使用线程池?

如果我们在方法中直接new一个线程来处理,当这个方法被调用频繁时就会创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性,一不小心把系统搞崩了,就可以完蛋了。

特点: 线程复用; 控制最大并发数; 管理线程。

线程池优势:

  • 重用存在的线程,减少线程创建和销毁的开销,提高性能(陈低资源消耗)
  • 提高响应速度,当任务达到时,任务可以不需要等待线程创建就能立即执行(提高响应速度)
  • 提高线程的可管理性,统一对线程进行分配、调优和监控(增强可管理性)

4 线程池工作流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vfXxE8kh-1676791663323)(img\多线程-线程池运行步骤.png)]

  • 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。 不过,就算队列里面有任务,线程池也不会马上执行它们。
  • 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
    • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
  • 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  • 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小

队列已满并且正在运行的线程数量大于或等于 maximumPoolSize

5 线程池配置个数

  • IO 密集型配置线程数经验值是: 2N,其中 N 代表 CPU 核数。
  • CPU 密集型配置线程数经验值是: N + 1,其中 N 代表 CPU 核数。
  • 线程数的设置需要考虑三方面的因素,服务器的配置、服务器资源的预算和任务自身的特性。具体来说就是服务器有多少个 CPU,多少内存, IO 支持的最大 QPS 是多少,任务主要执行的是计算、 IO 还是一些混合操作,任务中是否包含数据库连接等的稀缺资源。线程池的线程数设置主要取决于这些因素。
  • 如果一个请求中,计算机操作需要 5ms,DB 需要 100ms,对于一个 8 核的 cpu 来说需要设置多少个线程
    • 可以拆分为两个线程池,cpu 密集型的是 n+1 个线程,IO 密集型的就是 n*2
    • 如果不可以拆分,就是 5/(5+100),cpu 的利用率是在 21%,1/(5/(100+5)) *n 就是168,也就是需要的线程数
  • 那么如何获取 n 的值?(也就是 cpu 核心数目)
    int n = Runtime.getRuntime().availableProcessors();

6 线程池内抛出异常,线程池会怎么办?

当线程池中线程执行任务的时候,任务出现未被捕获的异常的情况下,线程池会将允许该任务的线程从池中移除并销毁,且同时会创建一个新的线程加入到线程池中可以通过 ThreadFactory 自定义线程并捕获线程内抛出的异常,也就是说甭管我们是否去捕获和处理线程池中工作线程抛出的异常,这个线程都会从线程池中被移除

7 submit和execute区别

  • 参数有区别,都可以是 Runnable, submit 也可以是 Callable
  • submit 有返回值,而 execute 没有
  • submit 方便 Exception 处理

8 线程池目前有5个状态

  • RUNNING:接受新任务并处理排队的任务。
  • SHUTDOWN:不接受新任务,但处理排队的任务。
  • STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务。
  • TIDYING:所有任务都已终止,workerCount 为零,线程转换到 TIDYING 状态将运行 terminated() 钩子方法。
  • TERMINATED:terminated() 已完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7H4oTfM-1676791663324)(img\多线程-线程池状态.png)]

9 线程池使用场景

线程池在实际项目中的使用场景?项目中多个业务需要用到线程池是为每个线程池都定义一个还是定义一个公共
的线程池呢?

  • 线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。
  • 一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。

10 start和run的区别

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

总结:简单来说,start开辟了一个新的线程,可以异步执行。只调用run(),不调用start(),只能从上到下执行。

MySQL

1 SQL执行顺序

(1)from 子句组装来自不同数据源的数据;
(2)where 子句基于指定的条件对记录行进行筛选;
(3)group by 子句将数据划分为多个分组;
(4)使用聚集函数进行计算;
(5)使用 having 子句筛选分组;
(6)计算所有的表达式; 
(7)select 的字段;
(8)使用order by 对结果集进行排序。

2 索引

索引分类

索引分类:主键索引,唯一索引

在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:聚集索引,二级索引

索引结构

索引结构:B+tree、B-tree、Hash

B+tree:所有的数据都在叶子结点、上面的是索引,而且有向链表

MySQL的B+tree:优化了,所有的数据都在叶子结点、上面的是索引,而且有向链表

***Hash:***类似hashmap的put的方法(数组+链表)

**B-Tree (多路平衡查找树) **

以一棵最大度数(max-degree,指一个节点的子节点个数)为5(5阶)的 b-tree 为例(每个节点最多存储4个key,5个指针)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECU5n8Qr-1676791663325)(https://dhc.pythonanywhere.com/media/editor/B-Tree结构_20220316163813441163.png “B-Tree结构”)]

B+Tree

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dt8gnQWi-1676791663325)(https://dhc.pythonanywhere.com/media/editor/B+Tree结构图_20220316170700591277.png “B+Tree结构图”)]

与 B-Tree 的区别:

  • 所有的数据都会出现在叶子节点
  • 叶子节点形成一个单向链表

MySQL 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,提高区间访问的性能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HKGDPjA8-1676791663326)(https://dhc.pythonanywhere.com/media/editor/结构图_20220316171730865611.png “MySQL B+Tree 结构图”)]

Hash

哈希索引就是采用一定的hash算法,将键值换算成新的hash值,映射到对应的槽位上,然后存储在hash表中。
如果两个(或多个)键值,映射到一个相同的槽位上,他们就产生了hash冲突(也称为hash碰撞),可以通过链表来解决。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ChsDXkW-1676791663326)(https://dhc.pythonanywhere.com/media/editor/Hash索引原理图_20220317143226150679.png “Hash索引原理图”)]特点:

  • Hash索引只能用于对等比较(=、in),不支持范围查询(betwwn、>、<、…)
  • 无法利用索引完成排序操作
  • 查询效率高,通常只需要一次检索就可以了,效率通常要高于 B+Tree 索引

存储引擎支持:

  • Memory
  • InnoDB: 具有自适应hash功能,hash索引是存储引擎根据 B+Tree 索引在指定条件下自动构建的

面试题:为什么 InnoDB 存储引擎选择使用 B+Tree 索引结构?

  • 相对于二叉树,层级更少,搜索效率高
  • 对于 B-Tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针也跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低
  • 相对于 Hash 索引,B+Tree 支持范围匹配及排序操作

聚集索引

在 InnoDB 存储引擎中,根据索引的存储形式,又可以分为以下两种:

分类含义特点
聚集索引(Clustered Index)将数据存储与索引放一块,索引结构的叶子节点保存了行数据必须有,而且只有一个
二级索引(Secondary Index)将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一(UNIQUE)索引作为聚集索引
  • 如果表没有主键或没有合适的唯一索引,则 InnoDB 会自动生成一个 rowid 作为隐藏的聚集索引

回表查询:假设执行select * from where name = ‘Jack’,先查二级索引,查到Jack得到id,因为二级索引叶子节点没有存放数据,所以通过得到id回表查询得到所有信息

https://dhc.pythonanywhere.com/media/editor/原理图_20220318194454880073.png “大致原理”

性能分析

  • 查看执行频次

    查看当前数据库的 INSERT, UPDATE, DELETE, SELECT 访问频次:

  • 慢查询日志

    慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志。MySQL的慢查询日志默认没有开启,需要在MySQL的配置文件(/etc/my.cnf)中配置

  • explain

    直接在select语句之前加上关键字 explain / desc
    语法:EXPLAIN SELECT 字段列表 FROM 表名 HWERE 条件;
    例子:explain select * from pd_auth_menu;
    

索引失效情况

  1. 在索引列上进行运算操作,索引将失效。

    ​ 如:explain select * from tb_user where substring(phone, 10, 2) = '15';

  2. 字符串类型字段使用时,不加引号,索引将失效。

    ​ 如:explain select * from tb_user where phone = 17799990015;,此处phone的值没有加引号

  3. 模糊查询中,如果仅仅是尾部模糊匹配,索引不会是失效;如果是头部模糊匹配,索引失效

    ​ 如:explain select * from tb_user where profession like '%工程';,前后都有 % 也会失效。

  4. 用 or 分割开的条件,如果 or 其中一个条件的列没有索引,那么涉及的索引都不会被用到。(若是联合索引,用or也会失效)

  5. 数据分布影响

    ​ 如果 MySQL 认为(评估)使用索引比全表更慢,则不使用索引。

3 事务和原理

事务(ACID)

  1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位
  2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

事务的并发问题

  1. 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  2. 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致
  3. 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

**小结:**不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

MySQL事务隔离级别:

事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)
读提交(read-committed)
可重复读(repeatable-read)
串行化(serializable)

事务原理

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时败。具有ACID四大特征。

原子性,一致性,持久性这三大特性由 redo log重做日志 和 undo log回滚日志 日志来保证的。
隔离性 是由锁机制和MVCC保证的。

4 MyISAM和InnoDB

主要是三种区别:事务、外键、行锁

特点InnoDBMyISAMMemory
存储限制64TB
事务安全支持--
锁机制行锁表锁表锁
B+tree索引支持支持支持
Hash索引--支持
全文索引支持(5.6版本之后)支持-
空间使用N/A
内存使用中等
批量插入速度
支持外键支持--

MyISAM分为内存结构和磁盘结构

内存结构:缓冲池(buffer pool)、改变缓冲池(change pool)、缓冲日志(log buffer)

  • 缓冲池(buffer pool):free page(没有使用的页)、clean page(使用过,没修改过的),dirty page(脏页,和磁盘不同,需要刷新到磁盘种)。缓冲池就是把脏页数据刷到磁盘中。
  • **改变缓冲池(change pool):**主要是二级索引(不规律)修改,删除的情况下,直接操作缓冲池,会出现磁盘大量IO,所以先写到改变缓冲池,由改变缓冲池刷到缓冲池,再刷到磁盘。
  • **缓冲日志(log buffer):**存储redo log和undo log,然后刷到磁盘中

磁盘结构:系统表空间、通用表空间、双写缓冲池,撤销表空间、临时表空间、双写缓冲区、redo long

**后台进程:**主线程(调配资源,刷新磁盘、合并脏页…总之全做),IO线程(读写,将日志缓冲区刷到磁盘),Purge log(对于提交的undo log,进行回收)、page cleaner thread(协作主线程刷新脏页到磁盘)

InnoDB的整个体系结构为:

当业务操作的时候直接操作的是内存缓冲区,如果缓冲区当中没有数据,则会从磁盘中加载到缓冲区,增删改查都是在缓冲区的,后台线程以一定的速率刷新到磁盘。

MVCC多版本并发控制

全称Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的 三个隐式字段、undo log日志、readView。

  • readView是确定返回那个版本链

  • undo log日志会记录原来的版本的数据,因为是通过undo log 日志进行回滚的

  • 三个隐式字段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xEwWGqJ-1676791663327)(D:\资料\2-后端笔记\MySQL笔记\images\MVCC.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sikeEPyI-1676791663328)(D:\资料\2-后端笔记\MySQL笔记\images\总结.png)]

5 锁

NOTE : 针对事物才有加锁的意义。

分类:MySQL中的锁,按照锁的粒度分,分为以下三类:

  • 全局锁:锁定数据库中的所有表。

  • 表级锁:每次操作锁住整张表。

  • 行级锁:每次操作锁住对应的行数据。

全局锁

全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将被阻塞。

​ 其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。

表级锁

表级锁,每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。

**表级锁:**主要分为以下三类:表锁、元数据锁、意向锁

  1. 表锁:对于表锁,分为两类:表共享读锁、表独占写锁

    • 表共享读锁(read lock)所有的事物都只能读(当前加锁的客户端也只能读,不能写),不能写

    • 表独占写锁(write lock),对当前加锁的客户端,可读可写,对于其他的客户端,不可读也不可写。

    • 结论:读锁不会阻塞所有客户端的读,但是会阻塞写。写锁会阻塞其他客户端的读和写,不会阻塞自己。

  2. **元数据锁:**加锁过程是系统自动控制,无需我们调用

  3. **意向锁:**意向共享锁(IS),意向排他锁(IX)

    • 为了避免DML在执行时,加的行锁与表锁的冲突,在InnoDB中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。

    • 一个客户端对某一行加上了行锁,那么系统也会对其加上一个意向锁,当别的客户端来想要对其加上表锁时,便会检查意向锁是否兼容,若是不兼容,便会阻塞直到意向锁释放。

行级锁

  • 行锁(Record Lock):锁定单个行记录的锁,防止其他事务对此行进行update和delete。在RC(read commit )、RR(repeat read)隔离级别下都支持。

    1. 针对唯一索引进行检索时,对已存在的记录进行等值匹配时,将会自动优化为行锁。
    2. InnoDB的行锁是针对于索引加的锁,不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,此时就会升级为表锁。
    
  • 间隙锁(GapLock):锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事务在这个间隙进行insert,产生幻读。在RR隔离级别下都支持。比如说 两个临近叶子节点为 15 23,那么间隙就是指 [15 , 23],锁的是这个间隙。

  • 临键锁(Next-Key Lock):行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap。在RR隔离级别下支持

    默认情况下,InnoDB在REPEATABLE READ事务隔离级别运行,InnoDB使用next-key 锁进行搜索和索引扫描,以防止幻读。
    
    1. 索引上的等值查询(唯一索引),给不存在的记录加锁时,优化为间隙锁。
    2. 索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。
    3. 索引上的范围查询(唯一索引)--会访问到不满足条件的第一个值为止。
    
    注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。
    

6 数据库设计规范

基础规范

  • 表存储引擎必须使用InnoDB,表字符集默认使用utf8,必要时候使用utf8mb4

    • (1)通用,无乱码风险,汉字3字节,英文1字节
      (2)utf8mb4是utf8的超集,有存储4字节例如表情符号时,使用它
      
  • 禁止使用存储过程,视图,触发器,Event

    • 对数据库性能影响较大,互联网业务,能让站点层和服务层干的事情,不要交到数据库层
      调试,排错,迁移都比较困难,扩展性较差
      
  • 禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统,数据库中存储路径

  • 禁止在线上环境做数据库压力测试

  • 测试,开发,线上数据库环境必须隔离

命名规范

  • 库名,表名,列名必须用小写,采用下划线分隔

    • 解读:abc,Abc,ABC都是给自己埋坑
  • 库名,表名,列名必须见名知义,长度不要超过32字符

    • 解读:tmp,wushan谁知道这些库是干嘛的
  • 备份

    • 库备份必须以bak为前缀,以日期为后缀

    • 从库必须以-s为后缀

    • 备库必须以-ss为后缀

表设计规范

  • 单实例表个数必须控制在2000个以内

  • 单表分表个数必须控制在1024个以内

  • 表必须有主键,推荐使用UNSIGNED整数为主键

    • 潜在坑:删除无主键的表,如果是row模式的主从架构,从库会挂住
  • 禁止使用外键,如果要保证完整性,应由应用程式实现

    • 解读:外键使得表之间相互耦合,影响update/delete等SQL性能,有可能造成死锁,高并发情况下容易成为数据库瓶颈
  • 建议将大字段,访问频度低的字段拆分到单独的表中存储,分离冷热数据

列设计规范

  • 根据业务区分使用tinyint/int/bigint,分别会占用1/4/8字节

  • 根据业务区分使用char/varchar

    • (1)字段长度固定,或者长度近似的业务场景,适合使用char,能够减少碎片,查询性能高

    • (2)字段长度相差较大,或者更新较少的业务场景,适合使用varchar,能够减少空间

  • 根据业务区分使用datetime/timestamp

    • 解读:前者占用5个字节,后者占用4个字节,存储年使用YEAR,存储日期使用DATE,存储时间使用datetime
  • 必须把字段定义为NOT NULL并设默认值

    • NULL的列使用索引,索引统计,值都更加复杂,MySQL更难优化

    • NULL需要更多的存储空间

    • NULL只能采用IS NULL或者IS NOT NULL,而在=/!=/in/not in时有大坑

  • 使用INT UNSIGNED存储IPv4,不要用char(15)

  • 使用varchar(20)存储手机号,不要使用整数

    • 牵扯到国家代号,可能出现+/-/()等字符,例如+86

    • 手机号不会用来做数学运算

    • varchar可以模糊查询,例如like ‘138%’

  • 使用TINYINT来代替ENUM

    • ENUM增加新值要进行DDL操作

索引规范

  • 唯一索引使用uniq_[字段名]来命名

  • 非唯一索引使用idx_[字段名]来命名

  • 单张表索引数量建议控制在5个以内

    • 互联网高并发业务,太多索引会影响写性能

    • 生成执行计划时,如果索引太多,会降低性能,并可能导致MySQL选择不到最优索引

    • 异常复杂的查询需求,可以选择ES等更为适合的方式存储

  • 组合索引字段数不建议超过5个

    • 解读:如果5个字段还不能极大缩小row范围,八成是设计有问题
  • 不建议在频繁更新的字段上建立索引

  • 非必要不要进行JOIN查询,如果要进行JOIN查询,被JOIN的字段必须类型相同,并建立索引

    • 解读:踩过因为JOIN字段类型不一致,而导致全表扫描的坑么?
  • 理解组合索引最左前缀原则,避免重复建设索引,如果建立了(a,b,c),相当于建立了(a), (a,b), (a,b,c)

SQL规范

  • *禁止使用select ,只获取必要字段

    • select *会增加cpu/io/内存/带宽的消耗

    • 指定字段能有效利用索引覆盖

    • 指定字段查询,在表结构变更时,能保证对应用程序无影响

  • insert必须指定字段,禁止使用insert into T values()

    • 解读:指定字段插入,在表结构变更时,能保证对应用程序无影响
  • 隐式类型转换会使索引失效,导致全表扫描

  • 禁止在where条件列使用函数或者表达式

    • 解读:导致不能命中索引,全表扫描
  • 禁止负向查询以及%开头的模糊查询

    • 解读:导致不能命中索引,全表扫描
  • 禁止大表JOIN和子查询

  • 同一个字段上的OR必须改写问IN,IN的值必须少于50个

  • 应用程序必须捕获SQL异常

    • 解读:方便定位线上问题

    • 说明:本规范适用于并发量大,数据量大的典型互联网业务,可直接参考。

场景问题,面对一条慢查询语句,你会怎么做

  • 首先看是否加了索引,如果加了索引,根据执行计划查看索引是否命中是否失效

  • 对于失效对比sql进行修改,让sql尽量走索引,比如上面的一些失效场景的排查修改

  • 如果走了索引还是很慢,判断是否数据达到了一定量级别,如果是,可以使用limit限制查询条数

  • 也可以进行分库分表,包括水平切分和垂直切分,垂直切分要注意id取值范围,水平拆分注意业务逻辑的修改

  • (面试官提示,最直接的方式,修改数据库配置, /etc/mysql/my.cnf文件

    query_cache_size– 指定等待运行的 MySQL 查询的缓存大小。建议从 10MB 左右的小值开始,然后增加到不超过 100-200MB。如果缓存查询过多,可能会遇到“等待缓存锁定”的级联查询。如果查询不断备份,则更好的过程是使用EXPLAIN评估每个查询并找到提高它们效率的方法。

    max_connection– 指允许进入数据库的连接数。如果您收到引用“连接太多”的错误,则增加此值可能会有所帮助。

    innodb_buffer_pool_size – 此设置将系统内存分配为数据库的数据缓存。如果您有大量数据,请增加此值。记下运行其他系统资源所需的 RAM。

    innodb_io_capacity – 此变量设置存储设备的输入/输出速率。这与存储驱动器的类型和速度直接相关。5400 rpm HDD 的容量将比高端 SSD 或 Intel Optane低得多。您可以调整此值以更好地匹配您的硬件。)

Linux

常用命令

序号命令命令解释
1top查看内存
2df -h查看磁盘存储情况 disk free
3iotop查看磁盘IO读写(yum install iotop安装)
4iotop -o直接查看比较高的磁盘读写程序
5netstat -tunlp | grep 端口号查看端口占用情况
6uptime查看报告系统运行时长及平均负载
7ps aux查看进程
8tail -n查看文件多少行
9tar -zxvf (-zcvf)zcvf压缩。zxvf解压
10netstat -anp | grep 端口号查看某个端口是否被占用

SSM

1 Spring

1.1 Spring设计模式(背)

  • 代理模式——spring 中两种代理方式,若目标对象实现了若干接口,spring 使用jdk 的java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类。

  • 单例模式——在 spring 的配置文件中设置 bean 默认为单例模式。

  • 模板方式模式——用来解决代码重复的问题。比如:RestTemplate、JmsTemplate、JpaTemplate

  • 工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例。

1.2 对 Spring 的理解?

Spring 是一个 IOC 和 AOP 容器框架。 Spring 容器的主要核心是:

  • 控制反转(IOC):传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。 而在spring 开发模式中, spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用 spring 提供的对象就可以了,这是控制反转的思想。
  • 依赖注入(DI) :spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
  • 面向切面编程(AOP):在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等。公用操作处理的过程就是面向切面编程的思想。 AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用 CGLIB 方式实现动态代理。

1.3 IOC详解

① IOC 中Bean注入方式
  • 通过XML配置文件进行属性注入:在XML配置文件中使用元素为bean的属性赋值。

  • 通过注解进行属性注入:使用Spring提供的注解,如@Value、@Autowired等,为bean的属性赋值。

  • 通过Java代码进行属性注入:使用Spring提供的API,如BeanWrapper、BeanFactory等,为bean的属性赋值。

② IOC 的容器构建流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MmKbnboT-1676791663329)(img\SSM-IOC的容器构建流程.jpg)]

1.4 AOP详解

AOP:全称 Aspect Oriented Programming,即: 面向切面编程。

  • AOP:能够将那些与业务无关,却为业务 模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少 系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
  • Spring AOP 是基于动态代理的,如果要代理的对象实现了某个接口,那么 SpringAOP 就会使 用 JDK 动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用 JDK 动态代理,转 而使用 CGlib 动态代理生成一个被代理对象的子类来作为代理。 当然也可以使用 AspectJ, Spring AOP 中已经集成了 AspectJ,AspectJ 应该算得上是 Java 生态系统中最完整的 AOP 框架了。使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样可以大大简化代码量。 我们需要增加新功能也方便,提高了系统的 扩展性。日志功能、事务管理和权限管理等场景都用到了 AOP。

AOP 主要用在哪些场景中

事务管理 比如项目中使用的事务注解@Transactional

  • 日志管理 创建日志切面
  • 用于全局异常处理拦截
  • 性能统计 将与业务无关的代码,使用 AOP 拦截他们。
  • 缓存使用 @Cacheable 和@CacheEvict
  • 安全检查
    • 在支付行业往往对安全性要求比较高,我们在保存/接收/发送数据前先要对数据进行验签/签名/加密等操作,都需要做特殊处理。
    • 比如一个手机号,我们可以通过一个"拦截器"对手机号,身份证号这种敏感
      信息做这种特殊处理;

1.5 Resource和Autowire

自动装配提供五种不同的模式供 Spring 容器用来自动装配 beans 之间的依赖注入:

  • **byName:**通过参数名自动装配, Spring 容器查找 beans 的属性,这些 beans 在XML 配置文件 中被设置为 byName。之后容器试图匹配、装配和该 bean 的属性具有相同名字的 bean。

  • byType:通过参数的数据类型自动自动装配, Spring 容器查找 beans 的属性,这些 beans 在 XML 配置文件中被设置为 byType。之后容器试图匹配和装配和该bean 的属性类型一样的 bean。 如果有多个 bean 符合条件,则抛出错误。

@Resource 和 @Autowire 的区别

  • @Resource 和 @Autowired 都可以用来装配 bean
  • @Autowired 默认按类型装配,默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false。@Autowire 用名称来注入使用@Qualifier
  • @Resource 如果指定了 name 或 type,则按指定的进行装配;如果都不指定,则优先按名称装配,当找不到与名称匹配的 bean 时才按照类型进行装配。

1.6 Spring bean作用域

Spring 框架支持以下五种 bean 的作用域:

  • singleton:唯一 bean 实例, Spring 中的 bean 默认都是单例的。
  • prototype:每次请求都会创建一个新的 bean 实例。
  • request:每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTPrequest 内有 效。
  • session:每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTPsession 内有 效。
  • global-session:全局 session 作用域,仅仅在基于 Portlet 的 Web 应用中才有意义, Spring5 中已经没有了 Portlet。。

1.7 Spring bean的生命周期

Bean的生命周期

  • 默认情况下,IOC容器中bean的生命周期分为五个阶段:

    • 调用构造器 或者是通过工厂的方式创建Bean对象

    • 给bean对象的属性注入值

    • 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.

    • 使用

    • IOC容器关闭时, 销毁Bean对象

  • 当加入了Bean的后置处理器后,IOC容器中bean的生命周期分为七个阶段:

    • 调用构造器 或者是通过工厂的方式创建Bean对象
    • 给bean对象的属性注入值
    • 执行Bean后置处理器中的 postProcessBeforeInitialization
    • 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的.
    • 执行Bean的后置处理器中 postProcessAfterInitialization
    • 使用
    • IOC容器关闭时, 销毁Bean对象

只需要回答出第一点即可,第二点也回答可适当 加分。

bean 的生命周期主要有以下几个阶段,深色底的5个是比较重要的阶段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CIiadWuU-1676791663329)(img\SSM-Spring bean的生命周期.jpg)]

1.8 Spring 的事务管理

声明式事务 和 编程式事务

  • 编程式事务:在代码中硬编码在代码中显式挪用 beginTransaction()、commit()、rollback()等事务治理相关的方法, 这就是编程式事务管理

  • 声明式事务:基于 XML 的声明式事务,基于注解的声明式事务

    • **XML声明式事务:**transactionManager,用来指定一个事务治理器, 并将具体事务相关的操作请托给它; 其他一个是 Properties 类型的transactionAttributes 属性,该属性的每一个键值对中,键指定的是方法名,方法名可以行使通配符, 而值就是表现呼应方法的所运用的事务属性

      <bean id="transactionManager" class="...DataSourceTransactionManager">
              <property name="dataSource" ref="myDataSources" />
      </bean>
      
      <tx:advice id="myAdvice" transaction-manager="transactionManager">
      	<tx:attributes>
      		<tx:method name="buy" propagation="REQUIRED" isolation="DEFAULT" rollback-for="java.lang.NullPointerException"/>
              </tx:attributes>
      </tx:advice>
      
    • 注解声明式事务:@Transactional 可以浸染于接口、接口方法、类和类方法上。算作用于类上时,该类的一切public 方法将都具有该类型的事务属性。

      //若在a()方法添加注解,在a()中调用了b()方法,b()方法若没事务,会创建新的事务,与a()都有事务
      public class Test(){
          @Transactional
          public static void a(){
              b();
          }
          public static void b(){
              
          }
      }
      
    • 声明式事务好处:用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可,这样维护起来极其方便。

1.9 Spring 事务传播7行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。

例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。 在 TransactionDefinition定义中包括了如下几个表示传播行为的常量:

required(必须的),supports(支持),mandatory(弹性)

  • 支持当前事务的情况

    • TransactionDefinition.PROPAGATION_REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务;
    • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
    • TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  • 不支持当前事务的情况

    • TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起;
    • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  • 其他情况

    • TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

1.10 Spring事务隔离级别

Spring 的事务隔离级别底层其实是基于数据库的,Spring 并没有自己的一套隔离级别。

TransactionDefinition.ISOLATION_DEFAULT,TransactionDefinition.ISOLATION_READ_UNCOMMITTED…

  • DEFAULT:使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ(可重复读) 隔离级别, Oracle 默认采用的 READ_COMMITTED(读已提交) 隔离级别;

  • READ_UNCOMMITTED:读未提交,最低的隔离级别,会读取到其他事务还未提交的内容,存在脏读。

  • READ_COMMITTED:读已提交,读取到的内容都是已经提交的,可以解决脏读,但是存在不可重复读。

  • REPEATABLE_READ:可重复读,在一个事务中多次读取时看到相同的内容,可以解决不可重复读,但是存在幻读。

  • SERIALIZABLE:串行化,最高的隔离级别,对于同一行记录,写会加写锁,读会加读锁。在这种情况下,只有读读能并发执行,其他并行的读写、写读、写写操作都是冲突的,需要串行执行。可以防止脏读、不可重复度、幻读,没有并发事务问题。

1.11 Spring 事务实现原理

Spring 事务的底层实现主要使用的技术:AOP(动态代理) + ThreadLocal + try/catch。

  • 动态代理:基本所有要进行逻辑增强的地方都会用到动态代理,AOP 底层也是通过动态代理实现。

  • ThreadLocal:主要用于线程间的资源隔离,以此实现不同线程可以使用不同的数据源、隔离级别等等。

  • try/catch:最终是执行 commit 还是 rollback,是根据业务逻辑处理是否抛出异常来决定。

Spring 事务的核心逻辑伪代码如下:

public void invokeWithinTransaction() {
    // 1.事务资源准备
    try {
        // 2.业务逻辑处理,也就是调用被代理的方法
    } catch (Exception e) {
        // 3.出现异常,进行回滚并将异常抛出
    } finally {
        // 现场还原:还原旧的事务信息
    }
    // 4.正常执行,进行事务的提交
    // 返回业务逻辑处理结果
}
SSM-Spring 事务的实现原理

1.12 Spring 三级缓存

Spring 的三级缓存其实就是解决循环依赖时所用到的三个缓存。

  • singletonObjects:正常情况下的 bean 被创建完毕后会被放到该缓存,key:beanName,value:bean 实例。

  • singletonFactories:上面说的提前曝光的 ObjectFactory 就会被放到该缓存中,key:beanName,value:ObjectFactory。

  • earlySingletonObjects:该缓存用于存放 ObjectFactory 返回的 bean,也就是说对于一个 bean,ObjectFactory 只会被用一次,之后就通过
    earlySingletonObjects 来获取,key:beanName,早期 bean 实例。

1.13 Spring循环依赖

Spring 是通过提前暴露 bean 的引用来解决的,具体如下。

  • Spring 首先使用构造函数创建一个 “不完整” 的 bean 实例(之所以说不完整,是因为此时该 bean 实例还未初始化),并且提前曝光该 bean 实例的 ObjectFactory(提前曝光就是将 ObjectFactory 放到 singletonFactories 缓存)。
  • 通过 ObjectFactory 我们可以拿到该 bean 实例的引用,如果出现循环引用,我们可以通过缓存中的ObjectFactory 来拿到 bean 实例,从而避免出现循环引用导致的死循环。

举个例子:A 依赖了 B,B 也依赖了 A,那么依赖注入过程如下。

  • 检查 A 是否在缓存中,发现不存在,进行实例化
  • 通过构造函数创建 bean A,并通过 ObjectFactory 提前曝光 bean A
  • A 走到属性填充阶段,发现依赖了 B,所以开始实例化 B。
  • 首先检查 B 是否在缓存中,发现不存在,进行实例化
  • 通过构造函数创建 bean B,并通过 ObjectFactory 曝光创建的 bean B
  • B 走到属性填充阶段,发现依赖了 A,所以开始实例化 A。
  • 检查 A 是否在缓存中,发现存在,拿到 A 对应的 ObjectFactory 来获得 bean A,并返回。
  • B 继续接下来的流程,直至创建完毕,然后返回 A 的创建流程,A 同样继续接下来的流程,直至创建完毕。

这边通过缓存中的 ObjectFactory 拿到的 bean 实例虽然拿到的是 “不完整” 的 bean 实例,但是由于是单例,所以后续初始化完成后,该 bean 实例的引用地址并不会变,所以最终我们看到的还是完整 bean 实例。

2 SpringMVC

2.1 SpringMVC工作流程

1. 用户发送请求至前端控制器DispatcherServlet 
2. DispatcherServlet收到请求调用HandlerMapping处理器映射器。 
3. 处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回DispatcherServlet。 
4. DispatcherServlet调用HandlerAdapter处理器适配器 
5. HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。 
6. Controller执行完成返回ModelAndView 
7. HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet 
8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器 
9. ViewReslover解析后返回具体View 
10. DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。 
11. DispatcherServlet响应用户

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qBghIdRJ-1676791663330)(img\SSM-SpringMVC工作流程.png)]

2.2 常用的注解

注解解析
@Component使用在类上实例化bean,id为类名首字母小写
@Controller使用在web层类上实例化bean,id为类名首字母小写
@Service使用在Service层类上实例化bean,id为类名首字母小写
@Repository使用在Dao层类上实例化bean,id为类名首字母小写
@Autowired(required)根据类型自动注入,spring提供。可以用在字段,setter方法,构造器方法
默认情况下按照类型进行匹配,并且默认情况下要求依赖的对象必须存在(required=true),如果允许null值,可以设置required=false
说明@Autowired无法人为去干预注入规则的,然后当容器中同一类型的bean存在多个的话,@Autowired该如何去选择呢,这时就需要@Qualifier注解来帮忙了
@Qualifier不能单独使用,结合Autowired使用,并指定名称进行依赖注入
@Resource(name,type)1、根据名称id自动注入。可以用在字段,setter方法上。
2、若没有使用属性,则通过反射机制,默认按照byName方式进行装配,如果没有匹配,则再类型进行装配。
3、若只使用了name属性,则使用byName的自动注入策略,从上下文中查找名称id,如果匹配不到则抛出异常
4、若只使用了type属性,则使用byType自动注入策略,从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个都会抛出异常.
5、若两个属性都使用了,则需要找到唯一匹配的bean进行装配,找不到则抛出异常
@Value给类注入普通属性

3 Mybatis

3.1 #{}和${}的区别

  • #使用 ?在sql语句中做占位的, 使用PreparedStatement执行sql,效率高
  • #能够避免sql注入,更安全。
  • $不使用占位符,是字符串连接方式,使用Statement对象执行sql,效率低
  • $有sql注入的风险,缺乏安全性。
  • $:可以替换表名或者列名
  • #{}的执行结果,#属于字符串会加上单引号 ‘asc’ 的执行结果, {}的执行结果, 的执行结果,属于语句拼接,不会加上单引号 asc

3.2 Mybatis缓存

一级缓存: 一级缓存是SqlSession级别的缓存,默认开启。

第一次DQL和第二次DQL之间你做了以下两件事中的任意一件,都会让一级缓存清空:
	- 执行了sqlSession的clearCache()方法,这是手动清空缓存。
	- 执行了INSERTDELETEUPDATE语句。不管你是操作哪张表的,都会清空一级缓存。

二级缓存: 二级缓存是NameSpace级别(Mapper,SqlSessionFactory)的缓存,多个SqlSession可以共享,使用时需要进行配置开启。

使用二级缓存需要具备以下条件:
	-全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是true.
	-需要使用二级缓存的SqlMapper.xml文件中添加配置:<cache />
	-使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
	-SalSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
			
二级缓存失效:只要两次查询之间出现了增删改操作,二级缓存就会失效。【一级缓存也会失效】
		
二级缓存相关配置:
    ① eviction:指定从缓存中移除某个对象的淘汰算法默认的清除策略是 LRULRU – 最近最少使用:移除最长时间不被使用的对象。
        □ FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
        □ SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
        □ WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
    ② flushInterval(刷新间隔):属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
    ③ size(引用数目):属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
    ④ readOnly(只读):默认值是 false
    true:多条相同的sql语句执行之后返回的对象是共享的同一个,性能好,但是多线程并发可能会存在安全问题。
    false:多条相同的sql语句执行之后返回的对象是副本,调用了clone方法,性能一般,但安全

    提示 :二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

3.3 动态SQL

  • : 进行条件的判断

  • :在判断后的SQL语句前面添加WHERE关键字,并处理SQL语句开始位置的and或者or的问题

  • :可以在SQL语句前后进行添加指定字符 或者去掉指定字符.

  • : 主要用于修改操作时出现的逗号问题

  • :类似于java中的switch语句。在所有的条件中选择其一

  • :迭代操作

  • sql标签和include标签:代码复用

3.4 MyBatis获取自动生成的主键值?

在标签中使用 useGeneratedKeys 和 keyProperty 两个属性来获取自动生成的主键值。

<insert id=”insertname” useGeneratedkeys=true” keyProperty=”id”>  
    insert into names (name) values (#{name})  
</insert> 

3.5 批量操作

批量添加

int insertBatch(@Param("cars") List<Car> cars);

<insert id="insertBatch">
  insert into t_car values
  <foreach collection="cars" item="car" separator=",">
    (null,#{car.carNum},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType})
  </foreach>
</insert>

3.6 resultMap和resultType

  • resultType是返回的结果类型,可以是基本类型、自定义的java bean。如果是自定义的Java bean,要求取出的数据库的数据的字段名必须和bean的字段名一致,才能进行映射;如果所有表字段和bean的字段都不一致,则获得的bean实例为空。

  • resultMap是最复杂的元素,可以配置映射规则、级联、typeHandler等,与ResultType不能同时存在。如果表字段使用了下划线命名,而Bean用驼峰命名,可以使用resultMap进行字段的映射。

3.7 执行原理流程

在这里插入图片描述
  • 读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
  • 加载映射文件:映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
  • 构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
  • 创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法,是一个既可以发送sql执行并返回结果的,也可以获取mapper的接口。
  • Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
  • MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
  • 输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
  • 输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。

4 JavaWeb

Http常见状态码

200 OK //客户端请求成功

301 Moved Permanently(永久移除),请求的 URL 已移走。Response 中应该包含一个 Location URL, 说明资源现在所处的位置

302 found 重定向

400 Bad Request //客户端请求有语法错误,不能被服务器所理解

401 Unauthorized //请求未经授权,这个状态代码必须和 WWW-Authenticate 报头域一起使用

403 Forbidden //服务器收到请求,但是拒绝提供服务

404 Not Found //请求资源不存在,eg:输入了错误的 URL

500 Internal Server Error //服务器发生不可预期的错误

503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

get和Post

区别getpost
GET 请求的数据会附在URL 之后(就是把数据放置在 HTTP 协议头中),以?分割URL 和传输数据,参数之间以&相连,如:login.action?name=zhagnsan&password=123456。POST 把提交的数据则放置在是 HTTP 包的包体中。
GET 方式提交的数据最多只能是 1024 字节,get请求无法发送大数据量。post请求可以发送大数据量,理论上没有长度限制。

Cookie和Session

Cookie 是 web 服务器发送给浏览器的一块信息,浏览器会在本地一个文件中给每个 web 服务器存储 cookie。以后浏览器再给特定的 web 服务器发送请求时,同时会发送所有为该服务器存储的 cookie。

Session 是存储在 web 服务器端的一块信息。session 对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。

Cookie 和session 的不同点:

(1)无论客户端做怎样的设置,session 都能够正常工作。当客户端禁用 cookie 时将无法使用 cookie。

(2)在存储的数据量方面:session 能够存储任意的java 对象,cookie 只能存储 String 类型的对象

区别Httpsessioncookie
Httpsession:存放在服务端计算机内存,服务器Cookie:存放在客户端计算机(浏览器内存/硬盘)
HttpSession对象可以存储任意类型的共享数据objectcookie对象存储共享数据类型只能是string
Httpsession使用map集合存储共享数据,所以可以存储任意数量共享数据一个cookie对象只能存储一个共享数据
HattpSession相当于客户在服务端【私人保险柜】cookie相当于客户在服务端【会员卡】

Springboot

优点

  • 独立运行

    • Spring Boot而且内嵌了各种servlet容器,Tomcat、Jetty等,现在不再需要打成war包部署到容器中,Spring Boot只要打成一个可执行的jar包就能独立运行,所有的依赖包都在一个jar包内。
  • 简化配置

    • spring-boot-starter-web启动器自动依赖其他组件,简少了maven的配置。除此之外,还提供了各种启动器,开发者能快速上手。
  • 自动配置

    • Spring Boot能根据当前类路径下的类、jar包来自动配置bean,如添加一个spring-boot-starter-web启动器就能拥有web的功能,无需其他配置。
  • 无代码生成和XML配置

    • Spring Boot配置过程中无代码生成,也无需XML配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是Spring4.x的核心功能之一。
  • 应用监控

    • Spring Boot提供一系列端点可以监控服务及应用,做健康检测。

核心注解

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

  • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。

  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,

  • 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

  • @ComponentScan:Spring组件扫描。

自动配置原理

ES

什么是ElasticSearch?

Elasticsearch是一个基于Lucene的搜索引擎。

倒排索引:根据字面意思可以知道他和正序索引是反的。在搜索引擎中每个文件都对应一个文件ID,文件内容被表示为一系列关键词的集合(文档要除去一些无用的词,比如’的’这些,剩下的词就是关键词,每个关键词都有自己的ID)。例如“文档1”经过分词,提取了3个关键词,每个关键词都会记录它所在在文档中的出现频率及出现位置。

那么上面的文档及内容构建的倒排索引结果会如下图(注:这个图里没有记录展示该词在出现在哪个文档的具体位置):[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nuuzqTzu-1676791663330)(C:\Users\cqk\AppData\Roaming\Typora\typora-user-images\image-20230219101306609.png)]

如何来查询呢?

比如我们要查询‘搜索引擎’这个关键词在哪些文档中出现过。首先我们通过倒排索引可以查询到该关键词出现的文档位置是在1和3中;然后再通过正排索引查询到文档1和3的内容并返回结果。

Redis

1 redis是单线程吗?

  • 为什么使用 Redis ?

    • 速度快,因为数据存在内存中,类似于 HashMap, HashMap 的优势就
      是查找和操作的时间复杂度都是 O(1)
    • 支持丰富数据类型,支持 string, list, set, Zset, hash 等
    • 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
    • 丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除
  • Redis到底是单线程还是多线程?

    • 如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程
    • 如果是聊整个Redis,那么答案就是多线程
  • 在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:

    • Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlink
    • Redis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率

    因此,对于Redis的核心网络模型,在Redis 6.0之前确实都是单线程。是利用epoll(Linux系统)这样的IO多路复用技术在事件循环中不断处理客户端情况。

  • 为什么Redis要选择单线程?

    • 抛开持久化不谈,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
    • 多线程会导致过多的上下文切换,带来不必要的开销
    • 引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,而且性能也会大打折扣
    • 使用了 I/O 多路复用模型,select、epoll 等,基于 reactor 模式开发了自己的网络事件处理器

2 redis底层数据结构

Redis 的字符串(SDS)和C语言的字符串区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n2gBAZBw-1676791663330)(img\redis-底层String结构.png)]

Sorted Set底层数据结构

Sorted Set(有序集合)当前有两种编码:ziplist、skiplist

ziplist:使用压缩列表实现,当保存的元素长度都小于64字节,同时数量小于128时,使用该编码方式,否则会使用 skiplist。这两个参数可以通过 zset-max-ziplist-entries、zset-max-ziplist-value 来自定义修改。

skiplist:zset实现,一个zset同时包含一个字典(dict)和一个跳跃表(zskiplist)

Sorted Set 为什么同时使用字典和跳跃表?

主要是为了提升性能。

单独使用字典:在执行范围型操作,比如 zrank、zrange,字典需要进行排序,至少需要 O(NlogN) 的时间复杂度及额外 O(N) 的内存空间。

单独使用跳跃表:根据成员查找分值操作的复杂度从 O(1) 上升为 O(logN)。

Hash 对象底层结构

Hash 对象当前有两种编码:ziplist、hashtable

ziplist:使用压缩列表实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的节点推入到压缩列表的表尾,然后再将保存了值的节点推入到压缩列表表尾。

因此:1)保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;2)先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加的会被放在表尾方向。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aHNLv6GD-1676791663331)(C:\Users\cqk\AppData\Roaming\Typora\typora-user-images\image-20230131130952251.png)]

hashtable:使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值来保存,跟 java 中的 HashMap 类似。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AYTu2WVP-1676791663331)(C:\Users\cqk\AppData\Roaming\Typora\typora-user-images\image-20230131131016739.png)]

3 内存淘汰

**内存淘汰:**就是当Redis内存使用达到设置的上限时,主动挑选部分key删除以释放更多内存的流程。Redis会在处理客户端命令的方法processCommand()中尝试做内存淘汰:

  • Redis支持8种不同策略来选择要删除的key:

    • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

    • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

    • allkeys-random:对全体key,随机进行淘汰。也就是直接从db->dict中随机挑选

    • volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。

    • allkeys-lru: 对全体key,基于LRU算法进行淘汰

    • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

    • allkeys-lfu: 对全体key,基于LFU算法进行淘汰

    • volatile-lfu: 对设置了TTL的key,基于LFI算法进行淘汰

  • 比较容易混淆的有两个:

    • LRU(Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
    • LFU(Least Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

4 过期key处理

  • 惰性删除:顾明思议并不是在TTL到期后就立刻删除,而是在访问一个key的时候,检查该key的存活时间,如果已经过期才执行删除。

  • 周期删除:顾明思议是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。执行周期有两种

    • Redis服务初始化函数initServer()中设置定时任务,按照server.hz的频率来执行过期key清理,模式为SLOW
    • Redis的每个事件循环前会调用beforeSleep()函数,执行过期key清理,模式为FAST
  • SLOW模式规则:

    • 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。

    • 执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms

    • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期

    • 如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束

    • FAST模式规则(过期key比例小于10%不执行 ):

    • 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms

    • 执行清理耗时不超过1ms

    • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期 如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束

小总结:

RedisKey的TTL记录方式:在RedisDB中通过一个Dict记录每个Key的TTL时间

过期key的删除策略:

  • 惰性清理:每次查找key时判断是否过期,如果过期则删除

  • 定期清理:定期抽样部分key,判断是否过期,如果过期则删除。

  • 定期清理的两种模式:

    • SLOW模式执行频率默认为10,每次不超过25ms
    • FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

5 持久化

Redis 的持久化机制有:RDB、AOF、混合持久化(RDB+AOF,Redis 4.0引入)

RDB

描述:类似于快照。在某个时间点,将 Redis 在内存中的数据库状态(数据库的键值对等信息)保存到磁盘里面。RDB 持久化功能生成的 RDB 文件是经过压缩的二进制文件。

命令:有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE。

**save 60 1000代表什么含义?**代表60秒内至少执行1000次修改则触发RDB

RDB持久化在四种情况下会执行:save、bgsave、Redis停机时、触发RDB条件时

关闭:注释掉 或者 设置 save “”

RDB原理

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存;

  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

  • RDB方式bgsave的基本流程?

    • fork主进程得到一个子进程,共享内存空间

    • 子进程读取内存数据并写入新的RDB文件

    • 用新RDB文件替换旧的RDB文件

RDB的缺点?

  • RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
  • fork子进程、压缩、写出RDB文件都比较耗时

AOF

描述:保存 Redis 服务器所执行的所有写操作命令来记录数据库状态,并在服务器启动时,通过重新执行这些命令来还原数据集。

开启:AOF 持久化默认是关闭的,可以通过配置:appendonly yes 开启。

关闭:使用配置 appendonly no 可以关闭 AOF 持久化。

AOF 持久化功能的实现可以分为三个步骤:命令追加、文件写入、文件同步。

appendfsync 参数有三个选项

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NwmvILdY-1676791663332)(C:\Users\cqk\AppData\Roaming\Typora\typora-user-images\image-20230131133033790.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xO5AbCE5-1676791663332)(C:\Users\cqk\AppData\Roaming\Typora\typora-user-images\image-20230131133100325.png)]

混合持久化

描述:混合持久化并不是一种全新的持久化方式,而是对已有方式的优化。混合持久化只发生于 AOF 重写过程。使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。

整体格式为:[RDB file][AOF tail]

开启:混合持久化的配置参数为 aof-use-rdb-preamble,配置为 yes 时开启混合持久化,在 redis 4 刚引入时,默认是关闭混合持久化的,但是在 redis 5 中默认已经打开了。

关闭:使用 aof-use-rdb-preamble no 配置即可关闭混合持久化。

混合持久化本质是通过 AOF 后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork 出的子进程先将当前全量数据以 RDB 方式写入新的 AOF 文件,然后再将 AOF 重写缓冲区(aof_rewrite_buf_blocks)的增量命令以 AOF 方式写入到文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

优点:结合 RDB 和 AOF 的优点, 更快的重写和恢复。

缺点:AOF 文件里面的 RDB 部分不再是 AOF 格式,可读性差。

RDB AOF 混合持久 用哪个

一般来说, 如果想尽量保证数据安全性, 你应该同时使用 RDB 和 AOF 持久化功能,同时可以开启混合持久化。

如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

如果你的数据是可以丢失的,则可以关闭持久化功能,在这种情况下,Redis 的性能是最高的。

使用 Redis 通常都是为了提升性能,而如果为了不丢失数据而将 appendfsync 设置为 always 级别时,对 Redis 的性能影响是很大的,在这种不能接受数据丢失的场景,其实可以考虑直接选择 MySQL 等类似的数据库。

6 主从复制

1)开启主从复制

通常有以下三种方式:

  • 在 slave 直接执行命令:slaveof
  • 在 slave 配置文件中加入:slaveof
  • 使用启动命令:–slaveof

注:在 Redis 5.0 之后,slaveof 相关命令和配置已经被替换成 replicaof,例如 replicaof 。为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。

2)主从数据同步原理

概念

  • Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid

  • **offset:**偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

  • repl_backlog:master怎么知道slave与自己的数据差异在哪里呢?这就要说到全量同步时的repl_baklog文件了。这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UE2qb6uE-1676791663333)(C:\Users\cqk\AppData\Roaming\Typora\typora-user-images\image-20230131133849970.png)]

全量同步

  • 主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点

  • salve节点宕机时,从节点repl_backlog 被 主节点的 repl_backlog 超过,会全量同步

  • 完整流程描述:

    • slave节点请求增量同步

    • master节点判断replid,发现不一致,拒绝增量同步,把自己的rep_id和offset发过去,salve进行保存。

    • master将完整内存数据生成RDB,发送RDB到slave

    • slave清空本地数据,加载master的RDB

    • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave

    • slave执行接收到的命令,保持与master之间的同步

增量同步

  • master怎么知道slave与自己的数据差异在哪里呢?这就要说到全量同步时的repl_baklog文件了。

  • 这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

7 哨兵

  • 哨兵的作用:

    • 监控:Sentinel 会不断检查您的master和slave是否按预期工作

    • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主

    • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

  • **集群监控原理:**Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

    • 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线

    • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

  • 集群故障恢复原理

    • 一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
      • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
      • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举
      • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
      • 最后是判断slave节点的运行id大小,越小优先级越高(随便挑一个)
    • 当选出一个新的master后,该如何实现切换呢?
      • sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master
      • sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
      • 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点

8 集群

哨兵模式最大的缺点就是所有的数据都放在一台服务器上,无法较好的进行水平扩展。

为了解决哨兵模式存在的问题,集群模式应运而生。在高可用上,集群基本是直接复用的哨兵模式的逻辑,并且针对水平扩展进行了优化。

集群模式具备的特点如下:

  1. 采取去中心化的集群模式,将数据按槽存储分布在多个 Redis 节点上。集群共有 16384 个槽,每个节点负责处理部分槽。
  2. 使用 CRC16 算法来计算 key 所属的槽:crc16(key,keylen) & 16383。
  3. 所有的 Redis 节点彼此互联,通过 PING-PONG 机制来进行节点间的心跳检测。
  4. 分片内采用一主多从保证高可用,并提供复制和故障恢复功能。在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题,下面的架构图就是这样设计的。
  5. 客户端与 Redis 节点直连,不需要中间代理层(proxy)。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

集群选举

故障转移的第一步就是选举出新的主节点,以下是集群选举新的主节点的方法:

1)当从节点发现自己正在复制的主节点进入已下线状态时,会发起一次选举:将 currentEpoch(配置纪元)加1,然后向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。

2)其他节点收到消息后,会判断是否要给发送消息的节点投票,判断流程如下:

  1. 当前节点是 slave,或者当前节点是 master,但是不负责处理槽,则当前节点没有投票权,直接返回。
  2. 请求节点的 currentEpoch 小于当前节点的 currentEpoch,校验失败返回。因为发送者的状态与当前集群状态不一致,可能是长时间下线的节点刚刚上线,这种情况下,直接返回即可。
  3. 当前节点在该 currentEpoch 已经投过票,校验失败返回。
  4. 请求节点是 master,校验失败返回。
  5. 请求节点的 master 为空,校验失败返回。
  6. 请求节点的 master 没有故障,并且不是手动故障转移,校验失败返回。因为手动故障转移是可以在 master 正常的情况下直接发起的。
  7. 上一次为该master的投票时间,在cluster_node_timeout的2倍范围内,校验失败返回。这个用于使获胜从节点有时间将其成为新主节点的消息通知给其他从节点,从而避免另一个从节点发起新一轮选举又进行一次没必要的故障转移
  8. 请求节点宣称要负责的槽位,是否比之前负责这些槽位的节点,具有相等或更大的 configEpoch,如果不是,校验失败返回。

如果通过以上所有校验,那么主节点将向要求投票的从节点返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。

3)每个参与选举的从节点都会接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根据自己收到了多少条这种消息来统计自己获得了多少个主节点的支持。

4)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1 张支持票时,这个从节点就会当选为新的主节点。因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N个主节点进行投票,那么具有大于等于 N/2+1 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。

5)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

这个选举新主节点的方法和选举领头 Sentinel 的方法非常相似,因为两者都是基于 Raft 算法的领头选举(leader election)方法来实现的。

9 Redis 事务

一个事务从开始到结束通常会经历以下3个阶段:

1)事务开始:multi 命令将执行该命令的客户端从非事务状态切换至事务状态,底层通过 flags 属性标识。

2)命令入队:当客户端处于事务状态时,服务器会根据客户端发来的命令执行不同的操作:

  • exec、discard、watch、multi 命令会被立即执行
  • 其他命令不会立即执行,而是将命令放入到一个事务队列,然后向客户端返回 QUEUED 回复。

3)事务执行:当一个处于事务状态的客户端向服务器发送 exec 命令时,服务器会遍历事务队列,执行队列中的所有命令,最后将结果全部返回给客户端。

不过 redis 的事务并不推荐在实际中使用,如果要使用事务,推荐使用 Lua 脚本,redis 会保证一个 Lua 脚本里的所有命令的原子性。

RabbitMq

MQ解决场景

消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削锋和消息通讯四个场景

异步处理:发送短信到数据库,写入消息队列,就结束了。mq监听消息4队列,消费队列里的信息。image-20230213230517436

应用解耦

订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。

假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦

image-20230213231050731

流量削锋:用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面,秒杀业务根据消息队列中的请求信息,再做后续处理

image-20230213231139858

消息通讯

客户端A和客户端B使用同一队列,进行消息通讯。

聊天室通讯:

客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。

以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。

image-20230213231347547

MQ解决分布式事务

**分布式事务:**不同的服务操作不同的数据源(库或表),保证数据一致性的问题。

**解决:**采用RabbitMQ消息最终一致性的解决方案,解决分布式事务问题。

分布式事务场景:

​ 1、电商项目中的商品库和ES库数据同步问题。

​ 2、电商项目中:支付----à订单—à库存,一系列操作,进行状态更改等。

在互联网应用中,基本都会有用户注册的功能。在注册的同时,我们会做出如下操作:

收集用户录入信息,保存到数据库向用户的手机或邮箱发送验证码等等…

如果是传统的集中式架构,实现这个功能非常简单:开启一个本地事务,往本地数据库中插入一条用户数据,发送验证码,提交事物。

问题:但是在分布式架构中,用户和发送验证码是两个独立的服务,它们都有各自的数据库,那么就不能通过本地事物保证操作的原子性。这时我们就需要用到 RabbitMQ(消息队列)来为我们实现这个需求。

解决方案:在用户进行注册操作的时候,我们为该操作创建一条消息,当用户信息保存成功时,把这条消息发送到消息队列。验证码系统会监听消息,一旦接受到消息,就会给该用户发送验证码。

MQ如何确保消息可靠性

  • 消息队列本身:将Exchange、Queue、message进行持久化。

  • 生产者保证到交换机implements RabbitTemplate.ConfirmCallback

    • RabbitMQ提供transaction事务和confirm模式来确保生产者不丢消息。
    • confirm(CorrelationData correlationData, boolean ack, String cause),ack确认是否到交换机
  • 交换机到消费队列 RabbitTemplate.ReturnCallback

    • 消费者丢数据一般是因为采用了自动确认消息模式,消费者在收到消息之后,处理消息之前,会自动回复RabbitMQ已收到消息;如果这时处理消息失败,就会丢失该消息;改为手动确认消息即可!手动确认模式下消费失败时,不将其重新放入队列(确认重试也不会成功的情形),打印错误信息后,通知相关人员,人工介入处理。

MQ消息重复消费

  • 方式一:Redis的setNX() , 做消息id去重 java版本目前不支持设置过期时间

  • 方式二:redis的 Incr 原子操作:key自增,大于0 返回值大于0则说明消费过,(key可以是消息的md5取值, 或者如果消息id设计合理直接用id做key)

     int num =  jedis.incr(key);
     if(num == 1){
       //消费
     }else{
       //忽略,重复消费
     }
    
  • 方式三:数据库去重表,设计一个去重表,某个字段使用Message的key做唯一索引,因为存在唯一索引,所以重复消费会失败

    ​ CREATE TABLE message_record ( id int(11) unsigned NOT NULL AUTO_INCREMENT, key varchar(128) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY key (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

四大核心概念

生产者 、交换机 、队列 、消费者

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vvaDxWtM-1676791663333)(C:\Users\cqk\AppData\Roaming\Typora\typora-user-images\image-20230219132700593.png)]

交换机类型

  • Fanout :这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的
    所有队列中。系统中默认有些 exchange 类型

  • Direct exchange :

  • Topics :发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单
    词列表,以点号分隔开。这些单词可以是任意单词,比如说: “stock.usd.nyse”, “nyse.vmw”,
    “quick.orange.rabbit”.这种类型的。当然这个单词列表最多不能超过 255 个字节。
    在这个规则列表中,其中有两个替换符是大家需要注意的
    *(星号)可以代替一个单词
    #(井号)可以替代零个或多个单词

设计模式

1 单例模式

单例设计模式分类两种:

​ 饿汉式:类加载就会导致该单实例对象被创建

​ 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

饿汉式

饿汉式-方式1(静态变量方式)

//饿汉式-静态变量创建类的对象
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance = new Singleton();

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

饿汉式-方式2(静态代码块方式)

//饿汉式-在静态代码块中创建该类对象
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    static {
        instance = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

懒汉式

懒汉式-方式3(双重检查锁DCL,double check lock)

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

//双重检查方式
public class Singleton { 
    //私有构造方法
    private Singleton() {}

    private static volatile Singleton instance;

   //对外提供静态方法获取该对象
    public static Singleton getInstance() {
		//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

懒汉式-方式4(静态内部类方式)

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

public class Singleton {
    //私有构造方法
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

说明:第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder,并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

小结:静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

2 动态代理

//接口
public interface Sell {
    public void sell();
}

//实现类
public class SellImpl implements Sell {
    @Override
    public void sell(){
        System.out.println("卖东西!!");
    }
}

//实现InvocationHandler
public class MyInvocationHandler implements InvocationHandler {
    private Object obj;
    public MyInvocationHandler(Object obj) {
        this.obj = obj;
    }
    //object proxy:jdk创建的代理对象,无需赋值。
    //Method method:目标类中的方法,jdk提供method对象的
    //object[]args:目标类中方法的参数,jdk提供的。
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前面");
        Object invoke = method.invoke(obj, args);
        System.out.println("后面");
        return null;
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
        Sell sellImpl = new SellImpl();
        InvocationHandler invocationHandler = new MyInvocationHandler(sellImpl);
        Sell sell = (Sell)Proxy.newProxyInstance(
                sellImpl.getClass().getClassLoader(),
                sellImpl.getClass().getInterfaces(),
                invocationHandler);
        sell.sell();
    }
}

3 工厂模式

简单工厂

一个工厂方法,依据传入的参数,生成对应的产品对象;
角色:抽象产品、具体产品、具体工厂、产品使用者。

先将产品类抽象出来,比如,苹果和梨都属于水果,抽象出来一个水果类Fruit,苹果和梨就是具体的产品类,然后创建一个水果工厂,分别用来创建苹果和梨。代码如下:

// 水果接口
public interface Fruit {  
    void whatIm();  
} 
// 苹果类
public class Apple implements Fruit {  
    @Override  
    public void whatIm() {  
        System.out.println("苹果");  
    }  
}  
//	梨类
public class Pear implements Fruit {  
    @Override  
    public void whatIm() {  
        System.out.println("梨");  
    }  
}  
// 水果工厂
public class FruitFactory {    
    public Fruit createFruit(String type) {   
        if (type.equals("apple")) {//生产苹果  
            return new Apple();  
        } else if (type.equals("pear")) {//生产梨  
            return new Pear();  
        }  
  
        return null;  
    }  
}

// 测试方法
public class FruitApp {    
    public static void main(String[] args) {  
        FruitFactory mFactory = new FruitFactory();  
        Apple apple = (Apple) mFactory.createFruit("apple");//获得苹果  
        Pear pear = (Pear) mFactory.createFruit("pear");//获得梨  
        apple.whatIm();  
        pear.whatIm();  
   }  
} 

以上的这种方式,每当添加一种水果,就必然要修改工厂类,违反了开闭原则;

所以简单工厂只适合于产品对象较少,且产品固定的需求,对于产品变化无常的需求来说显然不合适。

工厂方法

定义:将工厂提取成一个接口或抽象类,具体生产什么产品由子类决定;
角色:抽象产品、具体产品、抽象工厂、具体工厂

使用说明:和上例中一样,产品类抽象出来,这次我们把工厂类也抽象出来,生产什么样的产品由子类来决定。代码如下:水果接口、苹果类和梨类。

// 抽象工厂接口
public interface FruitFactory {  
    Fruit createFruit();//生产水果  
}  

// 苹果工厂
public class AppleFactory implements FruitFactory {  
    @Override  
    public Apple createFruit() {  
        return new Apple();  
    }  
}

// 梨工厂
public class PearFactory implements FruitFactory {  
    @Override  
    public Pear createFruit() {  
        return new Pear();  
    }  
} 

// 使用工厂生产产品
public class FruitApp {    
    public static void main(String[] args){  
        AppleFactory appleFactory = new AppleFactory();  
        PearFactory pearFactory = new PearFactory();  
        Apple apple = appleFactory.createFruit();//获得苹果  
        Pear pear = pearFactory.createFruit();//获得梨  
        apple.whatIm();  
        pear.whatIm();  
    }  
}  

以上这种方式,虽然解耦了,也遵循了开闭原则,但是如果我需要的产品很多的话,需要创建非常多的工厂,所以这种方式的缺点也很明显。

抽象工厂

定义:为创建一组相关或者是相互依赖的对象提供的一个接口,而不需要指定它们的具体类。
角色:抽象产品、具体产品、抽象工厂、具体工厂

使用说明:抽象工厂和工厂方法的模式基本一样,区别在于,工厂方法是生产一个具体的产品,而抽象工厂可以用来生产一组相同,有相对关系的产品;重点在于一组,一批,一系列;举个例子,假如生产小米手机,小米手机有很多系列,小米note、红米note等;假如小米note生产需要的配件有825的处理器,6英寸屏幕,而红米只需要650的处理器和5寸的屏幕就可以了。用抽象工厂来实现:

cpu接口和实现类

public interface Cpu {  
    void run();  
  
    class Cpu650 implements Cpu {  
        @Override  
        public void run() {  
            System.out.println("650 也厉害");  
        }  
    }  
  
   Cpu825 implements Cpu {  
        @Override  
        public void run() {  
            System.out.println("825 更强劲");  
        }  
    }  
} 

屏幕接口和实现类

public interface Screen {  
      void size();  
  
    class Screen5 implements Screen {    
        @Override  
        public void size() {  
            System.out.println("" +"5寸");  
        }  
    }  
  
    class Screen6 implements Screen {    
        @Override  
        public void size() {  
            System.out.println("6寸");  
        }  
    }  
}  

4 模板模式

算法

冒泡

for (int i = 0; i < arr.length - 1; i++) {
    for (int j = 0; j < arr.length - 1 - i; j++) {
        if (arr[j] > arr[j+1]){
            int temp = arr[j];
            arr[j] = arr[j+1];
            arr[j+1] = temp;
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mjAw7Ib6-1676791663334)(img\算法-冒泡排序.gif)]

选择排序

private static void selectSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[i] > arr[j]) {
                int temp = arr[j];
                arr[j] = arr[i];
                arr[i] = temp;
            }
        }
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RufJCwy2-1676791663334)(img\算法-选择排序.gif)]

快排

快速排序动画演示

JVM

1 JVM内存结构?

JVM-内存结构
  • 执行引擎:将字节码指令解析为对应 平台 的本地指令

  • 类加载器:将Class文件加载到内存中

  • jvm将虚拟机分为5大区域,程序计数器、栈、本地方法栈、java堆、方法区;

    • 程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址

    • :线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;

    • 本地方法栈:线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;

    • :java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存(new的对象),因此该区域经常发生垃圾回收的操作;

    • 方法区:存放已被加载的类信息、常量池、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;

    • 常量池在方法区中。字符串池在堆中。运行时常量池也是方法区的一部分

2 栈内存溢出

  • 栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
  • 当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归调用肯可能会出现该问题;
  • 调整参数-xss去调整jvm栈的大小

3 谈谈对 OOM 的认识?

除了程序计数器,其他内存区域都有 OOM 的风险。

  • 栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
  • Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元空间大小参数无效;
  • 堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;
  • 方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;
  • 直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。

排查 OOM 的方法:

  • 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
  • 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
  • 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。

4 判断一个对象存活?

判断一个对象是否存活,分为两种算法:引用计数法、可达性分析算法

引用计数法:通过判断对象的引用数量来决定对象是否可以被回收。

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

优点:简单,高效,现在的objective-c、python等用的就是这种算法。

缺点:引用和去引用伴随着加减算法,影响性能,很难处理循环引用,相互引用的两个对象则无法释放。

**可达性分析法:**在Java语言中,可以作为GC Roots的对象包括下面几种:

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

真正标记以为对象为可回收状态至少要标记两次。

第一次标记:不在 GC Roots 链中,标记为可回收对象。

第二次标记:判断当前对象是否实现了finalize() 方法,如果没有实现则直接判定这个对象可以回收,如果实现了就会先放入一个队列中。并由虚拟机建立一个低优先级的程序去执行它,随后就会进行第二次小规模标记,在这次被标记的对象就会真正被回收了!

5 四种引用

  • 强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)

  • 软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。SoftReference 实现

  • 弱引用,相比软引用来说,要更加无用一些,它拥有更短的生命周期,当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现

  • 虚引用是一种形同虚设的引用,在现实场景中用的不是很多,它主要用来跟踪对象被垃圾回收的活动。PhantomReference 实现

6 垃圾回收算法

标记清除法、标记整理法、复制算法、分代收集算法;

  • 标记清除法
    第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
    第二步:在遍历一遍,将所有标记的对象回收掉;
    特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;

  • 标记整理法
    第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
    第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉;
    特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;

  • 复制算法
    将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到另一块上,然后在把使用过的内存空间移除;
    特点:不会产生空间碎片;内存使用率极低;

  • 分代收集算法
    根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清理或者标记整理算法进行回收;

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FmcDN9ti-1676791663334)(img\JVM-回收算法.webp)]

7 垃圾回收器

  • 并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态

  • 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上

  • 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%

垃圾回收器主要分为三类:串行、吞吐量优先、响应时间优先

串行:Serial、ParNew、Serial Old

吞吐量优先:Parallel Scavenge、、Parallel Old、

响应时间优先:CMS

  • Serial:单线程的收集器,收集垃圾时,必须stop the world,使用复制算法。它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,是client级别的默认GC方式。

  • ParNew:Serial收集器的多线程版本,也需要stop the world,复制算法

  • Parallel Scavenge:新生代收集器,复制算法的收集器,并发的多线程收集器,目标是达到一个可控的吞吐量,和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量;

  • Serial Old:Serial收集器的老年代版本,单线程收集器,使用标记整理算法。

  • Parallel Old:是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。

  • CMS:是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片;

  • G1:标记整理算法实现,运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;

8 CMS详细流程

CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。

img

从名字就可以知道,CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:

  1. 初始标记 (CMS initial mark):主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
  2. 并发标记 (CMS concurrent mark):根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。
  3. 重新标记(CMS remark):顾名思义,就是要再标记一次。为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
  4. 并发清除(CMS concurrent sweep):清除阶段是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。

CMS 的问题:

1. 并发回收导致CPU资源紧张:

在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对用户程序的影响就可能变得很大。

2. 无法清理浮动垃圾:

在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃圾收集时再清掉。这一部分垃圾称为“浮动垃圾”。

3. 并发失败(Concurrent Mode Failure):

由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可以通过 -XX**😗* CMSInitiatingOccupancyFraction 参数来设置。

这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。

4.内存碎片问题:

CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。内存碎片过多时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。

为了解决这个问题,CMS收集器提供了一个 -XX**:+UseCMSCompactAtFullCollection 开关参数(默认开启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法并发的,这样停顿时间就会变长。还有另外一个参数 -XX😗*CMSFullGCsBeforeCompaction,这个参数的作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。

9 G1详细流程

G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS 被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两个Region之间)上看又是基于 标记-复制 算法实现的。

G1 回收过程,G1 回收器的运作过程大致可分为四个步骤:

  1. 初始标记(会STW):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  2. 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
  3. 最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
  4. 清理阶段(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的。

10 Minor GC 和 Full GC

Minor GC:只收集新生代的GC。

Full GC: 收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。

**Minor GC触发条件:**当Eden区满时,触发Minor GC。

Full GC触发条件

  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。如果发现统计数据说之前Minor GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
  • 老年代空间不够分配新的内存(或永久代空间不足,但只是JDK1.7有的,这也是用元空间来取代永久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)。
  • 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
  • 调用System.gc时,系统建议执行Full GC,但是不必然执行。

11 JVM中一次完整的GC

先描述一下Java堆内存划分。

在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的 1/3,老年代默认占 2/3。
新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。

新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。

老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。

img

再描述它们之间转化流程:

  • 对象优先在Eden分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC。

    • 在 Eden 区执行了第一次 GC 之后,存活的对象会被移动到其中一个 Survivor 分区;

    • Eden 区再次 GC,这时会采用复制算法,将 Eden 和 from 区一起清理,存活的对象会被复制到 to 区;

    • 移动一次,对象年龄加 1,对象年龄大于一定阀值会直接移动到老年代。GC年龄的阀值可以通过参数 -XX:MaxTenuringThreshold 设置,默认为 15;

    • 动态对象年龄判定:Survivor 区相同年龄所有对象大小的总和 > (Survivor 区内存大小 * 这个目标使用率)时,大于或等于该年龄的对象直接进入老年代。其中这个使用率通过 -XX:TargetSurvivorRatio 指定,默认为 50%;

    • Survivor 区内存不足会发生担保分配,超过指定大小的对象可以直接进入老年代。

  • 大对象直接进入老年代,大对象就是需要大量连续内存空间的对象(比如:字符串、数组),为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。

  • 老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,Full GC 清理整个内存堆 – 包括年轻代和老年代

12 类加载器和双亲委派

类加载的过程

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

类的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示:

img

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

类加载过程如下:

  • 加载,加载分为三步:
    1、通过类的全限定性类名获取该类的二进制流;
    2、将该二进制流的静态存储结构转为方法区的运行时数据结构;
    3、在堆中为该类生成一个class对象;

  • 验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全;

  • 准备:为class对象的静态变量分配内存,初始化其初始值;

  • 解析:该阶段主要完成符号引用转化成直接引用;

  • 初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程;

  • 加载: 类的字节码加载到方法区,instanceKlass和类.class(堆)互相持有引用,会先加载父类

  • 连接:检查规范,静态常量分配空间和赋值,静态变量分配空间,存在_java_mirror 末尾(指向类.class),final基本变量会赋值解析符号引用(只有基本类型常量在这个时候能确定值)

  • 初始化:懒惰,必要时才初始化,包括静态变量赋值,引用常量符号引用变直接引用,线程安全

  • 方法的执行和结束对应着虚拟机栈的弹栈和压栈,一些变量存储在局部变量表,需要计算时用到了操作数栈,对象存储在堆中

类加载器

类加载器ClassLoader和双亲委派机制

负责加载class文件,class文件在文件开头有特定的文件标示,并且ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p4yFXch4-1676791663336)(img\JVM-类加载器.png)]

类加载器分为四种:前三种为虚拟机自带的加载器。

  • 启动类加载器(Bootstrap)C++

    负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类

  • 扩展类加载器(Extension)Java

    负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/*.jar或-Djava.ext.dirs指定目录下的jar包

  • 应用程序类加载器(AppClassLoader)Java

    也叫系统类加载器,负责加载classpath中指定的jar包及目录中class

  • 用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式

双亲委派

工作过程:

  • 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
  • 5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

其实这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nWGmje9W-1676791663337)(C:\Users\cqk\Desktop\面试总结\img\JVM双亲委派.png)]

好处:防止内存中出现多份同样的字节码(安全性角度)
比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。

写段儿代码演示类加载器:

public class Demo {

    public Demo() {
        super();
    }
	//调用native方法的是本地的,是启动类加载器,获取不到
    //启动类加载器是C++实现的,在获取的时候拿不到,返回null
    //扩展类加载器和应用类加载器是java实现,可以获取到
    public static void main(String[] args) {
        Object obj = new Object();
        String s = new String();
        Demo demo = new Demo();
        System.out.println(obj.getClass().getClassLoader());
        System.out.println(s.getClass().getClassLoader());
        System.out.println(demo.getClass().getClassLoader().getParent().getParent());
        System.out.println(demo.getClass().getClassLoader().getParent());
        System.out.println(demo.getClass().getClassLoader());
    }
}

打印控制台中的sun.misc.Launcher,是一个java虚拟机的入口应用

由于BootStrap ClassLoader是用c++写的,所以在返回该ClassLoader时会返回null

补充:那怎么打破双亲委派模型?

自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法。

列举一些你知道的打破双亲委派机制的例子,为什么要打破?

JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。

JUC

Lock

Lock是接口,一般用ReentrantLock实现。是可重入锁,但是加几次锁,释放几次锁

**公平锁:**线程饿死,效率高。

**非公平锁(默认):**阳光普照。不会有线程饿死。每个人都可以使用,效率相对低

private final Lock lock = new ReentrantLock(); //创建可重入锁
lock.lock();//上锁
lock.unlock(); //释放锁,一般在finally里实现

newCondition方法

参数 Condition condition = lock.newCondition();newCondition
condition newCondition();返回绑定到lock的新的实例
condition.await();//等待 类似 wait();
condition.signalAll();//唤醒等待所有线程 类似 notifyAll();
condition.signal();唤醒单个线程,类似 notify();
  • 线程之间虚假唤醒:一个做面的厨师,一个吃面的食客,需要保证,厨师只有做一碗面,食客才可以吃一碗面,不能一次性多做几碗面,更不能没有面的时候吃面;按照上述操作,进行十轮做面吃面的操作。

  • 虚假唤醒解决方案

    • 出现虚假唤醒的原因是从阻塞态到就绪态再到运行态没有进行判断,我们只需要让其每次得到操作权时都进行判断就可以了。
    • 使用wait时,在哪里睡就会在哪里醒,使用if()条件,二次醒来时会跳过if()条件。使用while()会每次判断。
  • **线程间定制化通信:**假设a,b,c。依次执行使用condition.signal();挨个唤醒

synchronized

Ⅰ:public synchronized void sendEmail() //锁的是:this对象锁(类似一个房间门锁)
Ⅱ:public static synchronized void sendSMS() //锁的是:Class类锁(类似整个家的门锁)。
Ⅲ:public void sendEmail() //没有锁
注意:类锁(Class)和对象锁(this)是两个不同的锁。它们之间没有任何关系,不会存在竞争和锁住对方的。
1、如果一个实例对象的普通同步方法(I中方法)获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
2、一个静态同步方法(Ⅱ中方法)获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
  • synchronized三种方式
    • 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁
    • 作用于代码块,对括号里配置的对象加锁
    • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
  • **synchronized锁升级:**由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略
    • 偏向锁:单个线程多次访问。当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步
    • 轻量级锁:多线程竞争,但是在任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞,本质就是自旋锁。
    • 重锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁有。

首先是偏向锁,它会通过对象的markword头记录首次加锁线程id,当线程要加锁时只用判断线程id是否是自己即可

当多个线程要加锁但是没有发生竞争,会升级为轻量级锁,也就是在线程中增加一个锁记录,锁记录地址和markword互换,互相持有对方引用

当新的线程请求加锁,导致锁升级为重量级锁,这时候会申请一个管程,owner设置为当前线程,新加入的线程在自旋失败后会加入到管程的阻塞队列中去,管程还有一个waitset用于存放被wait()等待的那个线程

区别

synchronized隐式锁Lock显示锁
关键字
自动加锁和释放锁需要手动调用unlock方法释放锁
jvm层面的锁API层面的锁
非公平锁可以选择公平或者非公平锁
锁是一个对象,并且锁的信息保存在了对象中代码中通过int类型的state标识
有一个锁升级的过程

volatile

  • volatile修改的变量有2大特点:可见性、有序性(禁重排序)、不保证原子性

  • volatile凭什么可以保证有序性和可见性?靠的是内存屏障,内存屏障分为 loadload、StoreLoad、LoadStore、StoreStore。

volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。同时volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱。

synchronized和volatile有什么区别?

  • volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是可以锁定变量操作,只有当前线程可以访问操作该变量,其他线程被阻塞住。
  • volatile仅能用在变量级别,而synchronized可以使用在方法、类级别。
  • volatile仅能实现可见性,不能保证原子性;而synchronized则可以保证可见性和原子性。
  • volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
  • volatile标记的变量不会被编译器优化,synchronized的操作可以被编译器优化。

CAS

  • 概念:cas是一种乐观锁思想,是cpu原语,核心思想是每次请求假设没有竞争,则不需要加锁,通过比较当前预期旧的值和内存中实际的值进行比较,如果失败可能会在while循环中重试,成功才进行交换

  • CAS能解决ABA问题吗:cas存在aba问题,就是我们在比较的过程中,发现符合预期旧值和内存值相等,但这可能是已经发生了多次的修改变化结果。可以通过加版本号,每次还对这个版本号进行比较解决

BlockingQueue

这个了解不多,作为阻塞队列,要注意选取是有界还是无界

(ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素

ConcurrentLinkedDeque 是双向链表结构的无界并发队列

ConcurrentLinkedQueue 使用约定:

  1. 不允许 null 入列
  2. 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到
  3. 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点
    |
    | condition.await(); | //等待 类似 wait(); |
    | condition.signalAll(); | //唤醒等待所有线程 类似 notifyAll(); |
    | condition.signal(); | 唤醒单个线程,类似 notify(); |
  • 线程之间虚假唤醒:一个做面的厨师,一个吃面的食客,需要保证,厨师只有做一碗面,食客才可以吃一碗面,不能一次性多做几碗面,更不能没有面的时候吃面;按照上述操作,进行十轮做面吃面的操作。

  • 虚假唤醒解决方案

    • 出现虚假唤醒的原因是从阻塞态到就绪态再到运行态没有进行判断,我们只需要让其每次得到操作权时都进行判断就可以了。
    • 使用wait时,在哪里睡就会在哪里醒,使用if()条件,二次醒来时会跳过if()条件。使用while()会每次判断。
  • **线程间定制化通信:**假设a,b,c。依次执行使用condition.signal();挨个唤醒

synchronized

Ⅰ:public synchronized void sendEmail() //锁的是:this对象锁(类似一个房间门锁)
Ⅱ:public static synchronized void sendSMS() //锁的是:Class类锁(类似整个家的门锁)。
Ⅲ:public void sendEmail() //没有锁
注意:类锁(Class)和对象锁(this)是两个不同的锁。它们之间没有任何关系,不会存在竞争和锁住对方的。
1、如果一个实例对象的普通同步方法(I中方法)获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
2、一个静态同步方法(Ⅱ中方法)获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
  • synchronized三种方式
    • 作用于实例方法,当前实例加锁,进入同步代码前要获得当前实例的锁
    • 作用于代码块,对括号里配置的对象加锁
    • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁
  • **synchronized锁升级:**由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略
    • 偏向锁:单个线程多次访问。当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步
    • 轻量级锁:多线程竞争,但是在任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞,本质就是自旋锁。
    • 重锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁有。

首先是偏向锁,它会通过对象的markword头记录首次加锁线程id,当线程要加锁时只用判断线程id是否是自己即可

当多个线程要加锁但是没有发生竞争,会升级为轻量级锁,也就是在线程中增加一个锁记录,锁记录地址和markword互换,互相持有对方引用

当新的线程请求加锁,导致锁升级为重量级锁,这时候会申请一个管程,owner设置为当前线程,新加入的线程在自旋失败后会加入到管程的阻塞队列中去,管程还有一个waitset用于存放被wait()等待的那个线程

区别

synchronized隐式锁Lock显示锁
关键字
自动加锁和释放锁需要手动调用unlock方法释放锁
jvm层面的锁API层面的锁
非公平锁可以选择公平或者非公平锁
锁是一个对象,并且锁的信息保存在了对象中代码中通过int类型的state标识
有一个锁升级的过程

volatile

  • volatile修改的变量有2大特点:可见性、有序性(禁重排序)、不保证原子性

  • volatile凭什么可以保证有序性和可见性?靠的是内存屏障,内存屏障分为 loadload、StoreLoad、LoadStore、StoreStore。

volatile是Java提供的最轻量级的同步机制,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象。同时volatile禁止了指令重排,可以保证程序执行的有序性,但是由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱。

synchronized和volatile有什么区别?

  • volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是可以锁定变量操作,只有当前线程可以访问操作该变量,其他线程被阻塞住。
  • volatile仅能用在变量级别,而synchronized可以使用在方法、类级别。
  • volatile仅能实现可见性,不能保证原子性;而synchronized则可以保证可见性和原子性。
  • volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
  • volatile标记的变量不会被编译器优化,synchronized的操作可以被编译器优化。

CAS

  • 概念:cas是一种乐观锁思想,是cpu原语,核心思想是每次请求假设没有竞争,则不需要加锁,通过比较当前预期旧的值和内存中实际的值进行比较,如果失败可能会在while循环中重试,成功才进行交换

  • CAS能解决ABA问题吗:cas存在aba问题,就是我们在比较的过程中,发现符合预期旧值和内存值相等,但这可能是已经发生了多次的修改变化结果。可以通过加版本号,每次还对这个版本号进行比较解决

BlockingQueue

这个了解不多,作为阻塞队列,要注意选取是有界还是无界

(ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,采用先进先出的规则对节点进行排序,当添加一个元素时,会添加到队列的尾部,当获取一个元素时,会返回队列头部的元素

ConcurrentLinkedDeque 是双向链表结构的无界并发队列

ConcurrentLinkedQueue 使用约定:

  1. 不允许 null 入列
  2. 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到
  3. 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点
  4. head 节点跟 tail 不一定指向头节点或尾节点,可能存在滞后性)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值