java面试题

、Java基础

1.1 java的基本数据类型

基本数据类型共有8种,可以分为三类:

  • 数值型:整数类型(byte、short、int、long)和浮点类型(float、double)
  • 字符型:char
  • 布尔型:boolean

image-20230127192523435

image-20230127192249961

注意一下几点:

  • java八种基本数据类型的字节数:1字节(byte、boolean)、 2字节(short、char)、4字节(int、float)、8字节(long、double)
  • 浮点数的默认类型为double(如果需要声明一个常量为float型,则必须要在末尾加上f或F)
  • 整数的默认类型为int(声明Long型在末尾加上l或者L)
  • 八种基本数据类型的包装类:除了char的是Character、int类型的是Integer,其他都是首字母大写
  • char类型是无符号的,不能为负,所以是0开始的

int a=10;

long a=10;

long i = 20L;

float a =5.4f;

//        byte===>Byte;
//        short===>Short;
//        int===>Integer
//        float===>Float
//        double===>Double
//        char===>Character
//        boolean===>Boolean

        Double a = new Double(5);       //装箱,将基本类型转换成包装类型
        double b = a.doubleValue();             //拆箱,将包装类型转换成基本类型

        Double c = 5.8;         //java中可以实现自动拆箱和装箱
        double d=c;             //java中可以实现自动拆箱和装箱

1.2 java中常见的引用数据类型

类、接口类型、数组类型、枚举类型、注解类型

public enum AppHttpCodeEnum {

    SUCCESS(200,"操作成功"),
    NEED_LOGIN(1,"需要登录后操作");

    int code;
    String errorMessage;

    AppHttpCodeEnum(int code, String errorMessage){
        this.code = code;
        this.errorMessage = errorMessage;
    }

    public int getCode() {
        return code;
    }

    public String getErrorMessage() {
        return errorMessage;
    }
}
//元注解
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})    //指定@Demo2_3注解可以在哪些地方使用
@Retention(RetentionPolicy.RUNTIME)    //指定@Demo2_3注解在运行时起作用
public @interface Demo2_3 {

    String value() ;

    int age() default 0;
}

1.3、 ==和equals区别

(1) ==

如果比较的是基本数据类型,那么比较的是变量的值

(2)equals

如果没重写equals方法比较的是两个对象的地址值

如果重写了equals方法后我们往往比较的是对象中的属性的内容

equals()方法最初在Object类中定义的,默认的实现就是使用

    public static void main(String[] args) {
        int a=10;
        int b=10;
        System.out.println(a==b?true:false);



        String s1 = new String("abc");
        String s2= new String("abc");

        System.out.println(s1==s2?true:false);     //false
        System.out.println(s1.equals(s2));         //true


        String s3 = "abc";
        String s4 = "abc";

        System.out.println(s3==s4?true:false);        //true
        System.out.println(s3.equals(s4)?true:false);   //true

    }

1.4 接口和抽象类有什么共同点和区别?

区别

  • ​ 抽像类使用abstract进行修饰,接口使用interface来声明

  • ​ 抽像类中有构造方法,接口中没有构造方法

  • ​ 抽像类中可以没有抽像方法,但有抽像方法的类一定是抽像类。接口中的方法必须全部为抽像方法

  • ​ 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值。

  • 一个类只能继承一个类,但是可以实现多个接口。

  • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为,接口是一种规范和约束,强调的是have …a。抽象类主要用于代码复用,强调的是所属关系,强调的是is…a。

public interface MyInterface{

	public static final int a=10;

    public abstract void methodA();
    
     void methodB();
}
public abstrace class MyClass{

	int a=10;
	
	public abstract void methodA();
	
	public void methodB(){
		.....
	}
		
}

1.5、String a = new String(“abc“); 创建了几个对象?String a = “abc“; 呢?

String a = new String(“abc”); 创建了几个对象?String a = “abc”; 呢?
答案:String a = new String(“abc”); 创建了1个或2个对象;String a = “abc”; 创建了0个或1个都对象

String a = new String(“abc”); 创建过程

首先在堆中创建一个实例对象new String, 并让a引用指向该对象。(创建第1个对象)
JVM拿字面量"abc"去字符串常量池试图获取其对应String对象的引用。
若存在,则让堆中创建好的实例对象new String引用字符串常量池中"abc"。(只创建1个对象的情况)
若不存在,则在堆中创建了一个"abc"的String对象,并将其引用保存到字符串常量池中,然后让实例对象new String引用字符串常量池中"abc"(创建2个对象的情况)
String a = “abc”; 创建过程

首先JVM会在字符串常量池中查找是否存在内容为"abc"字符串对应String对象的引用。
若不存在,则在堆中创建了一个"abc"的String对象,并将其引用保存到字符串常量池中。(创建1个对象的情况)
若存在,则直接让a引用字符串常量池中"abc"。(创建0个对象的情况)

1.6、String、StringBuilder与StringBuffer不同点

String的值是不可变的,这就导致每次对String的操作都会生成新的String对象,不仅效率低下,而且浪费大量优先的内存空间

StringBuffer是可变类,和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。

与StringBuffer一样,StringBuilder也是可变类,不同之处它是线程非安全的。速度更快。

1.7、为什么重写 equals 方法一定要重写 hashCode 方法?

一、为什么重写equals方法一定要重写hashCode方法?
1、在使用了散列表数据结构的集合中(HashMap, HashSet, HashTable)
在存取元素时先判断取到key对象的hashCode,然后跟集合容量取余后得到具体的位置,如果该位置上已经有值,再通过equals方法 判断是否相等,相等则为同一个元素,否则为不同元素。
2、第一点总结:为了能在散列表结构的集合中正常使用,那么判断两个对象是否相等,应该符合下面两个条件
1)hashCode相等,equals不一定相等。
2)equals相等,hashCode一定相等。

二、不重写hashCode方法会导致的问题?

​ 1、根据本例重写了equals方法,不重写hashCode方法(注释掉重写的hashCode方法)
​ 结果:equals结果为true,hashCode结果不相等
​ 2、根据本例重写equals方法,重写hashCode方法

总结:只有重写了equals方法必须重写hashCode方法,只有这样这个类创建的对象才能在散列表结构的集合中正常工作

public class User {
 
    private String name;
    private Integer age;
 
    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        User user = (User) o;
        return name.equals(user.name) &&
                age.equals(user.age);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
 
}
 
class Demo {
 
    public static void main(String[] args) {
        User whf = new User("whf", 26);
        User lvq = new User("whf", 26);
        System.out.println("equlas结果为:" + whf.equals(lvq));
        System.out.println("hashCode结果为:" + (whf.hashCode() == lvq.hashCode()));
        System.out.println(whf.hashCode());
        System.out.println(lvq.hashCode());
    }
}

1.8、方法重载(Overload)与重写(Overriding)的区别

**方法重载:**在一个类中方法名称相同,参数列表不同。

/*** Overloading(重载,过载)* 方法名相同,参数类型不同或者参数类型顺序不同* 返回值,访问修饰符,异常可以不一样*/public class Overloading {    
	public int test(){        
		System.out.println("test1");        
		return 1;    
	}     
	
	public void test(int a){
    	System.out.println("test2");    
    }        
    
    //以下两个参数类型顺序不同    
    public String test(int a,String s){
    	System.out.println("test3");
        return "returntest3";    
    }
    
    public String test(String s,int a){
    	System.out.println("test4");        
    	return "returntest4";    
    }
    
    public static void main(String[] args){
    	Overloading o = new Overloading();
        System.out.println(o.test());
        o.test(1);
        System.out.println(o.test(1,"test3"));
        System.out.println(o.test("test4",1));    
    }
}

**重写(Overriding):**在继承的情况下,子类对父类的方法重新进行定义。

/**
* Overriding(重写,覆盖)
* 重写是子类继承父类对父类的方法进行修改。方法名,参数,返回值必须一样。
* 访问级别的限制性和异常不能比被重写的方法强
*/
class TestClass {
    public void test(){
        System.out.println("这是TestClass的test方法");
    }
}
 
public class Overriding extends TestClass {
 
    public static void main(String[] args) {
        new Overriding().test();//使用子类中重写的test方法
    }
 
    @Override
    public void test() {
        System.out.println("这是Overriding的test方法,重写了TestClass中的方法");
    }
}

1.9、什么是多态机制?Java语言是如何实现多态的?

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程 时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运 行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现 上,让程序可以选择多个运行状态,这就是多态性。

子代父类实例化。通常在设计方法参数时会使用父类型,但在调用时传的是子类的对象进来的。只有程序在运行时才知道它具体的类型。

1.10、final 有什么用?

被final修饰的类不可以被继承

被final修饰的方法不可以被重写

被final修饰的变量必须赋初始值,并且不可以被改变,

被final修饰不可变的是变量的引用,而不是引用指向的内容, 引用指向的内容是可以改变的

1.11、抽象类能使用 final 修饰吗?

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

1.12、&和&&的区别

&运算符有两种用法:(1)按位与;(2)逻辑与。

&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的,虽然二者都要求运算符左右两端 的布尔值都是true 整个表达式的值才是 true。

&&之所以称为短路运算,是因为如果&&左边的表 达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。

注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。

1.13、final finally finalize区别

final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、 修饰变量表 示该变量是一个常量不能被重新赋值。

finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法 finally代码块 中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代 码。

finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾 回收器来调 用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一 个对象是否可回收的 最后判断。

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

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

1.15、什么是反射机制?

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

        //方式一:
         Class clz =Person.class;
        //方式二:
         Class clz = Class.forName("java.util.ArrayList");
        //方式三:
        Person p = new Person();
        Class clz= p.getClass();


		//1、获取方法
        Method say1 = clz.getMethod("say", null);
        Method say = clz.getMethod("say", String.class);
        Method priavateSay = clz.getDeclaredMethod("priavateSay", null);   //获取私有方法
		priavateSay.setAccessible(true);  //暴力访问

		say1.getAnnoa
		
     

        //2、获取成员变量
        Field name = clz.getDeclaredField("name");
        name.set(clz.newInstance(),"dabao");

        //3、获取注解
        Demo2_3 annotation = priavateSay.getAnnotation(Demo2_3.class);
        int age = annotation.age();
        String value = annotation.value();


        //4、获取构造方法
        Constructor constructor = clz.getConstructor(null);
        Object teacher = constructor.newInstance();

1.16、Java集合框架

image-20230610224500318

Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。

Collection集合主要有List和Set两大接口

• List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

• Set:不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

• Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

Java集合框架中有哪些是线和安全的,哪些是线程非安全的

List:

​ 安全: vector 、CopyOnWriteArrayList、 可以通过Collections将ArrayList转换成线程安全的集合

​ 不安全: ArrayList LinkedList

Set:

​ 不安全:Hashset TreeSet

​ 安全:可以通过Collections将Set转换成线程安全的集合

Map:

​ 不安全:HashMap

​ 安全: HashTable、concurrentHashMap(CAS+sychronized)、可以通过Collections将HashMap转换成线程安全的集合

Collection与Collections的区别

Collection是一个集合接口,它的子接口包括List和Set

Collections它是一个工具类:

Collections.reverse();	//对集合元素降序排序
Collections.copy();     //拷贝一个集合中的元素到另一个集合中
Collections.sort();     //对集合进行排序
Collections.sychronized...		//将非安全的集合转换成线程安全集合
Collections.singtonList()

1.17、HashMap底层的数据结构是什么?

JDK1.8之前采用的是数组+链表来实现的。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。(存储的是entry对象)

image-20221204224505125

HashMap JDK1.8之后(存储的是Node对象)

  • 相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
image-20221204224516143

临界值threshold 就是由加载因子和当前容器的容量大小来确定的。临界值(threshold )= 默认容量(DEFAULT_INITIAL_CAPACITY) * 默认扩容因子(DEFAULT_LOAD_FACTOR)。比如容量是16,负载因子是0.75,那就是大于 16x0.75=12 时,就会触发扩容操作,扩容之后的长度是原来的二倍。容量都是2的幂次方。

1.19、HashMap的put操作流程是什么样的?

​ HashMap map = new HashMap();
​ map.put(“aa”,“bb”);

0

  1. 首先对key的哈希值通过扰动方法,获取一个新的哈希值。扰动函数: (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  2. 判断tab是否为空或者长度为0,如果是则进行扩容操作。
if ((tab = table) == null || (n = tab.length) == 0) 
    n = (tab = resize()).length;

  1. 根据哈希值计算数组桶下标,如果对应下标正好没有存放数据,则直接插入,否则说明hash冲突了,这时候要遍历链表或者红黑树进行插入或者覆盖。 计算下标的方法:tab[i = (n - 1) & hash])
  2. 判断tab[i]是否为树节点,否则向链表中插入数据,是则向树中插入节点。
  3. 如果链表中插入节点的时候,链表长度大于等于8,则需要把链表转换为红黑树。 转换树的方法:treeifyBin(tab, hash)
  4. 最后所有元素处理完成后,判断是否超过阈值; threshold ,超过则扩容。

1.18 为什么扩容2的n次幂?

https://blog.csdn.net/qq_41810415/article/details/128438687

当我们根据key的hash确定其在数组的位置时,如果n为2的幂次方,可以保证数据的均匀插入,如果n不是2的幂次方,可能数组的一些位置永远不会插入数据,浪费数组的空间,加大hash冲突。
另一方面,一般我们可能会想通过 % 求余来确定位置,这样也可以,只不过性能不如 & 运算。而且当n是2的幂次方时:hash & (length - 1) == hash % length

因此,HashMap 容量为2次幂的原因,就是为了数据的的均匀分布,减少hash冲突,毕竟hash冲突越大,代表数组中一个链的长度越大,这样的话会降低hashmap的性能。

1.20、什么时候HashMap链表转红黑树呢?

树化发生在table数组的长度大于64,且链表的长度大于8的时候。

1.21、有什么办法能解决HashMap线程不安全的问题呢?

Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。

  • HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个table数组,粒度比较大,不推荐;
  • Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现;
  • ConcurrentHashMap 在jdk1.7中使用分段锁,在jdk1.8中使用CAS+synchronized。

1.22、HashMap、HashTable、ConcurrentHashMap的区别

HashMap和Hashtable的区别。

HashMap线程不安全的,Hashtable是线程安全的(方法上使用了synchronized关键字)

HashMap允许键值对都可以为null, 而Hashtable不允许键值对为null

HashMap由数组+链表/红黑树,而Hashtable依然使用数组+链表

Hashtable与ConcurrentHashMap的区别。

Hashtable在进行元素操作的时候,是锁住整个table,而jdk1.7中的ConcurrentHashMap是采用分段的分式,锁住某个段,而jdk1.8之后去除了“分段锁”,而是改成了局部同步代码块,且是锁住的是某个Node节点,这样其他槽里的节点依然可以并发进行操作。

