java基础

1.java常见运行时异常

  • ClassCastException 类型转换异常

  • IndexOutOfBoundsException 数组索引越界异常

  • NullPointerException 空指针异常

  • ArithmeticException(算术异常)

  • IllegalArgumentException (非法参数异常)

2.空指针异常如何避免

  • 字符串比较,常量放在前面
if(status.equals(SUCCESS)){
}

上面这种情况下 status可能为null造成空指针异常,应该把常量放在前面,就能避免空指针异常。如下:

if(SUCCESS.equals(status)){
}
  • 初始化默认值

    在对象初始化的时候给它一个默认值或者默认构造实现 如:

Uses uses = new Uses;
String name = StringUtils.EMPTY;
  • 返回空集合

    在返回一个集合的话,默认会是NULL,统一规范返回一个空集合。如下:

public List getUsesList(){
	List list = usesMapper.getUsesList();
	return list == null ? new ArrayList() : List;
}

3.设计模式了解吗?

得分点 单例模式、工厂模式

常用的设计模式有单例模式工厂模式代理模式、适配器模式、装饰器模式、模板方法模式等等。

加分回答:像sping中的定义的bean默认为单例模式,spring中的BeanFactory用来创建对象的实例,他是工厂模式的体现。AOP面向切面编程是代理模式的体现,它的底层就是基于动态代理实现的。适配器模式在springMVC中有体现,它的处理器适配器会根据处理器规则适配相应的处理器执行,模板方法模式用来解决代码重复的问题等

4.讲讲单例模式、请你手写一下单例模式

单例模式(Singleton Pattern)是最简单的创建型设计模式。它会确保一个类只有一个实例存在。单例模式最重要的特点就是构造函数私有,从而避免外界直接使用构造函数直接实例化该类的对象。

单例模式在Java种通常有两种表现形式:

饿汉式:类加载时就进行对象实例化

懒汉式:第一次引用类时才进行对象实例化

饿汉式单例模式: 在类被加载时就会初始化静态变量instance,这时候类的私有构造函数就会被调用,创建唯一的实例。

public class Singleton{
    private Singleton(){}
    private static Singleton instance = new Singleton();
   
    public static Singleton getInstance(){
        return instance;
    }
} 

懒汉式单例模式: 类在加载时不会初始化静态变量instance,而是在第一次被调用时将自己初始化

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

但这时有一个问题,如果线程A和B同时调用此方法,会出现执行if (instance == null)语句时都为真的情况,那么线程AB都会创建一个对象,那内存中就会出现两个对象,这违反了单例模式的定义。为解决这一问题,可以使用synchronized关键字对静态方法 getInstance()进行同步,线程安全的的懒汉式单例模式代码如下:

public class Singleton{
    private Singleton(){}
    private static Singleton instance = null;
   
   synchronized public static Singleton getInstance(){
        if(instance==null){
            instance =new Singleton();
        }
        return instance;
    }
} 
  • 其实只有首次创建单例对象时才需要同步,但该代码实际上每次调用都会同步
  • 因此有了下面的双检锁改进
public class Singleton{
    private Singleton(){}
    private static  volatile Singleton instance = null;
   
    public static Singleton getInstance(){
        if(instance==null){
          synchronized(Singleton.class){
              if(instance==null){
                  instance =new Singleton();
              }
          } 
        }
        return instance;
    }
} 

5.请说说你对反射的了解

反射概念

反射就是在程序运行期间动态的获取对象的属性和方法的功能叫做反射。它能够在程序运行期间,对于任意一个类,都能知道它所有的方法和属性,对于任意一个对象,都能知道他的属性和方法。

获取Class对象的三种方式:getClass(); xx.class; Class.forName(“xxx”);

反射的优缺点:

  • 优点:运行期间能够动态的获取类,提高代码的灵活性。

  • 缺点:性能比直接的Java代码要慢很多。

应用场景:spring的xml配置模式,以及动态代理模式都用到了反射。

通过反射机制可以实现

  • 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;

  • 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;

  • 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。

常见的应用场景有

  • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;

  • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;

  • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。

6.请你说说ArrayList和LinkedList的区别

得分点:数据结构、访问效率

  • ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。

  • 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N)。

  • 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引。

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

7.TCP和UDP的区别

TCP

  • 使用tcp协议,必须双方先建立连接,它是一种面向连接可靠通信协议
  • 传输前,采用“三次握手”方式建立连接,所以是可靠的
  • 在连接中可进行大数据量的传输
  • 连接、发送数据都需要确认,且传输完毕后,还需要释放已建立的连接,通信效率较低

使用场景:

  • 对信息安全要求较高的场景,例如:文件下载、金融等数据通信

UDP:

  • UDP是一种无连接、不可靠传输的协议
  • 将数据源IP、目的地IP和端口包装成数据包,不需要建立连接
  • 每个数据包的大小限制在64kb内
  • 发送不管对方是否准备号,接受方收到也不确认,故是不可靠的
  • 可以广播发送,发送数据时无需释放资源,开销小,速度块

使用场景:

  • 语音通话,视频会话等

8.请你讲讲工厂模式

工厂模式其用意是定义一个创建产品的接口将具体创建推迟到子类中去。

工厂模式可以分为简单工厂、工厂方法、抽象工厂

  • 简单工厂就是定义一个工厂类,根据传入的参数返回不同的实例,被创建的实例具有共同的父类或者接口。

  • 工厂方法就是定义一个工厂接口,但是创建让子类去实现,去决定哪一个产品被实例化。

  • 抽象工厂就是对工厂方法的进一步深化,在工厂类中可以创建一组对象。实现方式是提供一个创建一系列相关或相互依赖对象的接口而无需执行具体的类。

9.String、StringBuffer、Stringbuilder有什么区别?

Stirng是不可变类,一个String对象创建之后,直到这个对象销毁为止,对象中的字符序列都不能被改变。

StringBufferStringBuilder则代表一个字符序列可变的字符串,当一个StringBuffer对象被创建之后,我们可以通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()、等方法来改变这个字符串对象的字符序列。

StringBuilder没有考虑线程安全问题,也正因如此,StringBuilder比StringBuffer性能略高。因此,如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuffer类。

10.请你说说HashMap底层原理

1)基本数据结构

  • 1.7 数组 + 链表
  • 1.8 数组 + (链表 | 红黑树)

2)put 与扩容

put 流程

  1. HashMap 是懒惰创建数组的,首次使用才创建数组
  2. 计算索引(桶下标)
  3. 如果桶下标还没人占用,创建 Node 占位返回
  4. 如果桶下标已经有人占用
    1. 已经是 TreeNode 走红黑树的添加或更新逻辑
    2. 是普通 Node,走链表的添加或更新逻辑,如果链表长度超过树化阈值,走树化逻辑
  5. 返回前检查容量是否超过阈值,一旦超过进行扩容

1.7 与 1.8 的区别

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化

扩容(加载)因子为何默认是 0.75f

  1. 在空间占用与查询时间之间取得较好的权衡
  2. 大于这个值,空间节省了,但链表就会比较长影响性能
  3. 小于这个值,冲突减少了,但扩容就会更频繁,空间占用也更多

2)树化与退化

树化意义

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

树化规则

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

退化规则

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

3)索引计算

索引计算方法

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

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

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

注意

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

5)并发问题

扩容死链(1.7 会存在)

数据错乱(1.7,1.8 都会存在)

6)key 的设计

key 的设计要求

  1. HashMap 的 key 可以为 null,但 Map 的其他实现则不然
  2. 作为 key 的对象,必须实现 hashCode 和 equals,并且 key 的内容不能修改(不可变)
  3. key 的 hashCode 应该有良好的散列性

11.请你说一下抽象类和接口的区别

接口和抽象类相同点有

  • 都不能被实例化,它们都位于继承树的顶端,用于被其它类实现和继承
  • 都可以有抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法

在用法上,接口和抽象类也有如下差异:

  • 接口里只能包含抽象方法和默认方法,不能为普通方法提供方法实现;抽象类则可以包含普通方法。
  • 接口里只能定义静态常量,不能定义普通成员变量;抽象类里既可以定义普通成员变量,也可以定义静态常量
  • 接口里不包含构造器;抽象类可以包含构造器,但抽象类的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作
  • 接口里不能包含初始化块,抽象类则可以包含初始化块
  • 一个类最多只能有一个父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足,总之,接口通常是定义允许多个实现的类型的最佳途径,但当演变的容易性比灵活性和功能更加重要时,应该使用抽象类来定义类型。

加分回答:在二者的设计目的上,接口作为系统与外界交互的窗口,体现了一种规范。对于接口的实现者来说,接口规定了实现者必须向外提供哪些服务;对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。 抽象类则不一样,抽象类作为系统中多个子类的共同父类,它体现的是一种模板式设计。抽象类作为多个子类的父类,它可以被当作系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当作最终产品,必须要有更进一步的完善。这种完善可能有几种不同方式。

12.请你说说==与equals()的区别

==和EQUALS都是JAVA中判断两个变量是否相等的方式。

如果判断的是两个基本类型的变量,并且两者都是数值类型(不一定要求数据类型完全相同),只要两个变量的值相等就会返回TRUE。

对于两个引用变量只有他们指向同一个引用时。==才会返回TRUE不能用于比较类型上没有父子关系的两个对象。 EQUALS()方法是OBJECT类提供的一个实例方法。所以所有的引用变量都能调用EQUALS()方法来判断他是否与其他引用变量相等,但使用这个方法来判断两个引用对象是否相等的判断标准与使用运算符没有区别,它同样要求两个引用变量指向同一个对象才会返回TRUE,但如果这样的话EQUALS()方法就没有了存在的意义,所以如果我们希望自定义判断相等的标准时,可以通过重写EQUALS方法来实现。重写EQUALS()方法时,相等条件是由业务要求决定的,因此EQUALS()方法的实现是由业务要求决定的。

13.说说static修饰符的用法

Java类中包含了成员变量、方法、构造器、初始化块和内部类(包括接口、枚举)5种成员,static关键字可以修饰除了构造器外的其他4种成员。

static关键字修饰的成员被称为类成员。类成员属于整个类,不属于单个对象。 static关键字有一条非常重要的规则,即类成员不能访问实例成员,因为类成员属于类的,类成员的作用域比实例成员的作用域更大,很容易出现类成员初始化完成时,但实例成员还没被初始化,这时如果类成员访问实例成员就会引起大量错误。

加分回答:static修饰的部分会和类同时被加载。被static修饰的成员先于对象存在,因此,当一个类加载完毕,即使没有创建对象也可以去访问被static修饰的部分。 静态方法中没有this关键词,因为静态方法是和类同时被加载的,而this是随着对象的创建存在的。静态比对象优先存在。也就是说,静态可以访问静态,但静态不能访问非静态而非静态可以访问静态。

14.请你说说final关键字

final关键字可以用来标值其修饰的类、方法和变量不可变。

  • 当final修饰类时,该类不能被继承。例如:java.lang.Math类
  • final修饰的方法不能被重写。
  • 当final用来修饰变量时,代表该变量不可被改变,一旦获得了初始值,该final变量的值就不能能被重新赋值。
  • final即可修饰成员变量(包括类变量和实例变量),也可以修饰局部变量、形参。

15.请你说说重载和重写的区别,构造方法能不能重写?

重载:重载要求发生在同一个类中,多个方法之间方法名相同且参数列表不同。注意重载与方法的返回值以及访问修饰符无关

重写:重写发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的方法名,参数列表必须与父类方法相同。另外,返回值要小于等于父类方法抛出的异常要小于等于父类方法访问修饰符则要大于等于父类方法若父类方法的访问修饰符为private,则子类不能对其重写

加分回答

同一个类中有多个构造器,多个构造器的形参列表不同就被称为构造器重载,构造器重载让Java类包含了多个初始化逻辑,从而允许使用不同的构造器来初始化对象。 构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。 父类方法和子类方法之间也有可能发生重载,因为子类会获得父类的方法,如果子类中定义了一个与父类方法名字相同但参数列表不同的方法,就会形成子类方法和父类方法的重载。