Hashtable和ConcurrentHashMap一样不允许键值对为null

1.23、ArrayList和LinkedList 的区别是什么?

数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

• 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。·

• 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

• 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

• 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList。

• LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。

1.24、ArrayList是线程安全的吗?有哪几种实现ArrayList线程安全的方法?

ArrayList不是线程安全的。保证ArrayList的线程安全可以通过这些方案:

  • 使用 Vector 代替 ArrayList。(不推荐,Vector是一个历史遗留类)

  • 使用 Collections.synchronizedList 包装 ArrayList,然后操作包装后的 list。

  • 使用 CopyOnWriteArrayList 代替 ArrayList。只适合读多写少的情况

1.25、CopyOnWriteArrayList是怎么实现的呢?

CopyOnWriteArrayList就是线程安全版本的ArrayList。cow

CopyOnWriteArrayList采用了一种读写分离的并发策略。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

如何实现对集合中的数据排序

//集合元素排序
List<Integer> list = new ArrayList();
list.add(10);
list.add(7);
list.add(9);
list.add(2);
list.add(5);
//⽅式⼀ : 使⽤ stream 流中的sorted⽅法
List<Integer> result1 = list.stream().sorted().collect(Collectors.toList());
System.out.println(result1);

List<Integer> result2 = list.stream().sorted(((o1, o2) -> o1 - o2)).collect(Collectors.toList());
System.out.println(result2);


//⽅式⼆ :
list.sort((o1, o2) -> o1 - o2);
System.out.println(list);


//⽅式三 : 转化为有序集合 , 例如 TreeSet 集合 , 传⼊⽐较器接⼝
TreeSet<Integer> set = new TreeSet<>(Comparator.naturalOrder());
set.addAll(list);
System.out.println(set);

//⽅式四 : ⾃⼰写算法排序 , 例如 : 冒泡 , 快排, 希尔排序

对集合数据进⾏遍历的⽅式有哪些

//集合元素遍历
List<Integer> list = new ArrayList();
list.add(10);
list.add(7);
list.add(9);
list.add(2);
list.add(5);
//⽅式⼀ : 普通for循环
for (int i = 0; i < list.size(); i++) {
 System.out.println(list.get(i));
}
//⽅式⼆ : 增强for循环
for (Integer integer : list) {
 System.out.println(integer);
}
//⽅式三 : 迭代器遍历
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
 System.out.println(iterator.next());
}
//⽅式四 : stream流遍历
list.stream().forEach(num-> System.out.println(num));

增强for循环和Iterator有什么区别

增强for循环底层就是使⽤的迭代器实现的 , 所以除了书写形式⼏乎没有什么区别

迭代器是⽤于⽅便集合遍历的,实现了Iterable接⼝的集合都可以使⽤迭代器来遍历

增强for循环,内部使⽤的是迭代器,所以它的操作对象是数组和可以使⽤迭代器的集合

需要注意的是使⽤迭代器遍历元素时,除了查看之外,只能做remove操作。

在使⽤增强for循环遍历的过程中是否可以删除元素

使⽤增强的for循环, 不能对元素进⾏删除,报出ConcurrentModifictionExecption迭代器内部的每次遍历都会记录List内部的modcount当做预期值,然后在每次循环中⽤预期值与List的

成员变量modCount作⽐较,但是普通list.remove调⽤的是List的remove,这时 modcount++ ,但是iterator内记录的预期值=并没有变化,所以会报错如果需要再遍历的过程中删除元素 , 可以使⽤ Iterable 迭代器进⾏遍历 , 使⽤迭代器的remove⽅法进⾏删除

1.26、创建线程有几种方式?

Java中创建线程主要有以下这几种方式:

  • 定义Thread类的子类,并重写该类的run方法
  • 定义Runnable接口的实现类,并重写该接口的run()方法
  • 定义Callable接口的实现类,并重写该接口的call()方法,一般配合Future使用
  • 线程池的方式

1.27、启动线程怎么启动?

start()方法被用来启动新创建的线程

1.28、线程有哪些状态?

线程有6个状态,分别是:New(新建), Runnable(就绪),Running(运行中), Blocked(阻塞), Waiting(等待), Timed_Waiting(超时等待), Terminated(终止)

转换关系图如下:

image-20230608174023607

1.30、synchronized和ReentrantLock的区别?

  1. 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
  2. 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
  3. synchronized 是一个Java内置的关键字,Lock是一个接口
  4. 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
  5. 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
  6. 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS使用Java代码来 实现的。
  7. 两者都是属于可重入锁

1.31、AQS

AbstractQueuedSynchronizer(AQS)抽像队列同步器。提供了一套可用于实现锁同步机制的框架

AQS的原理并不复杂,AQS维护了一个volatile int state变量和一个CLH(三个人名缩写)双向队列,队列中的节点持有线程引用,每个节点均可通过getState()setState()compareAndSetState()state进行修改和访问。·

image-20230722225106292

当线程获取锁时,即试图对state变量做修改,如修改成功则获取锁;如修改失败则包装为节点挂载到队列中,等待持有锁的线程释放锁并唤醒队列中的节点。

1.31、volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

image-20231016213437868

2)禁止进行指令重排序。

示例:volatile解决可见性问题

public class VolatileDemo_1 {

    volatile static boolean flag= false;   //解决了可见性和有序性

    public static void main(String[] args) {

        new Thread(new Runnable() {
            @Override
            public void run() {
                while(!flag){

                }
                System.out.println(Thread.currentThread().getName()+"结束了");
            }
        },"t1").start();



        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                flag=true;
                System.out.println(Thread.currentThread().getName()+"修改了flag");
            }
        },"t2").start();


    }
}

1.32 Semaphore信号量

Semaphore管理着一组许可(permit),许可的初始数量可以通过构造函数设定,操作时首先要获取到许可,才能进行操作,操作完成后需要释放许可。如果没有获取许可,则阻塞到有许可被释放

acquire();   //获取许可,如果获取到线程继续执行,否则阻塞,获取完后许可数量会减1
release();   //给信号量添加许可。

示例:使用信号量实现三个线程交替执行

public class Semaphore_demo2 {

    static Semaphore lock1 = new Semaphore(1);
    static Semaphore lock2 = new Semaphore(0);
    static Semaphore lock3 = new Semaphore(0);
    public static void main(String[] args)throws Exception {

        new Thread(()->{
            while (true){
                try {
                    lock1.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("A");
                lock2.release();
            }
        }).start();


        new Thread(()->{
            while (true){
                try {
                    lock2.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("B");
                lock3.release();
            }
        }).start();


        new Thread(()->{
            while (true){
                try {
                    lock3.acquire();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("C");
                lock1.release();
            }
        }).start();
    }
}

1.33、CountDownLatch概念

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

countDown(); //执行一次会将CountDownLatch里面的值减1

await();  //只有CountDownLatch里面的值为0时才执行

package com.heima.thread;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;

public class Semaphore_demo1 {


    /**
     * 所有子线程运行完后,主线程才能输出
     * @param args
     */
    public static void main(String[] args)throws Exception {

        CountDownLatch countDownLatch = new CountDownLatch(3);


        new Thread(new Runnable() {
            @Override
            public void run() {

                    countDownLatch.countDown();          //每执行一次countDownLatch里面的值减1

                System.out.println("第1个线程开始工作。。。");
            }
        }).start();

    new Thread(new Runnable() {
            @Override
            public void run() {
                countDownLatch.countDown();         //每执行一次countDownLatch里面的值减1
                System.out.println("第2个线程开始工作。。。");
            }
        }).start();

     new Thread(new Runnable() {
            @Override
            public void run() {
                countDownLatch.countDown();          //每执行一次countDownLatch里面的值减1
                System.out.println("第3个线程开始工作。。。");
            }
        }).start();


        countDownLatch.await();
        System.out.println("我是主线程");

    }
}


1.31 Object中有哪些方法

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

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

  1. 对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类 中的。
  2. sleep()可以指定超时时间,到达时间后自动恢复,wait()方法必须要调用notify()或notifyall()方法才能唤醒
  3. wait方法在使用时必须加锁,sleep()可以加锁也可以不加锁
  4. wati方法调用完后锁会释放,sleep()则不会释放锁。

代码演示:三个线程按顺序执行

package com.itheima;

public class WaitNotifyDemo_1 {



   static boolean t1End=false;
    static boolean t2End=false;
    static Object object = new Object();
    
    static Student student = new Student();
    /**
     * 三个线程分别调用同一个Student对象的三个方法,要求按顺序输出
     * @param args
     */
    public static void main(String[] args) {



        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object){

                    System.out.println(1);
                    t1End=true;
                    object.notifyAll();
                }

            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public  void run() {
                synchronized (object){
                    while(!t1End){
                        try {

                            object.wait();  //当前线程等待,并释放锁

                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    t2End=true;
                     System.out.println(2);
                    object.notify();

                }


            }
        });


        Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {

                synchronized (object){
                    while(!t2End){
                        try {
                            object.wait();  //当前线程等待,并
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }

                     System.out.println(3);
                }

            }
        });


        t1.start();
        t2.start();
        t3.start();

    }

}


1.33、什么是乐观锁什么是悲观锁

悲观锁: 就是默认认为在本线程使用该系统资源的时候就一定会有别的线程来进行争抢,就默认加锁。适合写多读少的场景,先加锁可以保证写操作是数据正确

乐观锁: 认为自己再使用数据的时候不会有别的线程来修改数据或资源,所以不会加锁。只是在更新数据的时候去判断一下有没有别的线程更新了这个数据,如果这个数据没有被更新,就将当前线程的数据写入,如果有更新,则根据不同的实现方式来执行不同的操作,比如放弃修改,重试抢锁等,适合读多写少的情况

使用乐观锁时如何避免ABA,需要引入version机制

判断规则:

  • 版本号机制version
  • 最常采用的是CAS算法,Java原子类中的递增操作就是通过CAS自旋实现

1.34、什么是公平锁?什么是非公平锁?

什么是公平锁?

  • 公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
  • 优点:所有的线程都能得到资源,不会饿死在队列中。
  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

什么是非公平锁?

  • 非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
  • 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

1.35、sychronized锁的升级流程

image-20230722223741433

1、当没有被当做锁的时候,这就是个普通对象,锁标志位为01,是否偏向锁为0

2、当对象被当做同步锁时,一个线程A抢到锁时,锁标志位依然是01,是否偏向锁为1,前23位记录A线程的线程ID,此时锁升级为偏向锁

3、当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码,这也是偏向锁的意义

4、当一个线程B尝试获取锁,JVM发现当前的锁处于偏向状态,并且现场ID不是B线程的ID,那么线程B会先用CAS将线程id改为自己的,如果失败,则执行5

5、偏向锁抢锁失败,则说明当前锁存在一定的竞争,偏向锁就升级为轻量级锁。JVM会在当前线程的现场栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁MarkWord中保存指向这片空间的指针。上面的保存都是CAS操作,如果竞争成功,代表线程B抢到了锁,可以执行同步代码。如果抢锁失败,则继续执行6

6、轻量级锁抢锁失败,则JVM会使用自旋锁,自旋锁并非是一个锁,则是一个循环操作,不断的尝试获取锁。从JDK1.7开始,自旋锁默认开启,自旋次数由JVM决定。如果抢锁成功,则执行同步代码;如果抢锁失败,则执行7

7、自旋锁重试之后仍然未抢到锁,同步锁会升级至重量级锁,锁标志位改为10,在这个状态下,未抢到锁的线程都会被阻塞,由Monitor来管理,并会有线程的park与unpark,因为这个存在用户态和内核态的转换,比较消耗资源,故名重量级锁

无锁—》偏向锁—》轻量锁—》自旋锁–》重量锁

1.29、线程之间的通信有几种方式,分别是怎么实现的?

http://www.imodou.com.cn/article/Java%E7%BA%BF%E7%A8%8B%E9%97%B4%E7%9A%84%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F.html

使用信号量wait/notify实现线程交替执行。

1.36、如何检测死锁?怎么预防死锁?死锁四个必要条件

死锁是指多个线程因竞争资源而造成的一种互相等待的僵局。如图感受一下:

image-20230509184928029

死锁的四个必要条件:

  • 互斥条件(Mutual exclusion):至少有一个资源被持有,且在任意时刻只有一个进程能够使用该资源。
  • 请求与保持条件(Hold and wait):进程已经持有至少一个资源,并且在等待获取其他进程持有的资源。
  • 不剥夺条件(Non-preemption):进程已经获得的资源在未使用完之前不能被剥夺,只能自愿释放。
  • 循环等待条件(Circular wait):进程之间形成一种头尾相接的循环等待资源关系。。

互斥条件,很显然,我们使用的是 synchronized 互斥锁,它的锁对象 o1、o2 只能同时被一个线程所获得,所以是满足互斥条件的。

请求与保持条件,可以看到,同样是满足的。比如,线程 1 在获得 o1 这把锁之后想去尝试获取 o2 这把锁 ,这时它被阻塞了,但是它并不会自动去释放 o1 这把锁,而是对已获得的资源保持不放。

不剥夺条件,在我们这个代码程序中,JVM 并不会主动把某一个线程所持有的锁剥夺,所以也满足不剥夺条件。

循环等待条件,可以看到在我们的例子中,这两个线程都想获取对方已持有的资源,也就是说线程 1 持有 o1 去等待 o2,而线程 2 则是持有 o2 去等待 o1,这是一个环路,此时就形成了一个循环等待。

public class DeadlockDemo {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: Waiting for lock 2");
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 & 2");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 2: Waiting for lock 1");
                synchronized (lock1) {
                    System.out.println("Thread 2: Holding lock 1 & 2");
                }
            }
        }).start();
    }
}


1.37、线程池的核心参数

我们先来看看ThreadPoolExecutor的构造函数

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
   long keepAliveTime,
   TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler) 

  • corePoolSize:线程池核心线程数最大值
  • maximumPoolSize:线程池最大线程数大小
  • keepAliveTime:线程池中非核心线程空闲的存活时间大小
  • unit:线程空闲存活时间单位
  • workQueue:存放任务的阻塞队列
  • threadFactory:用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
  • handler:线城池的饱和策略事件,主要有四种类型拒绝策略。

四种拒绝策略

  • AbortPolicy(抛出一个异常,默认的)
  • DiscardPolicy(直接丢弃任务)
  • DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
  • CallerRunsPolicy(交给线程池调用所在的线程进行处理)