16.请说说你对java集合的了解

java中的集合类分为4大类,分别由4个接口来代表,它们是Set、List、Queue、Map。其中,Set、List、Queue都继承自Collection接口。

  • Set代表无序的、元素不可重复的集合。
  • List代表有序的、元素可以重复的集合。
  • Queue代表先进先出(FIFO)的队列
  • Map代表具有映射关系(key-value)的集合。

java提供了众多接口的实现类,它们都是这些接口的直接或间接的实现类,其中比较常用的有:HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap等。

加分回答:

  • 上面所说的集合类的接口或实现,都位于java.util包下,这些实现大多数都是非线程安全的。虽然非线程安全,但是这些类的性能较好。如果需要使用线程安全的集合类,则可以利用Collections工具类,该工具类提供的synchronizedXxx()方法,可以将这些集合类包装成线程安全的集合类。java.util包下的集合类中,也有少数的线程安全的集合类,例如Vector、Hashtable,它们都是非常古老的API。虽然它们是线程安全的,但是性能很差,已经不推荐使用了。
  • 从JDK 1.5开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。第一类是以降低锁粒度来提高并发性能的容器,它们的类名以Concurrent开头,如ConcurrentHashMap。第二类是采用写时复制技术实现的并发容器,它们的类名以CopyOnWrite开头,如CopyOnWriteArrayList。第三类是采用Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。

17.请你讲一下Java8的新特性

Java8是一个拥有丰富特性的版本,新增了很多特性

Lambda表达式:

  • 该新特性可以将功能视为方法参数,或者将代码视为数据。使用Lambda表达式,可以更简洁地表示单方法接口(称为功能接口)的实例。

方法引用:

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

Java8对接口进行了改进:

  • 允许在接口中定义默认方法,默认方法必须使用default修饰。

Stream API:

  • 新添加的Stream API(java.util.stream)支持对元素流进行函数式操作。Stream API 集成在Collections API中,可以对集合进行批量操作,例如顺序或并行的map-reduce转换。

Date Time API:

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

18.请你说说泛型、泛型擦除

Java在1.5版本中引入了泛型,在没有泛型之前,每次从集合中读取对象都必须进行类型转换,而这么做带来的结果就是:如果有人不小心插入了类型错误的对象,那么在运行时转换处理阶段就会出错。

在提出泛型之后,我们可以告诉编译器集合中接受哪些对象类型。编译器会自动的为你插入进行转化,并在编译时告知是否插入了类型错误的对象。这使程序变得更加安全更加清楚。

19.请你说说HashMap和Hashtable的区别

HashMap和Hashtable都是典型的Map实现,它们的区别在于是否线程安全,是否可以存入null值

是否线程安全:

  • Hashtable在实现Map接口时保证了线程安全性,而HashMap则是非线程安全的。所以,Hashtable的性能不如HashMap,因为为了保证线程安全它牺牲了一些性能。

是否可以存入null值:

  • Hashtable不允许存入null,无论是以null作为key或value,都会引发异常。而HashMao是允许存入null的,无论是以null作为key或value,都是可以的。

加分回答:

虽然HashMap是线程安全的,但任然不建议在多线程环境下使用Hashtable,因为它的同步方案还不成熟,性能不好。

如果要在多线程环境下使用HashMap,建议使用ConcurrentHashMap。它不但保证了线程安全,也通过降低锁的粒度提高了并发访问的性能。

20.HashMap是线程安全的吗?如果不是该如何解决?

不是线程安全的。

  1. 使用HashTable,效率极慢。不推荐使用
  2. 使用collections下的synchronizeXXX()方法将HashMap包装成线程安全的集合。
  3. 使用ConcurrentHashMap并发集合。主要采用锁定头结点方式降低锁的粒度,从而极大的提高了并发效率。

mysql

1.事务四大特性

  • 原子性(Atomicity) : 事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
  • 隔离性(Isolation):数据库提供的隔离机制,保证事务不在受外部并发操作影响的独立环境下运行。
  • 持久性(Durabilitry):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

2.并发事务问题

  • 脏读:一个事务读到另一个事务还没有提交的数据。
  • 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。
  • 幻读:一个事务按照条件查询时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了幻影。

3.事务隔离级别

image-20220830174652676

事务隔离级别越高,数据越安全,但是性能越低。

4.为什么InnoDB存储引擎选择使用B+tree索引结构?

  • 相对于二叉树,层级更少,搜索效率高;

  • 对于B-tree,无论是叶子节点还是非叶子节点,都会保存数据,这样导致一页中存储的键值减少,指针跟着减少,要同样保存大量数据,只能增加树的高度,导致性能降低。

  • B+tree不管查找哪一个数据,都要到叶子节点查询,搜索效率稳定。

  • 相对于Hash索引只支持等值匹配,B+tree叶子节点形成了一个双向链表,支持范围匹配和排序操作。

5.请你说说MySQL索引,以及它们的好处和坏处

得分点:检索效率、存储资源、索引维护

标准回答:索引就像指向表行的指针,是一种允许查询操作快速确定哪些行符合WHERE子句中的条件,并检索到这些行的其他列值的数据结构;

检索效率:索引主要有普通索引、唯一索引、主键索引、外键索引、全文索引、复合索引几种;在大数据量的查询中,合理使用索引的优点非常明显,不仅能大幅提高匹配where条件的检索效率,还能用于排序和分组操作的加速。

存储资源:但是索引如果使用不当也有比较大的坏处:比如索引必定会增加存储资源的消耗;

索引维护:同时也增大了插入、更新和删除操作的维护成本,因为每个增删改操作后相应列的索引都必须被更新。

加分回答只要创建了索引,就一定会走索引吗?

不一定。比如,在使用组合索引的时候,如果没有遵从“最左前缀”的原则进行搜索,则索引是不起作用的。数据库会评估使用索引和全盘扫描哪个更快,所以不一定会用到索引。

6.请你说说聚簇索引和非聚簇索引

它们两个的最大区别就是索引和数据是否存放在一起。

聚簇索引:索引和数据存放在一起,叶子节点保留数据行

非聚簇索引:索引和数据分开存放,叶子节点存放的是指向数据行的地址。

7.数据库为什么不用红黑树而用B+树?

索引的数据结构会被存储在磁盘中,每次查询都需要到磁盘中访问,对于红黑树,树的高度可能会非常的高,会进行很多次的磁盘IO,效率会非常低,B+树的高度一般为2-4,也就是说在最坏的条件下,也最多进行2到4次磁盘IO,这在实际中性能时非常不错的

8.请你说说innodb和myisam的区别?

InnoDB是具有事务、回滚和崩溃修复能力的事务安全型引擎,它可以实现行级锁来保证高性能的大量数据中的并发操作;

MyISAM是具有默认支持全文索引、压缩功能及较高查询性能非事务性引擎。

区别:

事务:InnoDB支持事务;MyISAM不支持。

数据锁:InnoDB支持行级锁;MyISAM只支持表级锁。

读写性能:InnoDB增删改性能更优;MyISAM查询性能更优。

全文索引:InnoDB不支持(但可通过插件等方式支持);MyISAM默认支持。

外键:InnoDB支持外键;MyISAM不支持。

存储结构:InnoDB在磁盘存储为一个文件;MyISAM在磁盘上存储成三个文件(表定义、数据、索引)。

存储空间:InnoDB需要更多的内存和存储;MyISAM支持支持三种不同的存储格式:静态表(默认)、动态表、压缩表。

移植:InnoDB在数据量小时可通过拷贝数据文件、备份 binlog、mysqldump工具移植,数据量大时比较麻烦;可单独对某个表通过拷贝表文件移植。

崩溃恢复:InnoDB有崩溃恢复机制;MyISAM没有。

InnoDB中行级锁是怎么实现的?

InnoDB行级锁是通过给索引上的索引项加锁来实现的。只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。 当表中锁定其中的某几行时,不同的事务可以使用不同的索引锁定不同的行。另外,不论使用主键索引、唯一索引还是普通索引,InnoDB都会使用行锁来对数据加锁。

9.请你说说索引怎么实现的B+树,为什么选这个数据结构

索引本质上就是通过预排序+树型结构来加快检索的效率,而MySQL中使用InnoDB和MyISA引擎都使用了B+树实现索引。

它是一颗平衡多路查找树,是在二叉查找树基础上的改进数据结构。在二叉查找树上查找一个数据时,最快情况的查找次数为树的深度,当数据量很大时,查询次数可能还是很大,造成大量的磁盘IO,从而影响查询效率;

为了减少磁盘IO的次数,必须降低树的深度,因此在二叉查找树基础上将树改成了多叉加上一些限制条件,就形成了B树;

B+树中所有叶子节点值的总集就是全部关键字集合;B+树为所有叶子节点增加了链接,从而实现了快速的范围查找;在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行连接。在数据库中,B+树的高度一般都在2~4层,这也就是说查找某一键值的行记录时最多只需要2到4次 IO。

在数据库中,B+树索引还可以分为聚集索引和辅助索引,但不管是聚集索引还是辅助索引,其内部都是B+树的,即高度平衡的,叶子节点存放着所有的数据。聚集索引与辅助索引不同的是,叶子节点存放的是否是一整行的信息。

多线程并发

1.说说怎么保证线程安全

得分点 原子类、volatile、锁

  • JDK从1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,按功能可以归纳为4种类型的原子更新方式,分别是原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组。无论原子更新哪种类型,都要遵循“比较和替换”规则,即比较要更新的值是否等于期望值,如果是则更新,如果不是则失败。
  • volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全。可见性问题是由处理器核心的缓存导致的,每个核心均有各自的缓存,而这些缓存均要与内存进行同步。volatile具有如下的内存语义:当写一个volatile变量时,该线程本地内存中的共享变量的值会被立刻刷新到主内存;当读一个volatile变量时,该线程本地内存会被置为无效,迫使线程直接从主内存中读取共享变量。
  • 原子类和volatile只能保证单个共享变量的线程安全,锁则可以保证临界区内的多个共享变量的线程安全,Java中加锁的方式有两种,分别是synchronized关键字和Lock接口。
    • synchronized是比较早期的API,在设计之初没有考虑到超时机制、非阻塞形式,以及多个条件变量。若想通过升级的方式让它支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码。因此,JDK的开发团队在1.5新增了Lock接口,并通过Lock支持了上述的功能,即:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。

2.请你说说线程和进程的区别

得分点:地址空间、开销、并发性、内存

标准回答:进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

  1. 进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间;

  2. 进程和线程切换时,需要切换进程和线程的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,耗费资源较大,效率要差一些;

  3. 进程的并发性较低,线程的并发性较高;

  4. 每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;

  5. 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源;

  6. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

3.请你说说多线程

  1. 线程是程序执行的最小单元,一个进程可以拥有多个线程
  2. 各个线程之间共享程序的内存空间(代码段、数据段和堆空间)和系统分配的资源(CPU,I/O,打开的文件),但是各个线程拥有自己的栈空间
  3. 多线程优点:减少程序响应时间;提高CPU利用率;创建和切换开销小;数据共享效率高;简化程序结构

4.请你说说死锁定义及发生的条件

得分点:争夺共享资源、相互等待、互斥条件、请求和保持条件、不剥夺条件、环路等待条件

  1. 死锁 两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。这些永远在互相等待的进程称为死锁进程。

  2. 产生死锁的必要条件 虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:

    • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放;
    • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
    • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
    • 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。

5.乐观锁vs悲观锁

  • 悲观锁的代表是 synchronized 和 Lock 锁

    • 其核心思想是【线程只有占有了锁,才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程,都得停下来等待】
    • 线程从运行到阻塞、再从阻塞到唤醒,涉及线程上下文切换,如果频繁发生,影响性能
    • 实际上,线程在获取 synchronized 和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞的机会
  • 乐观锁的代表是 AtomicInteger,使用 cas 来保证原子性

    • 其核心思想是【无需加锁,每次只有一个线程能成功修改共享变量,其它失败的线程不需要停止,不断重试直至成功】
    • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
    • 它需要多核 cpu 支持,且线程数不应超过 cpu 核数