几种工作阻塞队列

  • ArrayBlockingQueue(用数组实现的有界阻塞队列,按FIFO排序量)
  • LinkedBlockingQueue(基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列)
  • DelayQueue(一个任务定时周期的延迟执行的队列)
  • PriorityBlockingQueue(具有优先级的无界阻塞队列)
  • SynchronousQueue(一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态)

1.38、Java的线程池执行原理

线程池的执行原理如下:

image-20230608174420868

  1. 提交任务后,先判断当前池中线程数是否小于 corePoolSize,如果小于,则创建新线程执行这个任务。
  2. 否则,判断线程池任务队列是否已满,如果没有满,则添加任务到任务队列。
  3. 否则,判断当前池中线程数是否大于 ,如果大于则执行预设拒绝策略。
  4. 否则,创建一个线程执行该任务,直至线程数达到maximumPoolSize,达到后执行预设拒绝策略

1.39、线程池有几种创建方式?

总体来说线程池的创建可以分为以下两类:

  • 通过 ThreadPoolExecutor 手动创建线程池
  • 通过 Executors 执行器自动创建线程池。

而以上两类创建线程池的方式,又有 7 种具体实现方法,这 7 种实现方法分别是:

Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
Executors.newSingleThreadPool:创建单个线程数的线程池,它可以保证先进先出的执行顺序。
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池。
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池。
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。

ThreadPoolExecutor:手动创建线程池的方式,它创建时最多可以设置 7 个参数。

1.40、为什么阿里发布的 Java开发手册中强制线程池不允许使用 Executors 去创建?

这是因为,JDK开发者提供了线程池的实现类都是有坑的,如newFixedThreadPoolnewCachedThreadPool都有内存溢出的坑。也可能导致CPU飙高

1.41、聊聊ThreadLocal原理?

ThreadLocal的内存结构图

为了对ThreadLocal有个宏观的认识,我们先来看下ThreadLocal的内存结构图

img

从内存结构图,我们可以看到:

  • Thread类中,有个ThreadLocal.ThreadLocalMap 的成员变量。
  • ThreadLocalMap内部维护了Entry数组,每个Entry代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型对象值。

关键源码分析

对照着关键源码来看,更容易理解一点哈~

首先看下Thread类的源码,可以看到成员变量ThreadLocalMap的初始值是为null

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;
}

成员变量ThreadLocalMap的关键源码如下:

static class ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //Entry数组
    private Entry[] table;
    
    // ThreadLocalMap的构造器,ThreadLocal作为key
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
}

ThreadLocal类中的关键set()方法:

 public void set(T value) {
        Thread t = Thread.currentThread(); //获取当前线程t
        ThreadLocalMap map = getMap(t);  //根据当前线程获取到ThreadLocalMap
        if (map != null)  //如果获取的ThreadLocalMap对象不为空
            map.set(this, value); //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //创建一个新的ThreadLocalMap
    }
    
     ThreadLocalMap getMap(Thread t) {
       return t.threadLocals; //返回Thread对象的ThreadLocalMap属性
    }

    void createMap(Thread t, T firstValue) { //调用ThreadLocalMap的构造函数
        t.threadLocals = new ThreadLocalMap(this, firstValue); this表示当前类ThreadLocal
    }
    

ThreadLocal类中的关键get()方法

    public T get() {
        Thread t = Thread.currentThread();//获取当前线程t
        ThreadLocalMap map = getMap(t);//根据当前线程获取到ThreadLocalMap
        if (map != null) { //如果获取的ThreadLocalMap对象不为空
            //由this(即ThreadLoca对象)得到对应的Value,即ThreadLocal的泛型值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value; 
                return result;
            }
        }
        return setInitialValue(); //初始化threadLocals成员变量的值
    }
     
     private T setInitialValue() {
        T value = initialValue(); //初始化value的值
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); //以当前线程为key,获取threadLocals成员变量,它是一个ThreadLocalMap
        if (map != null)
            map.set(this, value);  //K,V设置到ThreadLocalMap中
        else
            createMap(t, value); //实例化threadLocals成员变量
        return value;
    }

所以怎么回答ThreadLocal的实现原理?如下,最好是能结合以上结构图一起说明哈~

  • Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于自己的ThreadLocalMap
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型值。
  • 并发多线程场景下,每个线程Thread,在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而可以实现了线程隔离

1.42、类加载器

image-20230713104234856
  • 启动类加载器:负责加载$JAVA_HOME\lib下的类或者被参数-Xbootclasspath指定的能够被虚拟机识别的类(通过jar名字识别,如rt.jar),启动类加载器由java虚拟机直接控制,开发者不能直接使用启动类加载器。

  • 扩展类加载器:负责加载$JAVA_HOME\lib\ext下的类或者被java.ext.dirs系统变量指定的路径中所有类库,开发者可以直接使用这个类加载器。

  • 应用程序类加载器:负责加载$CLASS_PATH中指定的类库,开发者能直接使用这个类加载器,正常情况下如果我们在应用程序中没有自定义类加载器,一般用的就是这个类加载器。

  • 自定义类加载器:如果需要可以通过java.lang.ClassLoader的子类来定义自己的类加载器,一般我们选择继承URLClassLoader来进行适当改写就行了。

1.43、双亲委派机制

当一个类收到了加载请求时,它是不会先自己去尝试加载的,而是委派给上层的类加载器去完成,比如我现在要new一个Person,这个Person是我们自定义的类,如果我们要加载它,就会先委派App ClassLoader,只有当父类加载器都反馈自己无法完成这个请求(也就是父类加载器都没有找到加载所需的Class)时,App ClassLoader加载器才会自行尝试加载

这样做的好处是,加载位于rt.jar包中的类时不管是哪个加载器加载,最终都会委托到BootStrap ClassLoader进行加载,这样保证了使用不同的类加载器得到的都是同一个结果。

其实这个也是一个隔离的作用,避免了我们的代码影响了JDK的代码,比如我现在要来一个

public class String{
    public static void main(){sout;}
}

这种时候,我们的代码肯定会报错,因为在加载的时候其实是找到了rt.jar中的String.class,然后发现这也没有main方法

1.43、运行时数据区域

image-20230609110455423 image-20230609110535363

**线程私有的:**程序计数器、虚拟机栈、本地方法栈

**线程共享的:**堆、方法区、直接内存 (非运行时数据区的一部分)

程序计数器:】

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

⚠️ 注意 :程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

栈:

​ 与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

​ 方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中每一个方法调用结束后,都会有一个栈帧被弹出。

​ 栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

本地方法栈:

​ 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

堆:

Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

方法区:

方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

总结:

线程私有:栈、本地方法栈、程序计数器

线程公有:方法区 堆

除了程序计数器以外,所有的区域都有可能产生内存溢出。

1.44 单例模式

package com.heima.thread;

public class SingleInstance_demo {



    private static volatile SingleInstance_demo singleInstanceDemo = null;   //避免指令重排


    //1、私有构造方法
    private SingleInstance_demo(){

    }

    //2、提供一个静态用于获取对象的方法
    public  static SingleInstance_demo getInstance(){


       if(singleInstanceDemo==null){
           synchronized(SingleInstance_demo.class) {
               if(singleInstanceDemo==null){
                   singleInstanceDemo = new SingleInstance_demo();   //1、创建对象    //2、对象中的属性进行初始化  //3、将引用指向对象
               }

           }

       }

        return singleInstanceDemo;

    }
}


二、JVM

2.1 Jvm内存结构

image-20230712224057349

程序计数器

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存

⚠️ 程序计数器特点 :

  • 程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
  • 线程私有

Java 虚拟机栈

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表操作数栈动态链接方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误

⚠️ 栈特点 :

  • StackOverFlowError 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • 线程私有

本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务

堆是java虚拟机管理内存最大的一块,在虚拟机启动时创建,所有线程共享,堆中的对象永远不会被显式释放,必须由GC回收,所以GC也主要回收堆中的对象实例,我们平常讨论的垃圾回收就是回收堆内存。堆可以处于物理上不连续的空间,可以固定大小,也可以动态扩展,通过参数-Xms和-Xmx两个参数控制堆的最小值和最大值。

⚠️ 堆特点 :

  • OutOfMemoryError 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
  • 线程共享

方法区

方法区也是线程共享的区域,在虚拟机启动时创建,存储每个类的结构,比如:运行时常量池、属性和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化使用的特殊方法,方法区在逻辑上是堆的一部分,但是它又有另一个别名叫非堆,目的是与堆区分开。方法区可以是固定大小,也可以根据计算需要进行扩展。。

⚠️ 堆特点 :

  • **OutOfMemoryError:**如果方法区的内存无法满足分配请求时也会抛出OutOfMemoryError
  • 线程共享

哪些是线程私有的:程序计数器、栈

哪有是线程公有的:方法区、堆

哪些不会内存溢出:程序计数器

2.2、堆内存结构

Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。

下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。

image-20230724181711952

执行流程:

对象来了,首先在Eden区分配内存,Eden满了后触发GC,GC后把活下来的对象赋值到S0区(S1区是空的),然后继续在Eden区分配对象,再次触发GC之后如果发现S0装不下(产生空间碎片,实际还有空间),那么就把S0活下来的对象复制到S1,这时候S0区就空了,并依次反复操作。假如说S0区和S1区空间对象复制移动之后还是放不下,就说明真的满了,那么就去老年代借点空间(这就是担保机制,老年代需要提供这种空间分配担保),假如老年代空间也不够了,就会触发Full GC,如果还是不够,那就抛出OutOfMemoryError。

2.3、垃圾回收算法

标记-清除算法

回收前

image-20230127221138228 1、将堆内存扫描一遍,然后会把灰色区域对象标记一下 2、继续扫描,扫描的同时将被标记的对象统一回收

回收后:

image-20230127221148106 可以看到,回收之后内存空间不连续,产生了内存碎片。 缺点:

  • 标记和清除两个过程效率都不高。
  • 产生大量不连续的内存碎片

标记复制法

复制算法的思想就是把内存区域一分为二,两块内存大小相同,每次只使用其中一块,当其中一块使用完后,将存活的对象复制到另一块,然后一次性清理掉这一块内存。

回收前:

image-20230127221158004 回收后:

image-20230127221208540 复制算法的缺点是牺牲了一半的内存空间,有点浪费。复制算法在JVM中的体现就是:java堆内存做了几次划分,Hot Spot虚拟机中Eden和Survivor的比例是Eden:S0:S1=8:1:1,将Survivor分成了两个区域S0和S1来进行赋值,这种做法是为了弥补原始复制算法直接将一半空间作为空闲空间的浪费。

标记-整理算法

标记-整理算法就是为了老年代而设计的算法,标记-整理算法和标记-清除算法的区别在最后一步,标记-整理不会直接对对象清理,而是进行移动,将存活对象移动到一端,然后清理掉边界以外的对象。 回收前:

image-20230127221216331

回收后:

image-20230127221222830

分代收集算法

目前主流的商业虚拟机都是采用分代收集算法,这种算法就是上面三种算法的结合。新生代采用复制算法,老年代采用标记-整理或标记-清除算法。

2.4、垃圾回收器

垃圾收集器就是垃圾收集算法的具体实现,下面是垃圾收集器的汇总

image-20230724170017909

4.1、Serial和Serial Old收集器

Serial收集器是最基本、最早的收集器,Serial收集器是单一线程,就是在GC的时候STW(Stop The World),暂停所有用户线程,如果GC时间过长,用户可以感到卡顿。Serial Old也是单线程,作用于老年代。

image-20230724170131002

优点:简单高效,拥有很高的单线程收集效率 缺点:需要STW,暂停所有用户线程 算法:Serial采用复制算法,Serial Old采用标记-整理算法

4.2、ParNew收集器

ParNew是Serial的多线程版本,实现了并行收集,原理跟Serial一致(并行指的是多个GC线程并行,但是用户线程还是暂停,并发指的是用户线程和GC线程同时执行)。ParNew默认开启和CPU个数相同的线程数进行回收。

image-20230724170240907

优点:在多CPU时,比Serial的效率高。 缺点:还是需要STW,单CPU时比Serial效率低 算法:复制算法

4.3、 Parallel Scavenge收集器

新生代收集器,也是复制算法,和ParNew一样并行的多线程收集器,更关注系统的吞吐量(吞吐量=(运行用户代码的时间)/(运行用户代码的时间+GC时间))

4.4、Parallel Old收集器

自从Parallel Old出现,就有了Parallel Scavange+Parallel Old的组合,这是JDK1.8使用的,注重吞吐量的一组收集器。

image-20230724171050794

4.5、CMS收集器(已废弃)

CMS基于标记-清除算法实现,容易产生内存碎片

4.6、G1

G1采用了开创性的局部收集的设计思路和以Region为基本单位的内存布局方式,它将Java堆空间划分成多个大小相等的独立区域(Region)

image-20230724175939933

G1将所有Region分为四种类型:Eden、Survivor、Old、Humongous。

认识了G1中的内存规划之后,我们就可以理解为什么它叫做"Garbage First"。所有的垃圾回收,都是基于 region 的。G1根据各个Region回收所获得的空间大小以及回收所需时间等指标在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大(垃圾)的Region,从而可以有计划地避免在整个Java堆中进行全区域的垃圾收集。这也是 “Garbage First” 得名的由来。

G1基于“标记-复制”算法实现。G1运作期间不会产生内存空间碎片

G1 运作步骤

G1 收集器的工作过程分为以下几个步骤:

  • 初始标记(Initial Marking):Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。

    识别一个对象到底是不是垃圾:

    计数器法

    可达性算法 static Student s = new Student();

  • 并发标记(Concurrent Marking):使用一条标记线程与用户线程并发执行。此过程进行可达性分析,速度很慢。

  • 最终标记(Final Marking):Stop The World,使用多条标记线程并发执行。

  • 筛选回收(Live Data Counting and Evacuation):回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。(还会更新Region的统计数据,对各个Region的回收价值和成本进行排序)

三、线上故障排查

在开发过程中,如果遇到JVM问题时,通常都有各种各样的本地可视化工具支持查看。但开发环境中编写出的程序迟早会被部署在生产环境的服务器上,而线上环境偶尔也容易遇到一些突发状况,比如JVM在线上环境往往会出现以下几个问题:

  • ①JVM内存飙高。

  • ③业务线程死锁。

  • ⑤线程阻塞/响应速度变慢。

  • ⑥CPU利用率飙升或100%。

    当程序在线上环境发生故障时,就不比开发环境那样,可以通过可视化工具监控、调试,线上环境往往会“恶劣”很多,那当遇到这类问题时又该如何处理呢?首先在碰到这类故障问题时,得具备良好的排查思路,再建立在理论知识的基础上,通过经验+数据的支持依次分析后加以解决。

三、JVM线上故障问题排查实战

2.1 JVM内存飙高