6.乐观锁和悲观锁的应用场景

  • 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
  • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

总结:

悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低

乐观锁则适用于读多写少并发冲突少的场景

7.你知道哪些线程安全的集合?

java.util包下的集合类中,大部分都是非线程安全的,但也有少数的线程安全的集合类,例如Vector、Hashtable,它们都是非常古老的API。虽然它们是线程安全的,但是性能很差,已经不推荐使用了。对于这个包下非线程安全的集合,可以利用Collections工具类,该工具类提供的synchronizedXxx()方法,可以将这些集合类包装成线程安全的集合类。

在java5之后可以使用concurrent包提供的大量的支持并发访问的集合类,例如ConcurrentHashMap/CopyOnWriteArrayList

8.请你说说ConcurrentHashMap

ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树”的形式。

同时,它又采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。

线程安全的实现机制:

  1. 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换(原子操作,基于Unsafe类的原子操作API)。
  2. 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能很好
  3. 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。
  4. 查找数据时并不会加锁,所以性能很好。另外,在扩容的过程中,依然可以支持查找操作。

9.说说你了解的线程同步方式

Java通过加锁实现线程同步,锁有两类:synchronizedLock

  1. synchronized加在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同:

    加在普通方法上,则锁是当前的实例(this)。

    加在静态方法上,锁是当前类的Class对象

    加在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。

  2. Lock支持的功能包括:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。

10.说说synchronize的用法及原理

用法

  1. 静态方法上,则锁是当前类的Class对象。
  2. 作用在普通方法上,则锁是当前的实例(this)
  3. 作用在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。 能够保证同一个时刻只有一个线程执行该段代码,保证线程安全。 在执行完或者出现异常时自动释放锁。

原理:底层是采用Java对象头来存储锁信息的,并且还支持锁升级。在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步。

11.说说线程的状态

六种状态及转换

image-20210831090722658

分别是

  • 新建
    • 当一个线程对象被创建,但还未调用 start 方法时处于新建状态
    • 此时未与操作系统底层线程关联
  • 可运行
    • 调用了 start 方法,就会由新建进入可运行
    • 此时与底层线程关联,由操作系统调度执行
  • 终结
    • 线程内代码已经执行完毕,由可运行进入终结
    • 此时会取消与底层线程关联
  • 阻塞
    • 当获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,此时不占用 cpu 时间
    • 当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态
  • 等待
    • 当获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁进入 Monitor 等待集合等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的等待线程,恢复为可运行状态
  • 有时限等待
    • 当获取锁成功后,但由于条件不满足,调用了 wait(long) 方法,此时从可运行状态释放锁进入 Monitor 等待集合进行有时限等待,同样不占用 cpu 时间
    • 当其它持锁线程调用 notify() 或 notifyAll() 方法,会按照一定规则唤醒等待集合中的有时限等待线程,恢复为可运行状态,并重新去竞争锁
    • 如果等待超时,也会从有时限等待状态恢复为可运行状态,并重新去竞争锁
    • 还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,但与 Monitor 无关,不需要主动唤醒,超时时间到自然恢复为可运行状态

12.说说你对ThreadLocal的理解

作用

  • ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题
  • ThreadLocal 同时实现了线程内的资源共享

原理

每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

  • 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
  • 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
  • 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

ThreadLocalMap 的一些特点

  • key 的 hash 值统一分配
  • 初始容量 16,扩容因子 2/3,扩容容量翻倍
  • key 索引冲突后用开放寻址法解决冲突

弱引用 key

ThreadLocalMap 中的 key 被设计为弱引用,原因如下

  • Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存

内存释放时机

  • 被动 GC 释放 key
    • 仅是让 key 的内存释放,关联 value 的内存并不会释放
  • 懒惰被动释放 value
    • get key 时,发现是 null key,则释放其 value 内存
    • set key 时,会使用启发式扫描,清除临近的 null key 的 value 内存,启发次数与元素个数,是否发现 null key 有关
  • 主动 remove 释放 key,value
    • 会同时释放 key,value 的内存,也会清除临近的 null key 的 value 内存
    • 推荐使用它,因为一般使用 ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收

13.synchronized和Lock有什么区别

三个层面

不同点

  • 语法层面

    • synchronized是关键字,源码在jvm中,用c++语言实现
    • Lock是接口,源码由jdk提供,用java语言实现
    • 使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁
  • 功能层面

    • 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
    • Lock提供了许多synchronized不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
    • Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock
  • 性能层面

    • 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖
    • 在竞争激烈时,Lock的实现通常会提供更好的性能。

14.说说volatile的用法及原理

要求

  • 掌握线程安全要考虑的三个问题
  • 掌握 volatile 能解决哪些问题

原子性

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

可见性

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

有序性

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

15.请你说说JUC

JUC是java.util.concurrent的缩写,这个包是JDK 1.5提供的并发包,包内主要提供了支持并发操作的各种工具。这些工具大致分为如下5类:原子类、锁、线程池、并发容器、同步工具

  1. 原子类:从JDK 1.5开始,并发包下提供了atomic子包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组。
  2. :从JDK 1.5开始,并发包中新增了Lock接口以及相关实现类用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了多种synchronized关键字所不具备的同步特性,包括:可中断地获取锁、非阻塞地获取锁、可超时地获取锁。
  3. 线程池:从JDK 1.5开始,并发包下新增了内置的线程池。其中,ThreadPoolExecutor类代表常规的线程池,而它的子类ScheduledThreadPoolExecutor对定时任务提供了支持,在子类中我们可以周期性地重复执行某个任务,也可以延迟若干时间再执行某个任务。此外,Executors是一个用于创建线程池的工具类,由于该类创建出来的是带有无界队列的线程池,所以在使用时要慎重。
  4. 并发容器:从JDK 1.5开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。
    • 第一类是以降低锁粒度来提高并发性能的容器,它们的类名以Concurrent开头,如ConcurrentHashMap。
    • 第二类是采用写时复制技术实现的并发容器,它们的类名以CopyOnWrite开头,如CopyOnWriteArrayList。
    • 第三类是采用Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。
  5. 同步工具:从JDK 1.5开始,并发包下新增了几个有用的并发工具类,一样可以保证线程安全。其中,Semaphore类代表信号量,可以控制同时访问特定资源的线程数量;CountDownLatch类则允许一个或多个线程等待其他线程完成操作;CyclicBarrier可以让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。