引起JVM内存飙高的原因有两种:内存溢出和内存泄露

  • 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
  • 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间
2.2.1、Java线上内存飙高排查实操

模拟案例如下:

// JVM启动参数:-Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError 
// -XX:HeapDumpPath=/Heap_OOM.hprof
public class OOM {
    // 测试内存溢出的对象类
    public static class OomObject{}
    
    public static void main(String[] args){
        List<OomObject> OOMlist = new ArrayList<>();
        // 死循环:反复往集合中添加对象实例
        for(;;){
            OOMlist.add(new OomObject());
        }
    }
}

在Linux上,先以后台运行的方式启动上述的Java程序:

root@localhost ~]# java com.heima.jvm.OOM


使用jps查看当前OOM的进程号

[root@192 ~]# jps

得到如下结果:

4225 Jps
2761 OOM

新开启一个FinalShell会话,然后使用jmap命令获取当前OOM的堆内存快照【关于jmap命令使用详情参考:http://www.imodou.com.cn/article/JVM%E8%87%AA%E5%B8%A6%E5%86%85%E5%AD%98%E8%B0%83%E4%BC%98%E5%B7%A5%E5%85%B7.html】

[root@192 ~]# jmap -dump:live,format=b,file=dump.hprof 2761

下载dump.hprof文件到windows然后使用jvisualvm工具查看快照文件

image-20230724193920401


线上OOM问题排查思路:

  • ①首先获取Dump文件,最好是上线部署时配置了,这样可以保留第一现场,但如若未配置对应参数,可以调小堆空间,然后重启程序的时候重新配置参数,争取做到“现场”重现。

  • ②如果无法通过配置参数获得程序OOM自然导出的Dump文件,那则可以等待程序在线上运行一段时间,并协调测试人员对各接口进行压测,而后主动式的通过jmap等工具导出堆的Dump文件(这种方式没有程序自动导出的Dump文件效果好)。

    jmap -dump:live,format=b,file=dump.hprof 116690
    
    
  • ③将Dump文件传输到本地,然后通过相关的Dump分析工具分析,如JDK自带的jvisualvm,或第三方的MAT工具等。

  • ④根据分析结果尝试定位问题,先定位问题发生的区域,如:确定是堆外内存还是堆内空间溢出,如果是堆内,是哪个数据区发生了溢出。确定了溢出的区域之后,再分析导致溢出的原因(后面会列出一下常见的OOM原因)。

  • ⑤根据定位到的区域以及原因,做出对应的解决措施,如:优化代码、优化SQL等。

2.2.2、什么情况下产生内存溢出

Java程序在线上出现问题需要排查时,内存溢出问题绝对是“常客”,但通常情况下,OOM大多是因为代码问题导致的,在程序中容易引发OOM的情况:

  • ①一次性从外部将体积过于庞大的数据载入内存,如DB读表、读本地报表文件等。

  • ②程序中使用容器(Map/List/Set等)后未及时清理,内存紧张而GC无法回收。

  • ③程序逻辑中存在死循环或大量循环,或单个循环中产生大量重复的对象实例。

  • ④程序中引入的第三方依赖中存在BUG问题,因此导致内存出现故障问题。

  • ⑤程序中存在内存溢出问题,一直在蚕食可用内存,GC无法回收导致内存溢出。

  • ⑥第三方依赖加载大量类库,元空间无法载入所有类元数据,因而诱发OOM。

  • ⑦…

    上述都是程序内代码引发OOM的几种原因,在线上遇到这类情况时,要做的就是定位问题代码,而后修复代码后重新上线即可。同时,除开代码诱发的OOM情况外,有时因为程序分配的内存过小也会引发OOM,这种情况是最好解决的,重新分配更大的内存空间就能解决问题。

不过Java程序中,堆空间、元空间、栈空间等区域都可能出现OOM问题,其中元空间的溢出大部分原因是由于分配空间不够导致的,当然,也不排除会存在“例外的类库”导致OOM。真正意义上的栈空间OOM在线上几乎很难遇见,所以实际线上环境中,堆空间OOM是最常见的,大部分需要排查OOM问题的时候,几乎都是堆空间发生了溢出。

2.2.3 什么情况下产生内存泄露

1.静态集合类

static HashMap hashMap = new HashMap();

这类对象的生命周期与JVM周期一致,不会被回收 导致他们包含的元素也一直不会被回收。

2.单例模式

与静态集合类原因相似 由于单例模式中对象为静态 ,当他持有外部对象的引用时,外部对象也无法被回收。

2.4、业务线程死锁

死锁是指两个或两个以上的线程(或进程)在运行过程中,因为资源竞争而造成相互等待的现象,若无外力作用则不会解除等待状态,它们之间的执行都将无法继续下去。举个栗子:

某一天竹子和熊猫在森林里捡到一把玩具弓箭,竹子和熊猫都想玩,原本说好一人玩一次的来,但是后面竹子耍赖,想再玩一次,所以就把弓一直拿在自己手上,而本应该轮到熊猫玩的,所以熊猫跑去捡起了竹子前面刚刚射出去的箭,然后跑回来之后便发生了如下状况:
熊猫道:竹子,快把你手里的弓给我,该轮到我玩了…
竹子说:不,你先把你手里的箭给我,我再玩一次就给你…
最终导致熊猫等着竹子的弓,竹子等着熊猫的箭,双方都不肯退步,结果陷入僵局场面…

这个情况在程序中发生时就被称为死锁状况,如果出现后则必须外力介入,然后破坏掉死锁状态后推进程序继续执行。如上述的案例中,此时就必须第三者介入,把“违反约定”的竹子手中的弓拿过去给熊猫,从而打破“僵局”。

3.4.1、线上死锁排查小实战

上个简单例子感受一下死锁情景:

public class DeadLock implements Runnable {
    public boolean flag = true;

    // 静态成员属于class,是所有实例对象可共享的
    private static Object o1 = new Object(), o2 = new Object();

    public DeadLock(boolean flag){
        this.flag = flag;
    }