16.说说你了解的线程通信方式(to do)

框架

1.说说你对MVC的理解

MVC是一种设计模式,将软件分为三层,分别是模型层,视图层,控制器层。其中模型层代表的是数据,视图层代表的是界面,控制器层代表的是逻辑处理,是连接视图与模型之前的桥梁。降低耦合,便于代码的维护

2.说说你对AOP的理解

得分点 AOP概念、AOP作用、AOP的实现方式

  • AOP面向切面编程。是spring两大核心之一,它是一种编程思想,是对OOP的一种补充。
  • 它在不改变原代码的前提下对其功能进行增强,它可以对业务逻辑的各个部分进行隔离,降低耦合,提高代码的可重用性。
  • 它的底层是通过动态代理实现的。它的应用场景有事务、日志管理等。

4.springboot有哪些常用注解?

  1. @SpringBootApplication注解: 在Spring Boot入口类中,唯一的一个注解就是@SpringBootApplication。它是Spring Boot项目的核心注解,用于开启自动配置,准确说是通过该注解内组合的@EnableAutoConfiguration开启了自动配置。
  2. @EnableAutoConfiguration注解: @EnableAutoConfiguration的主要功能是启动Spring应用程序上下文时进行自动配置,它会尝试猜测并配置项目可能需要的Bean。自动配置通常是基于项目classpath中引入的类和已定义的Bean来实现的。在此过程中,被自动配置的组件来自项目自身和项目依赖的jar包中。
  3. @Import注解: @EnableAutoConfiguration的关键功能是通过@Import注解导入的ImportSelector来完成的。从源代码得知@Import(AutoConfigurationImportSelector.class)是@EnableAutoConfiguration注解的组成部分,也是自动配置功能的核心实现者。
  4. @Conditional注解: @Conditional注解是由Spring 4.0版本引入的新特性,可根据是否满足指定的条件来决定是否进行Bean的实例化及装配,比如,设定当类路径下包含某个jar包的时候才会对注解的类进行实例化操作。总之,就是根据一些特定条件来控制Bean实例化的行为。

5.说说你对IoC的理解

IoC:控制反转。控制:对象的创建的控制权限;反转:将对象的控制权限交给spring。

之前我们创建对象时用new,现在直接从spring容器中取,维护对象之间的依赖关系,降低对象之间的耦合度。 实现方式为DI,依赖注入,有三种注入方式:构造器、setter、接口注入

6.@Bean和@Autowired、@Resource之间的区别

1.提供方不同
@Autowired 是Spring提供的,@Resource 是J2EE提供的。

2.装配时默认类型不同
@Autowired只按type装配,@Resource默认是按name装配。

@Bean 修饰的方法表示初始化一个对象并交由Spring IOC去管理,@Bean 只能和@Component @Repository @Controller @Service @Configration 配合使用.

3.使用区别

@Bean 告诉Spring’这是此类的一个实例,请保留该类,并在我询问时将其还给我’。

@Autowired说“请给我一个该类的实例,例如,我@Bean之前用注释创建的一个实例”。

(1)@Autowired与@Resource都可以用来装配bean,都可以写在字段或setter方法上

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

(3)@Resource,默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行名称查找。如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。

7.bean的作用范围

在Spring中,bean作用域用于确定哪种类型的bean实例应该从Spring容器中返回给调用者。

目前Spring Bean的作用域或者说范围主要有五种。

作用域 描述
singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,bean作用域范围的默认值。
prototype 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()。
request 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于web的Spring WebApplicationContext环境。
session 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean。该作用域仅适用于web的Spring WebApplicationContext环境。
application 限定一个Bean的作用域为ServletContext的生命周期。该作用域仅适用于web的Spring WebApplicationContext环境。

8.spring的bean什么时候用单例,什么时候用多例?

当对象含有可改变状态时(在实际应用中该状态会改变),则多例,否则单例。例如dao和service层的数据一般不会有响应的属性改变,所以考虑单例,而controller层会存储很多需要操作的vo类,此时这个对象的状态就会被改变,则需要使用多例

9.spring的bean为什么是单例的?

答:为了提高性能。

  • 由于不会每次都新创建新对象,所以就减少了新生成实例的消耗。因为spring会通过反射或者cglib来生成bean实例这都是耗性能的操作,其次给对象分配内存也会涉及复杂算法。

  • 减少JVM垃圾回收,由于不会给每个请求都新生成bean实例,所以自然回收的对象少了。

  • 可以快速获取到bean,因为单例的获取bean操作除了第一次生成之外其余的都是从缓存里获取的所以很快。

  • 缺点就是在并发环境下可能会出现线程安全问题

10.说说bean的生命周期

Bean 生命周期大致分为 Bean 定义、Bean 的初始化、Bean的生存期Bean 的销毁4个部分。

具体步骤如下:

  1. spring启动,查找并加载需要被spring管理的bean,进行bean的实例化

  2. bean实例化后将bean的引入和值注入到bean的属性中

  3. 如果bean实现了BeanNameAware接口的话,spring将bean的id传给setBeanName()方法

  4. 如果bean实现了BeanFactoryAware接口的话,spring将调用setBeanFactory()方法,将BeanFactory容器实例传入

  5. 如果bean实现了ApplicationContextAware接口的话,spring将调用bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来

  6. 如果bean实现了BeanPostProcessor接口,spring就将调用他们的postProcessBeforeInitialization()方法。

  7. 如果bean实现了InitializingBean接口,spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用initmenthod声明了初始化方法,该方法也会被调用

  8. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。

  9. 此时,bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。

  10. 如果bean实现了DisposableBean接口,spring将调用它的destory()接口方法,同样,如果bean使用了destory-method声明销毁方法,该方法也会被调用。