    @Override
    public void run() {
        if (flag) {
            synchronized (o1) {
                System.out.println("线程:" + Thread.currentThread()
                        .getName() + "持有o1....");
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("线程:" + Thread.currentThread()
                        .getName() + "等待o2....");
                synchronized (o2) {
                    System.out.println("true");
                }
            }
        }
        if (!flag) {
            synchronized (o2) {
                System.out.println("线程:" + Thread.currentThread()
                        .getName() + "持有o2....");
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("线程:" + Thread.currentThread()
                        .getName() + "等待o1....");
                synchronized (o1) {
                    System.out.println("false");
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new DeadLock(true),"T1");
        Thread t2 = new Thread(new DeadLock(false),"T2");
        // 因为线程调度是按时间片切换决定的,
        // 所以先执行哪个线程是不确定的,也就代表着:
        //  后面的t1.run()可能在t2.run()之前运行
        t1.start();
        t2.start();
    }
}

如上是一个简单的死锁案例,在该代码中:

  • flag==true时,先获取对象o1的锁,获取成功之后休眠500ms,而发生这个动作的必然是t1,因为在main方法中,我们将t1任务的flag显式的置为了true
  • 而当t1线程睡眠时,t2线程启动,此时t2任务的flag=false,所以会去获取对象o2的锁资源,然后获取成功之后休眠500ms
  • 此时t1线程睡眠时间结束,t1线程被唤醒后会继续往下执行,然后需要获取o2对象的锁资源,但此时o2已经被t2持有,此时t1会阻塞等待。
  • 而此刻t2线程也从睡眠中被唤醒会继续往下执行,然后需要获取o1对象的锁资源,但此时o1已经被t1持有,此时t2会阻塞等待。
  • 最终导致线程t1、t2相互等待对象的资源,都需要获取对方持有的资源之后才可继续往下执行,最终导致死锁产生。

执行结果如下:

D:\> javac -encoding utf-8 DeadLock.java  
D:\> java DeadLock  
线程:T1持有o1....
线程:T2持有o2....
线程:T2等待o1....
线程:T1等待o2....

在上述案例中,实际上T1永远获取不到o1,而T2永远也获取不到o2,所以此时发生了死锁情况。那假设如果在线上我们并不清楚死锁是发生在那处代码呢?其实可以通过多种方式定位问题:

  • ①通过jps+jstack工具排查。

  • ②通过jconsole工具排查。

  • ③通过jvisualvm工具排查。

    当然你也可以通过其他一些第三方工具排查问题,但前面方式都是JDK自带的工具,不过一般Java程序都是部署在Linux系统上,所以对于后面两种可视化工具则不太方便使用。因此,线上环境中,更多采用的是第一种jps+jstack方式排查。


接下来我们用jps+jstack的方式排查死锁,此时保持原先的cmd/shell窗口不关闭,再新开一个窗口,输入jps指令:

D:\> jps
19552 Jps
2892 DeadLock

jps作用是显示当前系统的Java进程情况及其进程ID,可以从上述结果中看出:ID2892的进程是刚刚前面产生死锁的Java程序,此时我们可以拿着这个ID再通过jstack工具查看该进程的dump日志,如下:

D:\> jstack -l 2892

显示结果如下:

image-20230712143241016

可以从dump日志中明显看出,jstack工具从该进程中检测到了一个死锁问题,是由线程名为T1、T2的线程引起的,而死锁问题的诱发原因可能是DeadLock.java:43、DeadLock.java:27行代码引起的。而到这一步之后其实就已经确定了死锁发生的位置,我们就可以跟进代码继续去排查程序中的问题,优化代码之后就可以确保死锁不再发生。

2.5、CPU利用率居高不下或飙升100%

CPU飙升100%和OOM内存溢出是Java面试中老生常谈的话题,CPU100%倒是个比较简单的线上问题,因为毕竟范围已经确定了,CPU100%就只会发生在程序所在的机器上,因此省去了确定问题范围的步骤,所以只需要在单台机器上定位具体的导致CPU飙升的进程,然后再排查问题加以解决即可。

3.7.1、线上CPU100%排查小实战

模拟的Java案例代码如下:

public class CpuOverload {
    public static void main(String[] args) {
        // 启动十条休眠线程(模拟不活跃的线程)
        for(int i = 1;i <= 10;i++){
            new Thread(()->{
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(10*60*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"InactivityThread-"+i).start();
        }
        
        // 启动一条线程不断循环(模拟导致CPU飙升的线程)
        new Thread(()->{
            int i = 0;
            for (;;) i++;
        },"ActiveThread-Hot").start();
    }
}

首先新建一个shell-SSH窗口,启动该Java应用模拟CPU飙升的情景:

[root@localhost ~]# java com.heima.jvm.CpuOverload

紧接着再在另外一个窗口中,通过top指令查看系统后台的进程状态:

top -H

[root@localhost ~]# top
top - 14:09:20 up 2 days, 16 min,  3 users,  load average: 0.45, 0.15, 0.11
Tasks:  98 total,   1 running,  97 sleeping,   0 stopped,   0 zombie
%Cpu(s):100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :   997956 total,   286560 free,   126120 used,   585276 buff/cache
KiB Swap:  2097148 total,  2096372 free,      776 used.   626532 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 77915 root      20   0 2249432  25708  11592 S 99.9  2.6   0:28.32 java
   636 root      20   0  298936   6188   4836 S  0.3  0.6   3:39.52 vmtoolsd
     1 root      20   0   46032   5956   3492 S  0.0  0.6   0:04.27 systemd
     2 root      20   0       0      0      0 S  0.0  0.0   0:00.07 kthreadd
     3 root      20   0       0      0      0 S  0.0  0.0   0:04.21 ksoftirqd/0
     5 root       0 -20       0      0      0 S  0.0  0.0   0:00.00 kworker/0:0H
     7 root      rt   0       0      0      0 S  0.0  0.0   0:00.00 migration/0
     8 root      20   0       0      0      0 S  0.0  0.0   0:00.00 rcu_bh
     9 root      20   0       0      0      0 S  0.0  0.0   0:11.97 rcu_sched
     .......

从如上结果中不难发现,PID77915的Java进程对CPU的占用率达到99.9%,此时就可以确定,机器的CPU利用率飙升是由于该Java应用引起的。

此时可以再通过top -Hp [PID]命令查看该Java进程中,CPU占用率最高的线程:

[root@localhost ~]# top -Hp 77915
.....省略系统资源相关的信息......
   PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 77935 root      20   0 2249432  26496  11560 R 99.9  2.7   3:43.95 java
 77915 root      20   0 2249432  26496  11560 S  0.0  2.7   0:00.00 java
 77916 root      20   0 2249432  26496  11560 S  0.0  2.7   0:00.08 java
 77917 root      20   0 2249432  26496  11560 S  0.0  2.7   0:00.00 java
 77918 root      20   0 2249432  26496  11560 S  0.0  2.7   0:00.00 java
 77919 root      20   0 2249432  26496  11560 S  0.0  2.7   0:00.00 java
 77920 root      20   0 2249432  26496  11560 S  0.0  2.7   0:00.00 java
 77921 root      20   0 2249432  26496  11560 S  0.0  2.7   0:00.01 java
 .......

top -Hp 77915命令的执行结果中可以看出:其他线程均为休眠状态,并未持有CPU资源,而PID为77935的线程对CPU资源的占用率却高达99.9%

到此时,导致CPU利用率飙升的“罪魁祸首”已经浮现水面,此时先将该线程的PID转换为16进制的值,方便后续好进一步排查日志信息:

[root@localhost ~]# printf %x 77935
1306f

到目前为止,咱们已经初步获得了“罪魁祸首”的编号,而后可以再通过前面分析过的jstack工具查看线程的堆栈信息,并通过刚刚拿到的16进制线程ID在其中搜索:

[root@localhost ~]# jstack 77915 | grep 1306f
"ActiveThread-Hot" #18 prio=5 os_prio=0 tid=0x00007f7444107800
            nid=0x1306f runnable [0x00007f7432ade000]

image-20230725154740190

此时,从线程的执行栈信息中,可以明确看出:ID为1306f的线程,线程名为ActiveThread-Hot。同时,你也可以把线程栈信息导出,然后在日志中查看详细信息,如下:

[root@localhost ~]# jstack 77915 > java_log/thread_stack.log
[root@localhost ~]# vi java_log/thread_stack.log
-------------然后再按/,输入线程ID:1306f-------------
"ActiveThread-Hot" #18 prio=5 os_prio=0 tid=0x00007f7444107800
            nid=0x1306f runnable [0x00007f7432ade000]
   java.lang.Thread.State: RUNNABLE
        at CpuOverload.lambda$main$1(CpuOverload.java:18)
        at CpuOverload$$Lambda$2/531885035.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)

在线程栈的log日志中,对于线程名称、线程状态、以及该线程的哪行代码消耗的CPU资源最多,都在其中详细列出,接下来要做的就是根据定位到的代码,去Java应用中修正代码重新部署即可。

当然,如果执行jstack 77915 | grep 1306f命令后,出现的是““VM Thread” os_prio=0 tid=0x00007f871806e000 nid=0xa runnable”这类以“VM Thread”开头的信息,那么则代表这是JVM执行过程中,虚拟机自身的线程造成的,这种情况有需要进一步排查JVM自身的线程了,如GC线程、编译线程等。

3.7.2、CPU100%排查小结

CPU100%问题排查步骤几乎是死的模板:

  • top指令查看系统后台进程的资源占用情况,确定是否是Java应用造成的。

  • ②使用

    top -Hp [pid]
    
    

    进一步排查Java程序中,CPU占用率最高的线程。

    • 保存CPU占用率最高的线程PID,并将其转换为16进制的值。
  • ③通过jstack工具导出Java应用的堆栈线程快照信息。

  • ④通过前面转换的16进制线程ID,在线程栈信息中搜索,定位导致CPU飙升的具体代码。

  • ⑤确认引发CPU飙升的线程是虚拟机自带的VM线程,还是业务线程。

  • ⑥如果是业务线程就是代码问题,根据栈信息修改为正确的代码后,将程序重新部署上线。

  • ⑦如果是VM线程,那可能是由于频繁GC、频繁编译等JVM的操作导致的,此时需要进一步排查。

CPU飙升这类问题,一般而言只会有几种原因:
①业务代码中存在问题,如死循环或大量递归等。
②Java应用中创建的线程过多,造成频繁的上下文切换,因而消耗CPU资源。
③虚拟机的线程频繁执行,如频繁GC、频繁编译等。

2.6、Arthas在线诊断工具

1、下载测试程序和Arthas工具

1.首先下载并启动Arthas官方提供给我们的测试程序

curl -O https://arthas.aliyun.com/math-game.jar
java -jar math-game.jar

image-20230712162506998

2.另外再启动一个shell窗口下载并启动arthas工具

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

image-20230712162539737

3.输入“1”选择我们的测试程序

image-20230712162603203

完成以上操作后,接下来就可以来尝试使用arthas提供的命令了!

2、基础命令
1.dashboard

作用:查看当前应用进程的实时数据面板

image-20230712162735895

数据说明:
ID: Java 级别的线程 ID,注意这个 ID 不能跟 jstack 中的 nativeID 一一对应。
NAME: 线程名
GROUP: 线程组名
PRIORITY: 线程优先级, 1~10 之间的数字,越大表示优先级越高
STATE: 线程的状态
CPU%: 线程的 cpu 使用率。比如采样间隔 1000ms,某个线程的增量 cpu 时间为 100ms,则 cpu 使用率=100/1000=10%
DELTA_TIME: 上次采样之后线程运行增量 CPU 时间,数据格式为秒
TIME: 线程运行总 CPU 时间,数据格式为分:秒
INTERRUPTED: 线程当前的中断位状态
DAEMON: 是否是 daemon 线程
JVM 内部线程

2.jvm

作用:查看当前 JVM 的信息

image-20230712163057692

参数说明:
THREAD 相关
COUNT: JVM 当前活跃的线程数
DAEMON-COUNT: JVM 当前活跃的守护线程数
PEAK-COUNT: 从 JVM 启动开始曾经活着的最大线程数
STARTED-COUNT: 从 JVM 启动开始总共启动过的线程次数
DEADLOCK-COUNT: JVM 当前死锁的线程数
文件描述符相关
MAX-FILE-DESCRIPTOR-COUNT:JVM 进程最大可以打开的文件描述符数
OPEN-FILE-DESCRIPTOR-COUNT:JVM 当前打开的文件描述符数

3.memory

作用:查看 JVM 的内存信息

image-20230712163207780

4.vmoption

作用:查看和修改 JVM 里诊断相关的选项
image-20230712163436151
修改指定的选项:

image-20230712163545491

5.thread

作用:查看当前 JVM 的线程堆栈信息

image-20230712163635192

查看最忙的前3个线程:

image-20230712163806426

查看指定的线程:

image-20230712163903231

找出当前阻塞其他线程的线程:

image-20230712163931677

注意:目前只支持找出 synchronized 关键字阻塞住的线程, 如果是java.util.concurrent.Lock, 目前还不支持。

6.help

作用:查看当前 arthas 版本支持的指令

image-20230712164133472
7.命令 -h

作用:查看指定命令的用法

image-20230712164220290
6.stop

作用:关闭 Arthas 服务端,所有 Arthas 客户端全部退出。

3、进阶命令
1.heapdump

作用:生成内存快照,排查内存泄漏时很有用。

image-20230712164604473

2.jad

作用:反编译Java类。
反编译指定的Java类:

image-20230712165104121

3.sc

作用:搜索出所有已经加载到 JVM 中的 Class 信息。
查看指定的JVM已加载类的详细信息:

image-20230712165311570

4.sm

作用:查看已加载类的方法信息。
查看指定类的方法:

image-20230712165714539

四、框架

2.1、谈谈自己对于 Spring IoC 和 AOP 的理解

**IoC:**IoC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。将对象之间的相互依赖关系交给 IOC 容器来管理,并由 IOC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。DI(依赖注入)

**AOP:**AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理

2.2、在项目中有用到AOP吗?

​ 日志管理

​ 权限控制

mybatis的自动填充

2.3、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 都有不同的会话

2.4、SpringBoot的优点

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

2.4、SpringBoot常用注解

image-20230610184718279

创建对象:@Component @Controller @Service @Repository

依赖注入:@Autowired @Value

springmvc: @Restcontroller @RequestMapping @GetMapping … @Pathvariable @RequestParam @RequestBody

​ @ResponseBody

配置相关:@Configuration @Bean @ConfigurationProperties

Springboot: @SpringBootApplication

2.5、@Autowired和@Resource 的区别

总的来说,@Autowired和@Resource都是用来进行依赖注入的注解,但是它们有一些不同之处:

  • @Autowired 是 Spring 框架中的注解,用来标注需要自动装配的 bean,默认按类型装配,如果有多个同类型的 bean,会抛出异常。但如果配合@Qualifier注解指定需要注入的那个Bean的名称就能正常运行。
  • @Resource 是 Java 自带的注解,用来标注需要自动装配的 bean,按照名称进行装配,如果名称不存在,会使用类型装配,如果此时Spring容器有多个类型相同的Bean就会报错。

2.6、SpringCloud配置文件的加载顺序

加载顺序:boot-strap.yml>application.yml>配置中心中的user.yml>配置中心中的User-dev.yml

优先级反向。

2.7、Spring事务传播行为

支持当前事务的情况:

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

不支持当前事务的情况:

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

其他情况:

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

2.8、Spring事务隔离级别

TransactionDefinition 接口中定义了五个表示隔离级别的常量:

  • TransactionDefinition.ISOLATION_DEFAULT: 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别.
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • TransactionDefinition.ISOLATION_READ_COMMITTED: 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ: 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

2.9、Spring事务失效的场景

1、方法使用final类型修饰

2、非public修饰的方法

3、同一个类中非事务方法调用带事务的方法会导致事务失效

4、事务传播类型设置出错

5、回滚的异常类型不匹配

5、异常被内部catch,程序生吞异常

6、数据库不支持事务

7、多线程调用

参考:http://www.imodou.com.cn/article/%E6%88%91%E8%AE%A4%E7%9C%9F%E6%80%BB%E7%BB%93%E5%B9%B6%E5%88%86%E6%9E%90%E4%BA%86Spring%E4%BA%8B%E5%8A%A1%E5%A4%B1%E6%95%88%E7%9A%84%E5%8D%81%E7%A7%8D%E5%B8%B8%E8%A7%81%E5%9C%BA%E6%99%AF.html

2.10、SpringMVC工作流程

image-20230610184200617

1:浏览器发送请求送至前端控制器DispatcherServlet。

2:DispatcherServlet收到请求后调用HandlerMapping处理器映射器。

3:处理器映射器找到具体的Handler处理器返回给DispatcherServlet。

4:DispatcherServlet调用HandlerAdaptor处理器适配器。

5:HandlerAdaptor去调用具体的处理器(Controller)。

6:Controller返回一个ModelAndView对象给HandlerAdaptor。

7:HandlerAdaptor将接收到的ModelAndView对象返回给DispatcherServlet。

8:DispatcherServlet将ModelAndView对象传给ViewResolver视图解析器进行解析。

9:ViewResolver视图解析器将解析的结果View返回给DispatcherServlet。

10:DispatcherServlet根据View进行渲染视图。

11:DispatcherServlet响应浏览器的请求。

2.11、Spring 中的 bean 生命周期?

image-20230609120022465

Spring 中 Bean 的生命周期是指:Bean 在 Spring(IoC)中从创建到销毁的整个过程。 Spring 中 Bean 的生命周期主要包含以下 5 部分:

  1. 实例化:为 Bean 分配内存空间;
  2. 设置属性:将当前类依赖的 Bean 属性,进行注入和装配;
  3. 初始化:
    1. 执行各种通知;
    2. 执行初始化的前置方法;
    3. 执行初始化方法;
    4. 执行初始化的后置方法。
  4. 使用 Bean:在程序中使用 Bean 对象;
  5. 销毁 Bean:将 Bean 对象进行销毁操作。

2.12、Springboot自动装配原理?

Springboot自动装配原理

从启动类开始看

说起SpringBoot中的自动装配,首先要从SpringBoot的启动类开始看。

@SpringBootApplication
public class Application {

 public static void main(String[] args) {
     SpringApplication.run(Application.class, args);
 }

}

这是一个基本的SpringBoot启动类,我们主要看SpringBootApplication这个注解,点进去源码,看一下具体的实现。

image-20230131195118259

根据图中的信息,我们完全可以看得出来,SpringBootApplication这个注解,是一个复合注解。

SpringBootConfiguration注解,可能会有些陌生,但是对于@Configuration注解的话,就一定不会了; ComponentScan也是我们在Spring项目中常常会用到的扫描注解。

主要还是来说一下@EnableAutoConfiguration注解,自动配置注解,也可以说自动装配,既然是要聊自动装配原理,那也就是聊一下@EnableAutoConfiguration注解的具体实现了,下面就来看一下吧。

开启自动配置注解@EnableAutoConfiguration

看一下此注解的源码实现,如下图。

image-20230131195145005

Import注解才是自动装配的核心,继续深入。

Import注解实现了AutoConfigurationImportSelector类,自动装配也是在这个类中进行了具体的实现。

AutoConfigurationImportSelector类中实现了诸多方法,自动装配的实现则是在selectImports方法中,如图所示。

image.png

从源码中读起来,有一个getCandidateConfigurations方法,进入看一下代码情况。

image.png

这里存在一个断言,意为无法正确的找到spring.factories文件,结果就很自然了,这个方法就是去加载了spring.factories文件,让我们去找一下这个文件里面具体是什么内容吧。

通过IDEA中的当前类定位按钮进行寻找,

image.png

在这里能找到文件,如图:

image.png

文件内容如图:

image.png

可以看出,通过selectImports方法,取到该文件下的一系列类名,随后将这些类自动加载至IOC容器中。

这些类都属于内部存在自动配置的类,同样可以发现这些类名都是以AutoConfiguration结尾的。

总结

自动装配原理就已经说完了,总结一下,就是通过@EnableAutoConfiguration注解,加载AutoConfigurationImportSelector类中的selectImports方法,进而扫描MATE-INF下的spring.factories文件下的自动配置类,并将其装配至IOC容器的过程。

2.13、Spring怎么解决循环以来

什么是循环依赖:A依赖B的同时,B也依赖了A,就构成了循环依赖

一级缓存(singletonObjects) 也被称为单例池, 主要存放最终形态的bean(如果存在代理,存放的代理后的bean)。 一般情况我们获取bean都是从这里获取的。

二级缓存(earlySingletonObjects) 主要存放过渡bean,也就是三级缓存中ObjectFactory产生的对象。主要作用是防止bean被AOP切面代理时,重复通过三级缓存对象ObjectFactory创建对象。被代理情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。这明显不满足spring单例的原则,所以需要二级缓存进行缓存。 同时需要注意:二级缓存中存放的bean也是半成品的,此时属性未填充。

三级缓存(singletonFactories) 其存放的对象为ObjectFactory类型,主要作用是产生bean对象。Spring在这里存放的是一个匿名内部类,调用getObject()最终调用的是getEarlyBeanReference()。该方法的主要作用是:如果有需要,产生代理对象。如果bean被AOP切面代理,返回代理bean对象;如果未被代理,就返回原始的bean对象。

解决循环依赖的的思路:就是在实例化过程中,提前把bean暴露出来,虽然此时还是个半成品(属性未填充),但是我们允许先注入,这样确实能解决问题。我们梳理一下: 实例化A -> 暴露a对象(半成品)-> 属性填充注入B -> B还没有实例化,需要先进行实例化B(A等待) -> 实例化B -> 注入A(半成品) -> 实例化B成功 -> 实例化A成功。通过提前把半成品的对象暴露出来,支持别的bean注入,确实可以解决循环依赖的

参考:http://www.imodou.com.cn/article/%E8%81%8A%E9%80%8FSpring%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96.html

2.14、Spring 框架中都用到了哪些设计模式?

  1. 工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
  2. 单例模式:Bean默认为单例模式。
  3. 代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
  4. 模板模式:用来解决代码重复的问题。比如. RestTemplate,redisTemplate、ReabbitTemplate, JmsTemplate, JpaTemplate。
  5. 适配器模式:处理器适配器。
  6. 观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的 对象都会得到通知被制动更新,如Spring中listener的实现–ApplicationListener。
  7. 。。。。。

2.15、Springboot配置文件类型及优先级

Springboot的配置文件包括.properties、.yaml、yml三种

优先级:

.properties>.yml>.yaml

2.14、Mybatis分页插件原理

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。(一般使用pagehelper插件)

select * from tbl_user

分页插件拦截到sql语句之后生成的SQL

select * from tbl_user limit 20,10 //0:起始位置 10:每页显示的条数

select count(*) from tbl_user

2.15、#{} 和 ${} 的区别是什么?

• ${}:就是字符串的拼接,容易产生注入攻击

• #{}:它是一个占位符,可以防止注入攻击

#{}
String name="1 or 1=1"                  select * from user where name='1 or 1=1'

PreparedStatement ps = Connection.prepareStatement("select * from user where name=?")   //预编译
ps.setString(1,name);

${}
String name="1 or 1=1"                 select * from user where name=1 or 1=1
Statement st = Connection.executeStatement("select * from user where name="+name)

2.16、MyBatis的一级缓存和二级缓存

一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的是 SqlSession 之间的缓存数据区(HashMap)是互相不影响。

二级缓存是 Mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。

2.17、MyBatis如何获取返回的id

 <insert id="addRole" parameterType="com.yy.beans.Role" useGeneratedKeys="true" keyColumn="rid" keyProperty="rid">
    insert into t_role values (null,#{rname})
</insert>

Role role = new Role();
role.setName("红理");
role.getRid()//null

roleMapper.addRole(role);
role.getRid();// 6

2.18、MyBatis动态SQL

1、标签、标签
当查询语句的查询条件由于输入参数的不同而无法确切定义时,可以使用标签对来包裹需要动态指定的SQL查询条件,而在标签对中,可以使用条件来分情况设置SQL查询条件。

<select id="selectByCondition">
   select * from user
   <where>
   		<if test=‘name!=null and name!=''’>
   				and name like concat('%',#{name},'%')
   		</if>
   		<if test=‘age!=null ’>
   				and age=#{age}
   		</if>
   </where>

</select>


  select * from user where name like concat('%',#{name},'%') and age=#{age}

2、标签

标签属性说明:

属性 说明
index 当迭代对象是数组,列表时,表示的是当前迭代的次数。
item 当迭代对象是数组,列表时,表示的是当前迭代的元素。
collection 当前遍历的对象。
open 遍历的SQL以什么开头。
close 遍历的SQL以什么结尾。
separator 遍历完一次后,在末尾添加的字符等。

//insert into user values('dabao',20),('xiabao',20)

User user = new User();
user.setName('dabao');
user.setAge(20);

User user1 = new User();
user1.setName('xiaoabo');
user1.setAge(20);

List<User> list = new ArrayList();
list.add(user);
list.add(user1);



<insert id="batchInsert">
	insert into user 
	<foreach collection='list' item="item" seperator="," open="values" close="">
	(#{item.name},#{item.age})
	</foreach>
</insert>

insert into user values(#{item.name},#{item.age}),(#{item.name},#{item.age})

三、微服务

3.1 SpringBoot与SpringCloud的区别

1、Spring Boot用于快速构建单体应用程序,它提供了自动配置、嵌入式Web服务器、安全性、日志、监控等常用功能

2、而Spring Cloud的定位是构建分布式系统,它提供了服务发现、负载均衡、断路器、配置中心等功能

3、Springcloud依赖于SpringBoot

3.2、微服务五大组件是什么?

1、注册中心:Nacos Eureka

2、配置中心:Nacos

3、负载均衡: Ribbon feign(底层也使用了Ribbon)

4、网关:GateWay

5、服务熔断降级: hystrix sentinel

3.2、什么是CAP定理和Base理论?

  • Consistency(一致性):在分布式的环境中,一致性是指数据在多个副本之间是否能够保持一致的特性

  • Availability(可用性):可用性是指系统提供的服务必须一直处于可用的状态,而且对于用户的每一个操作请求,总是能够在有限的时间内获取到非错的响应——但是不保证获取的数据为最新数据

  • Partition tolerance(分区容忍性):分区容忍性是指当部分节点出现消息丢失或者分区故障的时候,分布式系统仍然能够继续对外提供满足一致性和可用性的服务

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写,是对 CAP 中 AP 的一个扩展。

CP 架构:对于 CP 来说,放弃可用性,追求一致性和分区容错性。

AP 架构:对于 AP 来说,放弃强一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,Base理论 也是根据 AP 来扩展的。

3.3、Nacos注册中心相关

3.3.1 Nacos是CP还是AP

Nacos注册中心是CP还是AP,具体看如何配置,如果注册的节点是临时节点,那么就是AP,如果是非临时节点,那么就是CP,默认是临时节点。

Nacos中的配置中心其实没什么CP或AP,Nacos是将配置信息存储本地文件或数据库

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        group:  mall-order
        cluster-name: SH
        #ephemeral: false   //持久化实例,使用 CP架构
        ephemeral: true	   //临时实例,使用 AP架构   
   application:
   	 name: user-application

3.3.2 Nacos和Eureka区别
NacosEureka
服务发现Nacos采用定时拉取和订阅推送两种模式Eureka只支持定时拉取模式
实例类型Nacos有非临时实例和临时实例两种Eureka 只有临时实例
健康检测Nacos对临时实例采用心跳检测,对永久实例采用主动请求Eureka 只支持心跳模式
Nacos支持AP和CP两种模式只支持AP模式

3.4 服务熔断与降级

  • 服务熔断
    服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。断路器(关闭 半开 打开),可以通过@HystrixCommand注解配置打开的这个阈值。
  • 服务降级
    服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数 据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。

3.5 Feign的使用步骤

1、定义Feign接口,通过@FeignClient(“微服务的名称”)

2、在调用方的引导类上加上@EnableFeignClients注解。

3.6 网关的作用

鉴权、路由、限流、日志

spring:
  cloud:
    gateway:
      routes:
       - id: userservice
         uri: lb://user=service
         predicates:
          - Path= /user/**

四、ElasticSearch

4.1、elasticsearch 的倒排索引是什么

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

而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了 倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。

image-20210826170950347

o(1):性能不受元素长度的限制

o(n):随着元素的增长,我们计算的速度也就越慢

4.2、什么是分片(SHARDS)-项目中Es服务器是怎么部署 7.14

由于应用程序在不同的机器上使用了多个ElasticSearch实例,因此在扩展方面存在诸如RAM、VCPU等资源限制。索引中的数据可以分为多个部分,由一个单独的ElasticSearch节点或实例管理。每个部分称为一个SHARDS。

image-20230205193256210
PUT /test_index2   //创建一个名为test_index2的索引
{ 
  "settings":{ 
    "number_of_shards" : 3, 
    "number_of_replicas" : 1 
    
  },
  "mappings":{
  	"properties":{
  		"id":{
  			type:"keyword",
  		},
  		"name":{
  			type:"text",
  			anlizier:"ik_smart",
  			index:true
  		}
  	}
  }
}

4.3、什么是副本REPLICAS

一个索引被分解成分片以便于分发和扩展。副本是分片的备份。一个节点是一个属于一个集群的ElasticSearch的运行实例。一个集群由一个或多个共享相同集群名称的节点组成。

4.4、ElasticSearch中的中文分词器是什么?

  1. IK分词 ik_max_word(细粒度) ik_smart(粗粒度)
  2. standard 分词器

4.5、你在项目中是如何使用ES的, 主要用到ES哪些功能 ?

如何使用的

  1. 使用 RestHighLevelClient进行Es操作

用到哪些功能

  1. 关键词搜索
  2. GEO地理位置查询

4.6、ElasticSearch常用的查询方式有哪些 ? 如何构建查询条件

ElasticSearch查询方式有很多 :

  1. matchAllQuery : 查询所有
  2. termQuery : 精确
  3. matchQuery : 全文检索
  4. wildcard : 模糊查询(不推荐)
  5. regexpQuery : 正则匹配
  6. prefixQuery : 前缀查询
  7. rangeQuery : 范围查询
  8. queryStringQuery : 查询条件分词查询,可以设置多个检索字段, 默认取并集 , 可以设置operation 指定取值方式
  9. boolQuery : 多条件组合查询 , must , must_not , shoud , shoud_not , filter

如何构建查询条件 ?

  1. 使用new关键字创建对应的 SearchRequest
  2. 使用QueryBuilders进行查询条件构建

4.9、详细描述一下 Elasticsearch 存入数据的过程

image-20210826171139042

ES中每 一个节点都有自己的角色:

主节点

协调节点

数据节点

第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。)

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

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

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

4.10、详细描述一下 Elasticsearch 搜索的过程?

elasticsearch的查询分成两个阶段:

  • scatter phase:分散阶段,coordinating node会把请求分发到每一个分片

  • gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户

image-20210723225809848

4.11、ElasticSearch如何实现深度分页?

分页方式性能优点缺点场景
from + size灵活性好,实现简单深度分页问题数据量比较小,能容忍深度分页问题
scroll解决了深度分页问题无法反应数据的实时性(快照版本)维护成本高,需要维护一个 scroll_id海量数据的导出需要查询海量结果集的数据
search_after性能最好不存在深度分页问题能够反映数据的实时变更实现复杂,需要有一个全局唯一的字段连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果,它不适用于大幅度跳页查询海量数据的分页

Search After

Search_after是 ES 5 新引入的一种分页查询机制,其原理几乎就是和scroll一样,因此代码也几乎是一样的。

「基本使用:」

第一步:

POST twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "es"
        }
    },
    "sort": [
        {"date": "asc"},
        {"_id": "desc"}
    ]
}

返回出的结果信息 :

{
      "took" : 29,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 5,
          "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [
          {
            ...
            },
            "sort" : [
              ...
            ]
          },
          {
            ...
            },
            "sort" : [
              124648691,
              "624812"
            ]
          }
        ]
      }
    }

上面的请求会为每一个文档返回一个包含sort排序值的数组。

这些sort排序值可以被用于search_after参数里以便抓取下一页的数据。

比如,我们可以使用最后的一个文档的sort排序值,将它传递给search_after参数:

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "es"
        }
    },
    "search_after": [124648691, "624812"],
    "sort": [
        {"date": "asc"},
        {"_id": "desc"}
    ]
}

若我们想接着上次读取的结果进行读取下一页数据,第二次查询在第一次查询时的语句基础上添加search_after,并指明从哪个数据后开始读取。

五、Seata有哪几种工作模式?及各自特点

seata两阶段提交:

image-20230725225314237

TCC:

​ Try: 执行sql提交事务,并冻结资源

​ Confirm:第二阶段如果没有异常正常提交,此时需要将冻结表中的资源删除

​ Cancel: 第二阶段如果有异常需要回滚,就需要将冻结表中的资源恢复到原有的表中

六、消息中间件

6.1 MQ(MessageQueue)的作用

常见的MQ产品包括:Rabbitmq kafka emq Rocketmq

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

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

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

6.2、项目中为什么要用RabbitMQ(RabbitMQ的特点)

几种常见MQ的对比:

RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP,SMTP,STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单机吞吐量一般非常高
消息延迟微秒级毫秒级毫秒级毫秒以内
消息可靠性一般一般

追求可用性:Kafka、 RocketMQ 、RabbitMQ

追求可靠性:RabbitMQ、RocketMQ

追求吞吐能力:RocketMQ、Kafka

追求消息低延迟:RabbitMQ、Kafka

6.3、RabbitMQ的消息模型?

基本队列(简单队列)

工作队列

广播模式(fanout)

image-20230727111108494

路由模式(Direct)

image-20230727111335939

主题模式(Topic)

image-20230727111548798

6.4、如何保证RabbitMQ消息的可靠传输?

  • 丢失又分为:生产者丢失消息、消息列表丢失消息、消费者丢失消息;
  1. 生产者丢失消息:从生产者弄丢数据这个角度来看,RabbitMQ采用了 publisher confirm机制来确保生产者不丢消息;

    publisher confirm机制确保发送消息时消息都将会被指派一个唯一的ID,一旦消息被投递到所有匹配的队列之后;

    rabbitMQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;

    如果rabbitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

  2. broker丢数据:消息持久化。

    生产者确认可以确保消息投递到RabbitMQ的中,但是消息发送到RabbitMQ以后,如果突然宕机,也可能导致消息丢失。

    要想确保消息在RabbitMQ中安全保存,必须开启消息持久化机制

    • 交换机持久化
    • 队列持久化
    • 消息持久化
  3. 消费者丢失消息

    RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向RabbitMQ发送ACK回执,表明自己已经处理消息。

​ 而SpringAMQP则允许配置三种确认模式:

​ •manual:手动ack,需要在业务代码结束后,调用api发送ack。

​ •auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

​ •none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

​ 由此可知:

​ •none模式下,消息投递是不可靠的,可能丢失

​ •auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack

​ •manual:自己根据业务情况,判断什么时候该ack

​ 一般,我们都是使用默认的auto即可。

6.5、RabbitMQ如何保证消息不被重复消费(幂等)?

一般来说消息重复消费都是在短暂的一瞬间消费多次,我们可以使用 redis 将消费过的消息唯一标识存储起来,然后在消费端业务执行之前判断 redis 中是否已经存在这个标识。如果已经存在代表处理过了。不存在就放进 redis 设置过期时间,执行业务

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(Message message){

        String messageId = message.getMessageProperties().getMessageId();
        if(redisTemplate.opsForValue().get(messageId)==null){
            //redis中不存在,说明没有消费过,此时执行业务正常消费
				
            
            //消费完成后,将messageId存储到redis中,注意设置过期时间
            redisTemplate.opsForValue().set(messageId,1,10, TimeUnit.SECONDS);
        }

    }

另外也可以对数据库设置唯一约束,可以保证数据库数据不会插入重复的两条数据来进行兜底解决。

6.6 接口幂等

在生成页面之后,我们可以在后端产生一个唯一ID,并将这个ID传到页面。页面接收ID并存储。这个页面每次发请求时都会将这个ID发到

后台。后台判断ID是否重复,如果重复就认为是表单重复提交。

6.6、如何解决消息积压的问题?

所谓消息积压一般是由于消费端消费的速度远小于生产者发消息的速度,导致大量消息在 RabbitMQ 的队列中无法消费。

其实这玩意我也不知道为什么面试这么喜欢问…既然消费者速度跟不上生产者,那么提高消费者的速度就行了呀!个人认为有以下几种思路

  • 对生产者发消息接口进行适当限流
  • 多部署几台消费者实例(推荐)
  • 如果单个消息消费时间很长,可以在消费端开启多个线程,来加快消费速度
  • 采用惰性队列

6.7 如何保证消息的顺序性

1、给每一个消息进行编号,然后在redis中记录当前需要消费哪一个编号的消息,消费完之后,再将redis中的值自增。

2、消息只发给一个队列,这个队列也只有一个消费者

6.7 Kafka 架构中的一般概念:

image-20230726191444852

Producer:生产者,也就是发送消息的一方。生产者负责创建消息,然后将其发送到 Kafka。

Consumer:消费者,也就是接受消息的一方。消费者连接到 Kafka 上并接收消息,进而进行相应的业务逻辑处理。

Consumer Group:一个消费者组可以包含一个或多个消费者。使用多分区 + 多消费者方式可以极大提高数据下游的处理速度,同一消费组中的消费者不会重复消费消息,同样的,不同消费组中的消费者消息消息时互不影响。Kafka 就是通过消费组的方式来实现消息 P2P 模式和广播模式。

Broker:服务代理节点。Broker 是 Kafka 的服务节点,即 Kafka 的服务器。

Topic:Kafka 中的消息以 Topic 为单位进行划分,生产者将消息发送到特定的 Topic,而消费者负责订阅 Topic 的消息并进行消费。

Partition:Topic 是一个逻辑的概念,它可以细分为多个分区,每个分区只属于单个主题。同一个主题下不同分区包含的消息是不同的,分区在存储层面可以看作一个可追加的日志(Log)文件,消息在被追加到分区日志文件的时候都会分配一个特定的偏移量(offset)。

Offset:offset 是消息在分区中的唯一标识,Kafka 通过它来保证消息在分区内的顺序性,不过 offset 并不跨越分区,也就是说,Kafka 保证的是分区有序性而不是主题有序性。

Replication:副本,是 Kafka 保证数据高可用的方式,Kafka 同一 Partition 的数据可以在多 Broker 上存在多个副本,通常只有主副本对外提供读写服务,当主副本所在 broker 崩溃或发生网络一场,Kafka 会在 Controller 的管理下会重新选择新的 Leader 副本对外提供读写服务。

Record: 实际写入 Kafka 中并可以被读取的消息记录。每个 record 包含了 key、value 和 timestamp

image-20230726192159378 image-20230726192220945

6.8、Kafka 是推模式还是拉模式,推拉的区别是什么?

Kafka Producer 向 Broker 发送消息使用 Push 模式,Consumer 消费采用的 Pull 模式。拉取模式,让 consumer 自己管理 offset,可以提供读取性能

6.9、Kafka 如何保证消息可靠性

消费端会不会弄丢数据?

唯一可能导致消费者弄丢数据的情况,就是说,你消拉取了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢了。

大家都知道 Kafka 会自动提交 offset,那么只要**关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。**但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次 , 出现了消息重复消费的 问题。这个时候可以通过一些其他的方式, 保证消息幂等就可以了

手动提交方案 : 同步提交 , 异步提交 , 同步异步结合

Broker 会不会弄丢数据?

这块比较常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。

所以此时一般是要求起码设置如下 4 个参数:

  • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
  • 在 producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。
  • 在 producer 端设置 retries=10000000(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。

生产者会不会弄丢数据?

如果设置了 acks=all,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次

  • acks=0:生产者不会等待任何来自服务器的响应。

如果当中出现问题,导致服务器没有收到消息,那么生产者无从得知,会造成消息丢失

由于生产者不需要等待服务器的响应所以可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量

  • acks=1(默认值):只要集群的Leader节点收到消息,生产者就会收到一个来自服务器的成功响应

如果消息无法到达Leader节点(例如Leader节点崩溃,新的Leader节点还没有被选举出来)生产者就会收到一个错误响应,为了避免数据丢失,生产者会重发消息

如果一个没有收到消息的节点成为新Leader,消息还是会丢失

此时的吞吐量主要取决于使用的是同步发送还是异步发送,吞吐量还受到发送中消息数量的限制,例如生产者在收到服务器响应之前可以发送多少个消息

  • acks=all:只有当所有参与复制的节点全部都收到消息时,生产者才会收到一个来自服务器的成功响应

这种模式是最安全的,可以保证不止一个服务器收到消息,就算有服务器发生崩溃,整个集群依然可以运行

延时比acks=1更高,因为要等待不止一个服务器节点接收消息

根据实际的应用场景,我们设置不同的 acks,以此保证数据的可靠性。

另外,Producer 发送消息还可以选择同步(默认,通过 producer.type=sync 配置) 或者异步(producer.type=async)模式。如果设置成异步,虽然会极大的提高消息发送的性能,但是这样会增加丢失数据的风险。如果需要确保消息的可靠性,必须将 producer.type 设置为 sync。

6.10、Kafka中是怎么体现消息顺序性的?

  1. 可以设置topic 有且只有一个partition

  2. 根据业务需要,需要顺序的 指定为同一个partition

    在这里插入图片描述

  3. 根据业务需要,需要顺序的指定为同一个partition (比如同一个订单,使用同一个key,可以保证分配到同一个partition上)

如果指定了partition就会发送到指定的partition , 如果没指定就会根据key计算partition , 相同key肯定落到同一个partition

七、Redis

4.1、Redis 有哪些基本数据结构?

image-20230608094527799

Redis有五种基本数据结构。

string

字符串最基础的数据结构。字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字 (整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。

字符串主要有以下几个典型使用场景:

  • 缓存功能
  • 计数
  • 共享Session
  • 限速

hash

哈希类型是指键值本身又是一个键值对结构。

哈希主要有以下典型应用场景:

  • 缓存用户信息
  • 缓存对象

list

列表(list)类型是用来存储多个有序的字符串。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色

列表主要有以下几种使用场景:

  • 消息队列
  • 文章列表

set

集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一 样的是,集合中不允许有重复元素,并且集合中的元素是无序的。

集合主要有如下使用场景:

  • 标签(tag)
  • 共同关注

sorted set

有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个权重(score)作为排序的依据。

有序集合主要应用场景:

  • 用户点赞统计
  • 用户排序

4.2、Redis为什么快呢?

Redis的速度⾮常的快,单机的Redis就可以⽀撑每秒十几万的并发,相对于MySQL来说,性能是MySQL的⼏⼗倍。速度快的原因主要有⼏点:

  1. 完全基于内存操作
  2. 使⽤单线程,避免了线程切换和竞态产生的消耗
  3. 基于⾮阻塞的IO多路复⽤机制
  4. C语⾔实现,优化过的数据结构,基于⼏种基础的数据结构,redis做了⼤量的优化,性能极⾼

4.3、Redis是单线程的吗?

Redis 4.0 之后开始变成多线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 Key 的删除等等。

Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。这样做的⽬的是因为Redis的性能瓶颈在于⽹络IO⽽⾮CPU,使⽤多线程能提升IO读写的效率,从⽽整体提⾼Redis的性能。

image-20230608095158890

4.4、Redis持久化⽅式有哪些?有什么区别?

Redis持久化⽅案分为RDB和AOF两种。

RDB:RDB持久化是把当前进程数据生成快照保存到硬盘的过程。RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。

**AOF(append only file):**以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。

RDB:记录的是快照、文件小、恢复快;数据备份不实时。

save 900 1
save 300 10
save 60 10000

AOF:记录的是命令的日志、文件大、恢复慢;相较RDB时实性高

appendonly yes
# appendfsync always
appendfsync everysec
# appendfsync no


RDB VS AOF

1、RDB备份的是内存中的二进制数据恢复更快、文件更小。AOF备份的是命令,文件比RDB要大,恢复起来也会慢一些

2、AOF它相较于RDB备份数据完整性更高一些。

3、RDB默认是开启的,AOF需要在redis.conf文件中appendonly yes才能开启

4.5、Redis高可用

Redis保证高可用主要有三种方式:主从、哨兵、集群。

主从:

一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。

解决了数据丢失和读写分离的问题

如果主节点宕机,整个结构就无法正常工作。

image-20230608100534526

哨兵:

主从复制存在一个问题,没法完成自动故障转移。所以我们需要一个方案来完成自动故障转移,它就是Redis Sentinel(哨兵)。

image-20230608100639640

集群:

集群一举解决高可用和分布式问题。Redis集群把所有的数据映射到16384个槽中。每个节点对应若干个槽,只有当节点分配了槽,才能响应和这些槽关联的键命令

image-20230608100838382

4.6、什么是缓存击穿、缓存穿透、缓存雪崩?

缓存击穿

一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。

image-20230608101139960

解决⽅案:

1、加锁更新,⽐如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写⼊缓存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。

    public Shop cacheBreakDown(Long id) {
        //定义一个ID
        String cacheID = CACHE_SHOP_KEY + id;
        //1、查询Redis
        String shopJson = stringRedisTemplate.opsForValue().get(cacheID);
        //2、判断是否命中
        if(StrUtil.isNotBlank(shopJson)){
            //命中了直接返回
            return BeanUtil.toBean(shopJson,Shop.class);
        }
    
        //提前定义一个shop,要不在try里跨作用域了最后return不了
        Shop shop = null;
        try{
            //4、缓存未命中,获取锁,判断是否成功拿到锁
            if(!getLock(id)){
                //没有拿到锁,说明已有别的线程进来了,正在添加缓存
                //休眠一会,重新查询缓存
                Thread.sleep(200);
                //一定要return!递归出口
                return cacheBreakDown(id);
            }
            //5、只有拿到锁的线程才可以查询数据库
            shop = getById(id);
            //模拟缓存重建过程所需时间
            Thread.sleep(500);
            //5、判断是否查询到数据
            if(shop == null){
                //没数据,返回空值缓存,防止缓存击穿
                stringRedisTemplate.opsForValue().set(cacheID,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //6、查询成功,添加缓存
            stringRedisTemplate.opsForValue().set(cacheID,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL,TimeUnit.MINUTES);
        }catch (Exception e){
            //统一异常处理
            throw new RuntimeException(e);
        }finally {
            //7、释放锁
            unLock(id);
        }
        //8、返回
        return shop;
    }

2、将过期时间组合写在value中,通过异步的⽅式不断的刷新过期时间,防⽌此类现象。

缓存穿透

缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样。

image-20230608101301666

解决方案:

1、一种方式是在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。

2、除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。

缓存雪崩

某⼀时刻发⽣⼤规模的缓存失效的情况,例如缓存服务宕机、大量key在同一时间过期,这样的后果就是⼤量的请求进来直接打到DB上,可能导致整个系统的崩溃,称为雪崩。

image-20230608101627449

解决方案:

1、通过集群来提升缓存的可用性

2、为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。

4.7、能说说布隆过滤器吗?

布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。

存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。

image-20221203201040694我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是1:

  • 如果全不是1,那么key不存在;
  • 如果都是1,也只是表示key可能存在。

布隆过滤器也有一些缺点:

  1. 它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。
  2. 不支持删除元素。

4.8、如何保证缓存和数据库数据的⼀致性?

1、数据库订阅+消息队列保证key被删除

可以用一个服务(比如阿里的 canal)去监听数据库的binlog,获取需要操作的数据。

然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除操作。这种方式降低了对业务的侵入,但其实整个系统的复杂度是提升的,适合基建完善的大厂

image-20230608104059470

2、延时双删防止脏数据

简单说,就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。

image-20230608104217574

3、设置缓存过期时间兜底

这是一个朴素但是有用的办法,给缓存设置一个合理的过期时间,即使发生了缓存数据不一致的问题,它也不会永远不一致下去,缓存过期的时候,自然又会恢复一致。

4.9、Redis的过期数据删除策略有哪些?

**立即删除:**对内存友好,数据到期能够立即被删除。但CPU压力大。不会用。

惰性删除

惰性删除指的是当我们查询key的时候才对key进⾏检测,如果已经达到过期时间,则删除。显然,他有⼀个缺点就是如果这些过期的key没有被访问,那么他就⼀直⽆法被删除,⽽且⼀直占⽤内存。

定期删除

定期删除指的是Redis每隔⼀段时间对数据库做⼀次检查,删除⾥⾯的过期key。由于不可能对所有key去做轮询来删除,所以Redis会每次随机取⼀些key去做检查和删除。

redis采用的是:定期删除+惰性删除

4.10、Redis有哪些内存淘汰策略?<你怎么保证redis中的数据一定是最活跃的数据>

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  3. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  4. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  5. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  6. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
  7. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  8. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.11、Redis 支持事务吗?

edis提供了简单的事务,但它对事务ACID的支持并不完备。

multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的:

127.0.0.1:6379> multi 
OK
127.0.0.1:6379> sadd user:a:follow user:b 
QUEUED 
127.0.0.1:6379> sadd user:b:fans user:a 
QUEUED
127.0.0.1:6379> sismember user:a:follow user:b 
(integer) 0
127.0.0.1:6379> exec 1) (integer) 1
2) (integer) 1


Redis事务的原理,是所有的指令在 exec 之前不执行,而是缓存在 服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。

image-20230608104756321

Redis事务的注意点有哪些?

需要注意的点有:

  • Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
  • Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。

4.12、Redis的管道了解吗?

当客户端需要执行多条 redis 命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time) 对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis 服务端。

Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。

image-20221203201858142

4.13、假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?

keys java*

使用 keys 指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令短。

4.14、说说Redis底层数据结构?

Redis有动态字符串(sds)链表(list)字典(ht)跳跃表(skiplist)整数集合(intset)压缩列表(ziplist) 等底层数据结构。

八、MySQL

8.1、存储引擎

常见的存储引擎就 **InnoDB、MyISAM、**Memory、NDB。

  1. **InnoDB 支持事务,MyISAM 不支持事务。**这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
  2. **InnoDB 支持外键,而 MyISAM 不支持。**对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
  3. InnoDB 是聚簇(集)索引,MyISAM 是非聚簇(集)索引。如果是聚簇索引,数据文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
  4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快
  5. **InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。**一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

5.2、CHAR 和 VARCHAR 的区别?

char是固定长度,varchar长度可变:

存储时,前者不管实际存储数据的长度,直接按 char 规定的长度分配存储空间;而后者会根据实际存储的数据分配最终的存储空间

char(11):必须为11位 abc

varchar(11): 最大11位(可变) abc

5.3、MySQL索引分类

数据结构角度
  • B+树索引
  • Hash索引
从物理存储角度
  • 聚集索引(clustered index)一张表中只能有一个聚集索引,一般是给主键建聚集索引

  • 非聚集索引(non-clustered index),也叫辅助索引(secondary index)

    聚集索引和非聚集索引都是B+树结构

从逻辑角度
  • 主键索引:主键索引是一种特殊的唯一索引,不允许有空值
  • 普通索引或者单列索引:每个索引只包含单个列,一个表可以有多个单列索引
  • 多列索引(复合索引、联合索引):复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用复合索引时遵循最左前缀集合
  • 唯一索引或者非唯一索引

5.3、BTree与B+Tree的区别

BTree:

B+Tree:

image-20230608110704475

从B-Tree结构图中可以看到每个节点中不仅包含数据的key值,还有data值。而每一个页的存储空间是有限的,如果data数据较大时将会导致每个节点(即一个页)能存储的key的数量很小,当存储的数据量很大时同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度

而且B+Tree的所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对B+Tree进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

5.4、什么是回表?

聚集索引

我们知道InnoDB索引是聚集索引,它的索引和数据是存入同一个.idb文件中的,因此它的索引结构是在同一个树节点中同时存放索引和数据,如下图中最底层的叶子节点有三行数据,对应于数据表中的id、stu_id、name数据项。

image-20230608111724624

非聚集索引:

这次我们以示例中学生表中的name列建立非聚集索引,它的索引结构跟聚集索引的结构有很大差别,在最底层的叶子结点有两行数据,第一行的字符串是辅助索引,按照ASCII码进行排序,第二行的整数是主键的值。

这就意味着,对name列进行条件搜索,需要两个步骤:

① 在非聚集索引上检索name,到达其叶子节点获取对应的主键;

② 使用主键在主索引上再进行对应的检索操作

这也就是所谓的“回表查询

image-20230608111924519

5.5、覆盖索引

覆盖索引(Covering Index),或者叫索引覆盖, 也就是平时所说的不需要回表操作。就是select的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询列要被所建的索引覆盖

5.6、索引创建

哪些情况需要创建索引

  1. 主键自动建立主键索引
  2. 频繁作为查询条件的字段
  3. 单键/组合索引的选择问题,高并发下倾向创建组合索引
  4. 查询中排序的字段,排序字段通过索引访问大幅提高排序速度
  5. 查询中统计或分组字段

哪些情况不要创建索引

  1. 表记录太少
  2. 经常增删改的表
  3. 数据重复且分布均匀的表字段,只应该为最经常查询和最经常排序的数据列建立索引(如果某个数据类包含太多的重复数据,建立索引没有太大意义)
  4. where条件里用不到的字段不创建索引

5.7、事务的四大特性

事务是由一组SQL语句组成的逻辑处理单元,具有4个属性,通常简称为事务的ACID属性。

  • A (Atomicity) 原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
  • C (Consistency) 一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏
  • I (Isolation)隔离性:一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰
  • D (Durability) 持久性:在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚

5.8、事务隔离级别

数据库事务的隔离级别有4种,由低到高分别为

  • READ-UNCOMMITTED(读未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、不可重复读、幻读
  • READ-COMMITTED(读已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是不可重复读、幻读或仍有可能发生。(oracle)
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。(mysql)
  • SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读

5.9、delete、truncate、drop的区别

1、truncate和delete只删除数据不删除表的结构(定义),而drop语句将删除表的结构被依赖的约束(constrain),触发器(trigger),索引(index);依赖于该表的存储过程/函数将保留,但是变为invalid状态。

2、delete命令是DML,删除的数据将存储在系统回滚段中,需要的时候,数据可以回滚恢复。而truncate,drop命令是DDL,删除的数据是操作立即生效,原数据不放到rollback segment中,不能回滚,数据不可以回滚恢复。

3、delete命令,不会自动提交事务,操作会触发trigger;而truncate,drop命令,执行后会自动提交事务,操作不触发trigger。

4、速度:一般来说:drop > truncate > delete

5.10、MySQL锁(innodb)

乐观锁

用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加1。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。

1、数据库表三个字段,分别是id、value、version
select id,value,version from TABLE where id = #{id}
2、每次更新表中的value字段时,为了防止发生冲突,需要这样操作

update TABLE
set value=2,version=version+1
where id=#{id} and version=#{version}

悲观锁

悲观锁就是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,这点跟java中的synchronized很相似,所以悲观锁需要耗费较多的时间。另外与乐观锁相对应的,悲观锁是由数据库自己实现了的,要用的时候,我们直接调用数据库的相关语句就可以了。

说到这里,由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

共享锁

​ 共享锁又称读锁 (read lock),是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁

如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据

打开第一个查询窗口

start transaction; 
SELECT * from TABLE where id = 1  lock in share mode;

然后在另一个查询窗口中,对id为1的数据进行更新

update TABLE set name="www.souyunku.com" where id =1

此时,操作界面进入了卡顿状态,过了很久超时,提示错误信息
如果在超时前,第一个窗口执行commit,此更新语句就会成功。

[SQL]update test_one set name="www.souyunku.com" where id =1;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

加上共享锁后,也提示错误信息

update test_one set name="www.souyunku.com" where id =1 lock in share mode;

加上共享锁后,对于update,insert,delete语句会自动加排它锁。

排它锁

排他锁 exclusive lock(也叫writer lock)又称写锁。

名词解释:若某个事物对某一行加上了排他锁,只能这个事务对其进行读写,在此事务结束之前,其他事务不能对其进行加任何锁,其他进程可以读取,不能进行写操作,需等待其释放。 排它锁是悲观锁的一种实现,在上面悲观锁也介绍过

排它锁会阻塞所有的排它锁和共享锁

排他锁使用方式:在需要执行的语句后面加上for update就可以了 select status from TABLE where id=1 for update;

image-20230726212829376

要使用排他锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。

我们可以使用命令设置MySQL为非autocommit模式:

set autocommit=0;
# 设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:
# 1. 开始事务
start transaction; 
# 2. 查询表信息(for update加锁)
select status from TABLE where id=1 for update;
# 3. 插入一条数据
insert into TABLE (id,value) values (2,2);
# 4. 修改数据为
update TABLE set value=2 where id=1;
# 5. 提交事务
commit;/commit work

Innodb中的行锁与表锁

前面提到过,在Innodb引擎中既支持行锁也支持表锁,那么什么时候会锁住整张表,什么时候只锁住一行呢? 只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。行级锁的缺点是:由于需要请求大量的锁资源,所以速度慢,内存消耗大。

5.11、MySQL优化

1、开启慢查询日志

[mysqld]
slow_query_log = ON
slow_query_log_file = /var/lib/mysql/hostname-slow.log
long_query_time = 3

2、Explain(执行计划)

使用 Explain 关键字可以模拟优化器执行SQL查询语句,从而知道 MySQL 是如何处理你的 SQL 语句的。分析你的查询语句或是表结构的性能瓶颈

使用方式:

explain + sql语句

image-20230608114306371

  • type(显示查询使用了那种类型,从最好到最差依次排列 system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

    • system:表只有一行记录(等于系统表),是 const 类型的特例,平时不会出现
    • const:表示通过索引一次就找到了,const 用于比较 primary key 或 unique 索引,因为只要匹配一行数据,所以很快,如将主键置于 where 列表中,mysql 就能将该查询转换为一个常量
    • eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配,常见于主键或唯一索引扫描
    • ref:非唯一性索引扫描,范围匹配某个单独值得所有行。本质上也是一种索引访问,他返回所有匹配某个单独值的行,然而,它可能也会找到多个符合条件的行,多以他应该属于查找和扫描的混合体
    • range:只检索给定范围的行,使用一个索引来选择行。key列显示使用了哪个索引,一般就是在你的where语句中出现了between、<、>、in等的查询,这种范围扫描索引比全表扫描要好,因为它只需开始于索引的某一点,而结束于另一点,不用扫描全部索引
    • index:Full Index Scan,index于ALL区别为index类型只遍历索引树。通常比ALL快,因为索引文件通常比数据文件小。(也就是说虽然all和index都是读全表,但index是从索引中读取的,而all是从硬盘中读的
    • ALL:Full Table Scan,将遍历全表找到匹配的行

    tip: 一般来说,得保证查询至少达到range级别,最好到达ref

  • possible_keys(显示可能应用在这张表中的索引,一个或多个,查询涉及到的字段若存在索引,则该索引将被列出,但不一定被查询实际使用)

  • key

    • 实际使用的索引,如果为NULL,则没有使用索引
    • 查询中若使用了覆盖索引,则该索引和查询的 select 字段重叠,仅出现在key列表中

5.12、什么情况会导致索引失效?

1、联合索引不满足最左匹配原则

2、索引列参与了运算,会导致全表扫描,索引失效

3、索引列00

能够使用上索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%';
#函数运算,索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc';

4、模糊查询时(like语句),模糊匹配的占位符位于条件的首部

#走索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE 'ab%';
#索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME LIKE '%ab%';

5、参数类型与字段类型不匹配,导致类型发生了隐式转换,索引失效

#索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = 123;
#走索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE NAME = '123';

6、OR前后存在非索引的列,索引失效

#如果class_id没有建索引,则索引失效
explain select * from student_info where name like 'li%' or class_id=1;

7、两列数据做比较,即便两列都创建了索引,索引也会失效

8、查询条件使用不等进行比较时,需要慎重,普通索引会查询结果集占比较大时索引会失效

9、查询条件使用is null时正常走索引,使用is not null时,不走索引

#使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NULL;
#无法使用索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE age IS NOT NULL;

10、查询条件使用not in时,如果是主键则走索引,如果是普通索引,则索引失效

5.13 百万数据分页优化

优化前:

select * from user where create_time between '2023-01-01' and '2023-07-07' limit 510000,10

优化后:

select * from (select id from user where create_time  between '2023-01-01' and '2023-07-07' limit 510000,10)
as temp
inner join
user u
on
u.id=temp.id


5.14 select count(1)与select count(*)的区别

在做SQL优化时,很多人建议不使用count(*)而是使用count(1),从而可以提升性能,给出的理由是count(*)会带来全表扫描。而实际上如何写count并没有区别。

要想使用count(表达式)这个聚合函数计数,那括号中的表达式是否为NULL就很关键,如果为NULL则不计数,而非NULL则会计数。

一、count情况

1、count(1):可以统计表中所有数据,不统计所有的列,用1代表代码行,在统计结果中包含列字段为null的数据;

2、count(字段):只包含列名的列,统计表中出现该字段的次数,并且不统计字段为null的情况;

3、count(*):统计所有的列,相当于行数,统计结果中会包含字段值为null的列;

二、执行效率

列名为主键,count(列名)比count(1)快;列名不为主键,count(1)会比count(列名)快;

如果表中多个列并且没有主键,则count(1)的执行效率优于count(*);

如果有主键,则select count(主键)的执行效率是最快的;

如果表中只有一个字段,则select count(*)最快的

5.15 MySql常用函数

聚合函数:count sum avg max min

数学函数: rand ceil floor abs

字符串:length substring CONCAT left

日期函数:now() year() month() datediff()

5.16 MVCC

http://www.imodou.com.cn/article/MySQL%20MVCC%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3.html#_4-%E6%BC%94%E7%A4%BA%E8%BF%87%E7%A8%8B

5.17 SQL的完整语法

select  *  from 表名   where 条件 group by 列名  having 条件  order by 列  limit  from 

九、其它

9.1 Linux常用命令

命令说明
cd切换目录
ls查看目录
mkdir创建目录
cat查看文件
touch创建文件
cp复制
tail -f查看文件尾部
mv移动
pwd查看当前目录
rm删除文件
ps -ef | grep java查看正在运行的java进程
vi文本编辑
tar文件压缩
df -H查看磁盘大小
free查看内存使用
netstat显示网络状态信息
top查看CPU占用
systemctl stop firewalld关闭防火墙
poweroff关机

9.2 Docker常用命令

镜像:

docker images 查看本地的所有镜像

​ docker rmi 镜像的ID 删除镜像

​ docker load -i xxxx.tar 将tar包加载到镜像库

docker pull 镜像名 从远程或私服拉取镜像

​ docker save -o 将本地仓库的镜像保存到磁盘中

​ docker search 到远程库搜索镜像

容器:

docker run --name 容器名称 -p 宿主机的端口:容器内部的端口 -v html:/var/nginx/html -d 镜像名

​ docker stop 容器名

​ docker start 容器名

​ docker rm -f 容器名

​ docker restart 容器名

docker exec -it 容器名 bash

docker logs -f 容器名

如何制作镜像:

1、编写Dokcerfile的文件

2、docker build -t javaweb:1.0 .

服务编排:

1、编写docker-compose.yml

2、docker-compose up -d

9.3 git常用命令

  1. git clone : 从远程仓库克隆代码仓库
  2. git add : 添加本地⽂件进暂存区
  3. git commit : 提交本地代码到本地仓库
  4. git push origin 分⽀名称 : 推送本地仓库到远程
  5. git branch : 查看本地分⽀
  6. git branch 分⽀名称 : 创建分⽀
  7. git checkout 分⽀名称 : 切换到指定分⽀
  8. git pull origin 分⽀名称 : 从远程分⽀拉取仓库 , ⾃动合并
  9. git fetch origin 分⽀名称 : 从远程抓取代码 , 不合并
  10. git status : 查看本地仓库状态
  11. git log : 查看本地仓库提交⽇志
  12. git tag 标签名称 : 创建tag

13.git diff ⼯作区与暂存区的差异

  1. git reset HEAD^ 恢复成上次提交的版本

  2. git reset –hard 版本号

    –soft:只是改变HEAD指针指向,缓存区和⼯作区不变;

    –mixed:修改HEAD指针指向,暂存区内容丢失,⼯作区不变;

    –hard:修改HEAD指针指向,暂存区内容丢失,⼯作区恢复以前状态;

  3. git revert HEAD # 撤销最近的⼀个提交

  4. git revert 版本号 # 撤销某次commit

  5. git stash 将当前分支下的代码进行暂存

9.4 git pull , git merge , git fetch 命令的区别是什么 ?

git clone: 是在本地没有版本库的情况下,从远程仓库克隆⼀份到本地,是⼀个本地版本库从⽆到有的过

git pull: 是在本地仓库已经存在的情况下,将远程最新的commits抓取并合并到本地版本库的过程

git fetch: 从远程版本库抓取最新的commits,不会进⾏合并

git merge: 分⽀合并

9.5 在你们开发过程中遇到冲突如何解决 ?

在开发的过程中如果两个开发者分别在本地进⾏代码开发, 开发完毕后提交代码到本地仓库并推送到远程, 如果这两个开发者

对同⼀个或者多个⽂件进⾏了修改, 那么后提交代码的开发者则提交不成功 , 会报代码冲突错误

出现冲突之后 , 代码推送就不会成功, 这个时候需要执⾏ git pull 命令从远程拉取最新代码, 拉取之后通过

git status 命令查看冲突的⽂件 , 然后通过开发⼯具打开冲突⽂件 , 对⽂件冲突进⾏解决 , ⽂件中会明确的

表名冲突的位置 , 以及本地代码和远程代码的最新内容 , 如果⾃⼰能确定如何修改就⾃⼰该就⾏了 , 如果不确定

, 可以通过 git log 指令查看该版本是谁提交的 , 找到同时讨论看如何调整代码 , 代码调整完毕之后通过 gi

t add 命令, 标记冲突已解决, 然后正常提交 , 推送即可

9.5 什么时候应使⽤ git stash

当我们正在 feature 分⽀上开发新功能。这时,⽣产环境上出现了⼀个 bug 需要紧急修复,这个时候我

们的代码还没开发完,不想提交 , 这个时候可以⽤ git stash 命令先把⼯作区已经修改的⽂件暂存起来,

然后切换到 bugfix 分⽀上进⾏ bug 的修复,修复完成后,切换回 feature 分⽀,从堆栈中恢复刚刚保

存的内容

在切换之前需要存储⼀下当前分⽀的修改

git stash save "message"

若在这个主分⽀上修复bug完毕,回到feature时

git stash pop // 应⽤最近⼀次暂存的修改,并删除暂存的记录 1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

末、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值