Redis

1.说说Redis的持久化策略

reids的持久化方式有两种:RDBAOF

  • RDB:RDB持久化是将当前进程数据以生成快照的方式保存到硬盘的过程,也是Redis默认的持久化机制。RDB会创建一个经过压缩的二进制文件,这个文件以’.rdb‘结尾,内部存储了各个数据库的键值对等信息。

​ 好处:只有一个dump.rdb文件,便于存储,容灾性较好,性能最大化,子进程来完成写操作,主进程继续处理命令。

​ 缺点:数据安全性低,RDB是隔一段时间进行一次备份,在此期间,如果发生了异常,可能导致数据的不完整性

  • AOF:AOF以独立日志的方式记录了每次写入的命令,重启时再重新执行AOF文件中的命令来恢复数据。

​ 优点:AOF持久化的优点是与RDB持久化可能丢失大量的数据相比,AOF持久化的安全性要高很多。通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1秒之内。

​ 缺点:其缺点则是,AOF文件存储的是协议文本,它的体积要比二进制格式的”.rdb”文件大很多。AOF需要通过执行AOF文件中的命令来恢复数据库,其恢复速度比RDB慢很多。

2.如何利用Redis实现一个分布式锁?

在分布式的环境下,会发生多个server并发修改同一个资源的情况,这种情况下,由于多个server是多个不同的JRE环境,而Java自带的锁局限于当前JRE,所以Java自带的锁机制在这个场景下是无效的,那么就需要我们自己来实现一个分布式锁。

采用Redis实现分布式锁,我们可以在Redis中存一份代表锁的数据,数据格式通常使用字符串即可。

  1. 加锁:setnx(key,1),解锁:del(key),问题:如果客户忘记解锁,将会出现死锁。
  2. 加锁:setnx(key,1)+expire(key,30),解锁:del(key)。问题:由于setnx和expire的非原子性,当第二步挂掉,仍然会出现死锁。
  3. 加锁:将setnx和expire变成原子性操作,set(key,1,30,NX),解锁:del(key)。问题:考虑到线程A还在执行,但是锁已经到期,当线程A执行结束时去释放锁时,可能就会释放别的线程锁。我们可以在加锁时为key赋一个随机值,来充当进程的标识,在解锁时要先判断一下value值,看是不是自己的锁,如果是,再进行删除。
  4. 当进程解锁的时候进行判断,是自己持有的锁才能释放,否则不能释放。另外判断,释放这两步需要保持原子性,否则如果第二步失败,就会造成死锁。而获取和删除命令不是原子的,这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。

3.请你说说Redis的数据类型

Redis主要提供了5种数据结构:字符串(String)、哈希(Hash)、列表(List)、集合(set)、有序集合(zset)

Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:

1652887393157

五种基本数据类型:

  • String:String是Redis中最基本的数据类型,可以存储任何数据,包括二进制数据、序列化的数据、JSON化的对象甚至是图片。
  • List:List是字符串列表,按照插入的顺序排序,元素可以重复,你可以添加一个元素到列表的头部或者尾部,底层是一个链表结构。
  • Set:Set是一个无序不重复的集合。
  • Hash:Hash是String类型的filed和value的集合,适合用于存储对象。
  • Sortedset:Zset和set一样也是String类型元素的集合,且不允许有重复的元素,但不同的是Zset的每个元素都会关联一个分数,分数可以重复,Redis通过分数来为集合汇总的成员进行从小到大的排序。

4.说说缓存穿透、击穿、雪崩的区别

缓存穿透

是指客户端查询了根本不存在的数据,使得这个请求直达存储层,导致其负载过大甚至造成宕机。这种情况可能是由于业务层误将缓存和库中的数据删除造成的,当然也不排除有人恶意攻击,专门访问库中不存在的数据导致缓存穿透。

我们可以通过缓存空对象的方式和布隆过滤器两种方式来解决这一问题。

  • 缓存空对象是指当存储层未命中后,仍然将空值存入缓存层 ,当客户端再次访问数据时,缓存层直接返回空值。还可以将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。

缓存击穿

当一份访问量非常大的热点数据缓存失效的瞬间,大量的请求直达存储层,导致服务崩溃。

  • 缓存击穿可以通过热点数据不设置过期时间来解决,这样就不会出现上述的问题,这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存

  • 除了永不过期的方式,我们也可以通过加互斥锁的方式来解决缓存击穿,即对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。

缓存雪崩:

是指当某一时刻缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。

缓存雪崩的解决方式有三种

  • 第一种是在设置过期时间时,附加一个随机数避免大量的key同时过期
  • 第二种是启用降级和熔断措施,即发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。
  • 第三种是构建高可用的Redis服务,也就是采用哨兵或集群模式,部署多个Redis实例,这样即使个别节点宕机,依然可以保持服务的整体可用。

5.Redis如何与数据库保持双写一致性

共有四种同步策略,即先更新缓存再更新数据库先更新数据库再更新缓存先删除缓存再更新数据库先更新数据库再删除缓存

先更新缓存的优点是每次数据变化时都能及时地更新缓存,这样不容易出现查询未命中的情况,但这种操作的消耗很大,如果数据需要经过复杂的计算再写入缓存的话,频繁的更新缓存会影响到服务器的性能。如果是写入数据比较频繁的场景,可能会导致频繁的更新缓存却没有业务来读取该数据。

删除缓存的优点是操作简单,无论更新的操作复杂与否,都是直接删除缓存中的数据。这种做法的缺点则是,当删除了缓存之后,下一次查询容易出现未命中的情况,,那么这时就需要再次读取数据库。对比而言,删除缓存无疑是更好的选择。

先操作数据库和后操作数据库的区别:

先删除缓存再操作数据库的话,如果第二步骤失败可能导致缓存和数据库得到相同的旧数据。

先操作数据库但删除缓存失败的话则会导致缓存和数据库得到的结果不一致。我们一般采用重试机制解决,而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行。

结论:先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。

6.请你说说Redis数据类型中的zset,它和set有什么区别?底层是怎么实现的?

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。Redis 正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数 ( score ) 却可以重复

zset底层的存储结构包括ziplistskiplist,在同时满足有序集合保存的元素数量小于128个和有序集合保存的所有元素的长度小于64字节的时候使用ziplist,其他时候使用skiplist。

  • 当ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。

  • 当skiplist作为zset的底层存储结构的时候,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系。

加分回答:实际上单独使用Hashmap或skiplist也可以实现有序集合,Redis使用两种数据结构组合的原因是:如果我们单独使用Hashmap,虽然能以O(1) 的时间复杂度查找成员的分值,但是因为Hashmap是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;而如果单独使用skiplist,虽然能执行范围操作,但查找操作的复杂度却由 O(1)变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。

7.说说Redis的单线程架构

Redis的网络IO和键值对读写是由一个线程来完成的,但Redis的其他功能,例如持久化、异步删除、集群数据同步等操作依赖于其他线程来执行。

单线程可以简化数据结构和算法的实现并且可以避免线程切换和竞争造成的消耗。但要注意如果某个命令执行时间过长,会造成其他命令的阻塞。Redis采用了io多路复用机制,这带给了redis并发处理大量客户端请求的能力

Redis单线程实现为什么这么快呢?

  • 单线程避免了线程切换和竞争所产生的消耗。
  • Redis的大部分操作是在内存尚完成的。
  • Redis还采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。

加分回答:Redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的。而Redis的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。所以,说Redis是单线程的只是一种习惯的说法,事实上它的底层不是单线程的。

8.如何实现Redis高可用

主要有哨兵和集群两种方式可以实现Redis高可用

哨兵

  • 哨兵模式是一个分布式架构,它包含若干个哨兵节点和数据节点,每一个哨兵节点都监控着其他的数据节点和哨兵节点,当发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它就会与其他哨兵节点协商,可以避免误判,当大多数哨兵节点都认为主节点不可达时,它们便会选择出一个哨兵节点来做自动故障转移工作,可以将从节点晋升为主节点,同时还会实时的通知到应用方,整个过程自动的,实现高可用。

哨兵节点包含如下特征:

  1. 哨兵节点会定期监控数据节点,其他哨兵节点是否可达;
  2. 哨兵节点会将故障转移的结果通知给应用方;
  3. 哨兵节点可以将从节点晋升维主节点,并维护后续正确的主从关系;
  4. 哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点信息;
  5. 节点的故障判断是由多个哨兵节点共同完成的,可有效地防止误判;
  6. 哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用,整个集合依然是健壮的;
  7. 哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令。

集群

  • Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内。每一个节点负责维护一部分槽以及槽所映射的键值数据。

虚拟槽分区具有如下特点:

  1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
  2. 节点自身维护槽的映射关系,不需要客户端或者代理服务器维护槽分区元数据;
  3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

JVM

1.说说你了解的JVM内存模型

JVM由三部分组成:类加载子系统执行引擎运行时数据区

1、类加载子系统:可以根据指定的全限定名来载入类或接口。

2、执行引擎:负责执行那些包含在被载入类的方法中的指令。

3、运行时数据区:分为方法区虚拟机栈本地方法栈程序计数器。当程序运行时,JVM需要内存来存储许多内容,例如:字节码、对象、参数、返回值、局部变量、运算的中间结果等,把这些东西都存储到运行时数据区中,以便于管理。

2.说说JVM的垃圾回收机制(to do)

3.说说类加载机制

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接,而前五个阶段则是类加载的完整过程。

img

  1. 在加载阶段JVM需要在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
  2. 验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
  3. 准备阶段是正式为类中定义变量(静态变量)分配到内存并设置类变量初始值的阶段,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域。
  4. 解析阶段是Java虚拟机将常量池内的符号替换为直接引用的过程,符号引用以一组符号来描述所引用的目标,直接引用是可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。
  5. 类的初始化阶段是类加载过程的最后一个步骤,直到初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序。本质上,初始化阶段就是执行类构造器<clinit>()的过程。<clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。

4.说说JVM的垃圾回收算法

标记清除法、标记整理法、标记赋值法

1.标记清除法

image-20210831211008162

解释:

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

要点:

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

2.标记整理法

image-20210831211641241

解释:

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

特点:

  • 标记速度与存活对象线性关系

  • 清除与整理速度与内存大小成线性关系

  • 缺点是性能上较慢

3.标记复制法

image-20210831212125813

解释:

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

特点:

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

6. 四种引用

强引用

  1. 普通变量赋值即为强引用,如 A a = new A();

  2. 通过 GC Root 的引用链,如果强引用不到该对象,该对象才能被回收

image-20210901111903574

软引用(SoftReference)

  1. 例如:SoftReference a = new SoftReference(new A());

  2. 如果仅有软引用该对象时,首次垃圾回收不会回收该对象,如果内存仍不足,再次回收时才会释放对象

  3. 软引用自身需要配合引用队列来释放

  4. 典型例子是反射数据

image-20210901111957328

弱引用(WeakReference)

  1. 例如:WeakReference a = new WeakReference(new A());

  2. 如果仅有弱引用引用该对象时,只要发生垃圾回收,就会释放该对象

  3. 弱引用自身需要配合引用队列来释放

  4. 典型例子是 ThreadLocalMap 中的 Entry 对象

image-20210901112107707

虚引用(PhantomReference)

  1. 例如: PhantomReference a = new PhantomReference(new A(), referenceQueue);

  2. 必须配合引用队列一起使用,当虚引用所引用的对象被回收时,由 Reference Handler 线程将虚引用对象入队,这样就可以知道哪些对象被回收,从而对它们关联的资源做进一步处理

  3. 典型例子是 Cleaner 释放 DirectByteBuffer 关联的直接内存

image-20210901112157901

版权声明:本文为jonah-liu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/jonah-liu/p/jonah.html