Java基础和数据库

HeJin大约 68 分钟

Java基础和数据库

一、基础

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

  • 在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机
  • 由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。跨平台

02.== 和 equals() 的区别

  • == 对于基本类型和引用类型的作用效果是不同的:
    • 对于基本数据类型来说,== 比较的是值。
    • 对于引用数据类型来说,== 比较的是对象的内存地址。
  • equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。
    • 类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
    • 类重写了 equals()方法 :一般我们都重写 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

03.hashCode() 与 equals()

hashCode() 有什么用?

  • hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
  • hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。
  • 两个对象的hashCode 值相等并不代表两个对象就相等。
    • 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
    • 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
    • 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。

为什么重写 equals() 时必须重写 hashCode() 方法?

  • equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
  • 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。

  • HashMap添加数据的时候,会先比较hashCode值和equals。因为HashMap的key是不允许重复的。
  • 如果没有重写hashCode() 方法,就会出现重复的key被添加。

04.包装类型的常量池技术了解么?

  • Java 基本类型的包装类的大部分都实现了常量池技术。
  • Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在 [0,127] 范围的缓存数据,Boolean 直接返回 True or False
  • 如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。
  • 所有整型包装类对象之间值的比较,全部使用 equals 方法比较。

05.自动装箱与拆箱了解吗?原理是什么?

  • 什么是自动拆装箱?
    • 装箱:将基本类型用它们对应的引用类型包装起来。
    • 拆箱:将包装类型转换为基本数据类型。
  • 装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。
  • 如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。

06.面向对象三大特征

  • 封装
  • 继承
  • 多态

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

  • 共同点 :
    • 都不能被实例化。
    • 都可以包含抽象方法。
    • 都可以有默认实现的方法(Java 8 可以用 default 关键在接口中定义默认方法)。
  • 区别 :
    • 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。
    • 抽象类主要用于代码复用,强调的是所属关系(比如说我们抽象了一个发送短信的抽象类,)。
    • 一个类只能继承一个类,但是可以实现多个接口。
    • 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。

08.深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

  • 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
  • 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
  • 引用拷贝就是两个不同的引用指向同一个对象。

09.Java 常见对象

Object

Object 类是一个特殊的类,是所有类的父类。

public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。

public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的HashMap。
public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用户比较字符串的值是否相等。

protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。

public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个方法。

public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。

public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。

public final native void wait(long timeout) throws InterruptedException//native方法,并且不能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。

public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。

public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直等待,没有超时时间这个概念

protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作

String

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

String 真正不可变有下面几点原因:

  • 保存字符串的数组被 final 修饰且为私有的,并且String 类没有提供/暴露修改这个字符串的方法。
  • String 类被 final 修饰导致其不能被继承,进而避免了子类破坏 String 不可变。

对于三者使用的总结:

  • 操作少量的数据: 适用 String。
  • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder。
  • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer。

字符串拼接用“+” 还是 StringBuilder?

  • Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的元素符。
  • 对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
  • 不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder 以复用,会导致创建过多的 StringBuilder 对象。

String#equals() 和 Object#equals() 有何区别?

String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Objectequals 方法是比较的对象的内存地址。

字符串常量池的作用了解吗?

  • 字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建

10.泛型

Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?

  • 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
  • Java 的泛型是伪泛型,因为 Java 在运行期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除。
  • 泛型一般有三种使用方式: 泛型类、泛型接口、泛型方法。

11.反射

何为反射?

反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。

反射机制优缺点

  • 优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。
  • 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

反射的应用场景

  • 框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
  • 注解的实现也用到了反射。

12.注解

注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value 、@Component)都是通过反射来进行处理的。

13.异常

Exception 和 Error 有什么区别?

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:

  • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
  • Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

Checked Exception 和 Unchecked Exception 有什么区别?

  • Checked Exception 即受检查异常,Java 代码在编译过程中,如果受检查异常没有被 catch/throw 处理的话,就没办法通过编译 。
  • 除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException...。
  • Unchecked Exception 即 不受检查异常 ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。RuntimeException 及其子类都统称为非受检查异常。

14.I/O

什么是序列化?什么是反序列化?

  • 序列化: 将数据结构或对象转换成二进制字节流的过程。
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。

Java 序列化中如果有些字段不想进行序列化,怎么办?

  • 对于不想进行序列化的变量,使用 transient 关键字修饰。

既然有了字节流,为什么还要有字符流?

  • 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。
  • 所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

15.为什么 Java 中只有值传递?

Java 中将实参传递给方法(或函数)的方式是 值传递

  • 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
  • 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。

二、集合

16.集合框架底层数据结构总结

List

  • ArraylistObject[] 数组。
  • VectorObject[] 数组。
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。

Set

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)。

Map

  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
  • LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
  • Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的。
  • TreeMap: 红黑树(自平衡的排序二叉树)。

17.为什么要使用集合?

  • 当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
  • 数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 但是集合提高了数据存储的灵活性,Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据。

18.Collection 子接口之 List

Arraylist 和 Vector 的区别?

  • ArrayListList 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 ;
  • VectorList 的古老实现类,底层使用Object[ ] 存储,线程安全的。

19.Arraylist 与 LinkedList 区别?

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全。
  • 底层数据结构: Arraylist 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表。
  • 是否支持快速随机访问。
  • 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

20.ArrayList 的扩容机制

21.comparable 和 Comparator 的区别

  • comparable 接口实际上是出自java.lang包 它有一个 compareTo(Object obj)方法用来排序。
  • comparator接口实际上是出自 java.util 包它有一个compare(Object obj1, Object obj2)方法用来排序。

22.无序性和不可重复性的含义是什么

  • 什么是无序性?无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
  • 什么是不可重复性?不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写 equals()方法和 HashCode()方法。

23.比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同

  • HashSetLinkedHashSetTreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • HashSetLinkedHashSetTreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。
  • 底层数据结构不同又导致这三者的应用场景不同。HashSet 用于不需要保证元素插入和取出顺序的场景,LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。

24.HashMap 和 HashSet 区别

HashSet 底层就是基于 HashMap 实现的。

25.HashSet 如何检查重复

当你把对象加入HashSet时,HashSet 会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcodeHashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。

26.HashMap 的底层实现

  • JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。
  • JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

27.HashMap 的长度为什么是 2 的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。数组下标的计算方法是“ (n - 1) & hash”。(hash%length==hash&(length-1))

28.HashMap 多线程操作导致死循环问题

29.HashMap 有哪几种常见的遍历方式?

30.ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同:

  • ConcurrentHashMap并发控制使用 synchronized 和 CAS 来操作。
  • Hashtable :使用 synchronized 来保证线程安全,效率非常低下。

31.Collections 工具类常用方法

  • 排序。
  • 查找,替换操作。
  • 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)。

三、并发编程

32.请简要描述线程与进程的关系,区别及优缺点?

  • 一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
    • 程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
    • 为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
  • 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

33.为什么要使用多线程呢?

利用好多线程机制可以大大提高系统整体的并发能力以及性能。

34.使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。

35.说说线程的生命周期和状态?

  • 线程创建之后它将处于 NEW(新建) 状态,调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
  • 当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。
  • 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()方法之后将会进入到 TERMINATED(终止) 状态。

36.什么是上下文切换?

  • 线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
  • 上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

37.什么是线程死锁?如何避免死锁?

  • 线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
  • 破坏死锁的产生的必要条件即可:
    • 破坏请求与保持条件 :一次性申请所有的资源。
    • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
    • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

38.说说 sleep() 方法和 wait() 方法区别和共同点?

  • 两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁
  • 两者都可以暂停线程的执行。
  • wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

39.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

40.说一说自己对于 synchronized 关键字的了解

synchronized 关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

41.说说自己是怎么使用 synchronized 关键字

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁。
  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
  • 修饰代码块 :指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。

42.构造方法可以使用 synchronized 关键字修饰么?

  • 构造方法不能使用 synchronized 关键字修饰。

  • 构造方法本身就属于线程安全的,不存在同步的构造方法一说。

43.讲一下 synchronized 关键字的底层原理

44.说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

  • 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。
  • 锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

45.谈谈 synchronized 和 ReentrantLock 的区别

  • 两者都是可重入锁。
    • 自己可以再次获取自己的内部锁。
    • 同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
  • synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API。
  • ReentrantLock 比 synchronized 增加了一些高级功能:
    • 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
    • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。
    • 实现选择性通知(锁可以绑定多个条件)。

46.CPU 缓存模型

  • CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。
  • CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。
  • CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。

47.讲一下 JMM(Java 内存模型)

  • Java 内存模型主要目的是为了屏蔽系统和硬件的差异,避免一套代码在不同的平台下产生的效果不一致。
  • 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
  • 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
  • 数据的不一致:要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。

48.并发编程的三个重要特性

  1. 可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  2. 有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。
  3. 原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized 可以保证代码片段的原子性。

49.说说 synchronized 关键字和 volatile 关键字的区别

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile 性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

50.ThreadLocal

  • 如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,不会刷回到主存,从而避免了线程安全问题。
  • ThreadLocal 原理:
  • ThreadLocal 内存泄露问题:
    • ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
    • 这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。
    • ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法。

51.为什么要用线程池?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

52.实现 Runnable 接口和 Callable 接口的区别

  • Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以。
  • 所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口 ,这样代码看起来会更加简洁。

53.执行 execute()方法和 submit()方法的区别是什么呢?

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否。
  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

54.如何创建线程池

通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

55.线程池的七个参数

ThreadPoolExecutor

public ThreadPoolExecutor
    (int corePoolSize,
     int maximumPoolSize,
     long keepAliveTime,
     TimeUnit unit,
     BlockingQueue<Runnable> workQueue,
     ThreadFactory threadFactory,
     RejectedExecutionHandler handler)
corePoolSize指定了线程池里的线程数量,核心线程池大小
maximumPoolSize指定了线程池里的最大线程数量
keepAliveTime当线程池线程数量大于corePoolSize时候,多出来的空闲线程,多长时间会被销毁
unit时间单位,TimeUnit
workQueue任务队列,用于存放提交但是尚未被执行的任务
threadFactory线程工厂,用于创建线程,线程工厂就是给我们new线程的
handler所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略

56.CAS和AQS

  • CAS:是给一个元素赋值的时候,先看看内存里的那个值到底变没变,如果没变我就修改,变了我就不改了,其实这是一种无锁操作,不需要挂起线程,无锁的思路就是先尝试,如果失败了,进行补偿,也就是你可以继续尝试。这样在少量竞争的情况下能很大程度提升性能。
  • 抽象队列同步器,用来解决线程同步执行的问题。

57.用过 CountDownLatch 么?什么场景下用的?

  • 这个类常常用于等待,等多个线程执行完毕,再让某个线程执行。
  • CountDownLatch的典型用法就是:某一线程在开始运行前等待n个线程执行完毕。
  • CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。
  • CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

四、JVM

58.介绍下 Java 内存区域(运行时数据区)

  • 线程私有的:

    • 程序计数器

    • 虚拟机栈

    • 本地方法栈

  • 线程共享的:

    • 方法区

    • 直接内存 (非运行时数据区的一部分)

程序计数器

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

虚拟机栈

  • Java 虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。
  • 局部变量表主要存放了编译期可知的各种数据类型、对象引用。
  • 那么方法/函数如何调用?
    • Java 栈中保存的主要内容是栈帧,每一次函数调用都会有一个对应的栈帧被压入 Java 栈,每一个函数调用结束后,都会有一个栈帧被弹出。
    • Java 方法有两种返回方式:return 语句。抛出异常。不管哪种返回方式都会导致栈帧被弹出。

本地方法栈

虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。

  • Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
  • Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。

方法区

方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池

  • 运行时常量池是方法区的一部分。
  • Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)。

59.对象的创建

  • 类加载检查:虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • 分配内存:在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
  • 初始化零值:内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
  • 执行 init 方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

60.字符串常量池常见问题

String 类型的变量和常量做“+”运算时发生了什么?

  • 对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
  • 引用的值在程序编译期是无法确定的,编译器无法对其进行优化。
  • 对象引用和“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。
  • 字符串使用 final 关键字声明之后,可以让编译器当做常量来处理。

61.类加载过程

Class 文件需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些 Class 文件呢?

系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。

五、数据库

MySQL

62.锁机制与 InnoDB 锁算法

表级锁和行级锁对比:

  • 表级锁: MySQL 中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
  • 行级锁: MySQL 中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。

InnoDB 存储引擎的锁的算法有三种:

  • Record lock:记录锁,单个行记录上的锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key lock:record+gap 临键锁,锁定一个范围,包含记录本身。

63.查询缓存

执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用。

64.何为事务?

  • 事务是逻辑上的一组操作,要么都执行,要么都不执行。
  • 事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:将小明的余额减少 1000 元;将小红的余额增加 1000 元。事务会把这两个操作就可以看成逻辑上的一个整体,这个整体包含的操作要么都成功,要么都要失败。这样就不会出现小明余额减少而小红的余额却并没有增加的情况。

65.何为数据库事务?

数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行 。

66.何为 ACID 特性呢?

  • 原子性(Atomicity) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用。
  • 一致性(Consistency): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的。
  • 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的。
  • 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

67.数据事务的实现原理呢?

  • MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
  • MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 REPEATABLE-READ )。
  • 保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。

68.并发事务带来哪些问题?

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。
  • 不可重复读(Unrepeatable read): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

69.事务隔离级别有哪些?

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

70.MySQL 的默认隔离级别是什么?

  • MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过SELECT @@tx_isolation;命令来查看,MySQL 8.0 该命令改为SELECT @@transaction_isolation。
  • MySQL InnoDB 的 REPEATABLE-READ(可重读)并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁读使用到的机制就是 Next-Key Locks。

71.MySQL 索引详解

何为索引?有什么作用?

  • 索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。
  • 索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。

索引的优缺点

优点 :

  • 使用索引可以大大加快 数据的检索速度(大大减少检索的数据量), 这也是创建索引的最主要的原因。
  • 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。

缺点 :

  • 创建索引和维护索引需要耗费许多时间。当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
  • 索引需要使用物理文件存储,也会耗费一定空间。

但是,使用索引一定能提高查询性能吗?

大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。

索引的底层数据结构

Hash表 & B+树

  • 哈希表是键值对的集合,通过键(key)即可快速取出对应的值(value),因此哈希表可以快速检索数据(接近 O(1))。
  • 为何能够通过 key 快速取出 value呢? 原因在于 哈希算法(也叫散列算法)。通过哈希算法,我们可以快速找到 key 对应的 index,找到了 index 也就找到了对应的 value。
  • 但是!哈希算法有个 Hash 冲突 问题,也就是说多个不同的 key 最后得到的 index 相同。通常情况下,我们常用的解决办法是 链地址法。链地址法就是将哈希冲突数据存放在链表中。就比如 JDK1.8 之前 HashMap 就是通过链地址法来解决哈希冲突的。不过,JDK1.8 以后HashMap为了减少链表过长的时候搜索时间过长引入了红黑树。

既然哈希表这么快,为什么MySQL 没有使用其作为索引的数据结构呢?

  • Hash 冲突问题 :我们上面也提到过Hash 冲突了,不过对于数据库来说这还不算最大的缺点。
  • Hash 索引不支持顺序和范围查询(Hash 索引不支持顺序和范围查询是它最大的缺点: 假如我们要对表中的数据进行排序或者进行范围查询,那 Hash 索引可就不行了。

B 树& B+树

  • B 树也称 B-树,全称为 多路平衡查找树 ,B+ 树是 B 树的一种变体。B 树和 B+树中的 B 是 Balanced (平衡)的意思。目前大部分数据库系统及文件系统都采用 B-Tree 或其变种 B+Tree 作为索引结构。

B 树& B+树两者有何异同呢?

  • B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
  • B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
  • B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。

索引类型

聚簇索引和非聚簇索引

  • 【主键和数据】共存的索引被称之为【聚簇索引】,其他的,比如我们使用【姓名列+主键】建立的索引,可以称为【非聚簇索引】,或者【辅助索引】,或者【二级索引】,同时聚簇索引只有在innodb引擎中才存在,而在myIsam中是不存在的。

二级索引

  • 普通索引 (常规索引)(normal):就是普普通通的索引,没有什么特殊要求,理论上任何列都可以当做普通索引,有的列【数据量比较大】,使用前几个字符就能【很快标识】出来一行数据,那我们就可以使用这种方式建立索引,比如我们的邮箱,邮箱很多后缀是相同的我们完全可以忽略。
  • 唯一索引(UNIQUE ):对列的要求:索引列的值不能重复。
  • 复合索引(联合索引):当【查询语句】中包含【多个查询条件,且查询的顺序基本保持一致】时,我们推荐使用复合索引,索引的【组合使用】效率是低于【复合索引】的。
  • 全文索引(FULLTEXT):做全文检索(不如百度的搜索功能)使用的索引,但是这种场景,我们有更好的替代品,如:ElacticSearch,所以实际使用不多,只当了解。
  • hash索引:hash索引是Memory存储引擎的默认方式,而且只有memory引擎支持hash索引,memory的数据是放在内存中的,一旦服务关闭,表中的数据就会丢失。
  • 空间索引(SPATIAL):MySQL在5.7之后的版本支持了空间索引,而且支持OpenGIS几何数据模型。这是在地理位置领域使用的一种索引,其他场景用的很少,所以不需要深入学习。
  • 覆盖索引:覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了, 而无需回表查询。

explain的用法

explain关键字可以模拟MySQL优化器执行SQL语句,可以很好的分析SQL语句或表结构的性能瓶颈。explain的使用很简单,只需要在目标sql前加上这个关键字就可以了。

执行explain会产生以下11列内容,如下:

列号说明
1idselect查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序
2select_type查询类型
3table正在访问哪个表
4partitions匹配的分区
5type/访问的类型
6possible_keys显示可能应用在这张表中的索引,一个或多个,但不一定实际使用到
7key实际使用到的索引,如果为NULL,则没有使用索引
8key_len表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度
9ref显示索引的哪一列被使用了,如果可能的话,是一个常数,哪些列或常量被用于查找索引列上的值
10rows根据表统计信息及索引选用情况,大致估算出找到所需的记录所需读取的行数 filtered //查询的表行占表的百分比
11filtered查询的表行占表的百分比
12Extra包含不适合在其它列中显示但十分重要的额外信息

重点是type字段,最好到最差:掌握以下10种常见的即可NULL>system>const>eq_ref>ref>ref_or_null>index_merge>range>index>ALL

使用索引的问题

  • 哪些情况下适合建索引

    • 频繁作为where条件语句查询的字段。

    • 关联字段需要建立索引。

    • 分组,排序字段可以建立索引。

    • 统计字段可以建立索引,例如count(),max()等。

  • 哪些情况下不适合建索引

    • 频繁更新的字段不适合建立索引。

    • where条件中用不到的字段不适合建立索引。

    • 表数据可以确定比较少的不需要建索引。

    • 数据重复且发布比较均匀的的字段不适合建索引(唯一性太差的字段不适合建立索引),例如性别,真假值。

    • 参与列计算的列不适合建索引,索引会失效。

  • 能用复合索引的要使用复合索引。

  • null值也是可以走索引的,他被处理成最小值放在b+树的最左侧。

  • 使用短索引:对字符串的列创建索引,如果可能,应该指定一个前缀长度。例如,如果有一个CHAR(255)的 列,如果在前10 个或20 个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

  • 排序的索引问题:

    • mysql查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。
    • 因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要,最好给这些列创建复合索引。
  • MySQL索引失效的几种情况:

    • 如果条件中有or,即使其中有条件带索引也不会使用走索引,除非全部条件都有索引。
    • 复合索引不满足最左原则就不能使用全部索引。
    • like查询以%开头。
    • 存在列计算。
    • 如果mysql估计使用全表扫描要比使用索引快,则不使用索引,比如结果的量很大。
    • 存在类型转化。

Redis

72.什么是 Redis?

Redis是一个基于 C 语言开发的开源数据库(BSD 许可),与传统数据库不同的是 Redis 的数据是存在内存中的(内存数据库),读写速度非常快,被广泛应用于缓存方向。并且,Redis 存储的是 KV 键值对数据。

73.Redis 为什么这么快?

Redis 内部做了非常多的性能优化,比较重要的主要有下面 3 点:

  • Redis 基于内存,内存的访问速度是磁盘的上千倍;
  • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
  • Redis 内置了多种优化过后的数据结构实现,性能非常高。

74.Redis 除了做缓存,还能做什么?

  • 分布式锁 : 通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
  • 一般是通过 Redis + Lua 脚本的方式来实现限流。
  • Redis 自带的 list 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
  • 通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。

75.Redis 数据结构

Redis 常用的数据结构有哪些?

  • 5 种基础数据结构 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。

  • 3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。

String 的应用场景有哪些?

  • 常规数据(比如 session、token、、序列化后的对象)的缓存。
  • 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数。
  • 分布式锁(利用 SETNX key value 命令可以实现一个最简易的分布式锁)。

String 还是 Hash 存储对象数据更好呢?

  • String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要单独查询对象中的个别字段信息,Hash 就非常适合。
  • String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。并且,存储具有多层嵌套的对象时也方便很多。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
  • 在绝大部分情况,我们建议使用 String 来存储对象数据即可!

购物车信息用 String 还是 Hash 存储更好呢?

由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:

  • 用户 id 为 key
  • 商品 id 为 field,商品数量为 value。

那用户购物车信息的维护具体应该怎么操作呢?

  • 用户添加商品就是往 Hash 里面增加新的 field 与 value;
  • 查询购物车信息就是遍历对应的 Hash;
  • 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
  • 删除商品就是删除 Hash 中对应的 field;
  • 清空购物车直接删除对应的 key 即可。

使用 Redis 实现一个排行榜怎么做?

  • Redis 中有一个叫做 sorted set 的数据结构经常被用在各种排行榜的场景,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
  • 相关的一些 Redis 命令: ZRANGE (从小到大排序) 、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。

使用 Set 实现抽奖系统需要用到什么命令?

SPOP key count : 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。

SRANDMEMBER key count : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。

使用 Bitmap 统计活跃用户怎么做?

使用日期(精确到天)作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。

使用 HyperLogLog 统计页面 UV 怎么做?

  • 将访问指定页面的每个用户 ID 添加到 HyperLogLog 中。
  • 统计指定页面的 UV。

76.Redis 线程模型

Redis 单线程模型了解吗?

Redis 中的文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

既然是单线程,那怎么监听大量的客户端连接呢?

I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。

Redis6.0 之前为什么不使用多线程

  • 单线程编程容易并且更容易维护;
  • Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
  • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。

Redis6.0 之后为何引入了多线程?

  • Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
  • 虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。

77.Redis 内存管理

Redis 给缓存数据设置过期时间有啥用?

  • 过期时间除了有助于缓解内存的消耗。
  • 很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效。如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。

Redis 是如何判断数据是否过期的呢?

  • Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。
  • 过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

过期的数据的删除策略了解么?

常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):

  • 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。

  • 定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。

  • 定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。

  • 但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,然后就 Out of memory 了。

Redis 内存淘汰机制了解么?

相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

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

78.Redis 持久化机制

怎么保证 Redis 挂掉之后再重启数据可以进行恢复?

  • Redis 的一种持久化方式叫快照(snapshotting,RDB)。
  • 另一种方式是只追加文件(append-only file, AOF)。

什么是 RDB 持久化?

  • Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。
  • Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
  • 快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置。

RDB 创建快照时会阻塞主线程吗?

Redis 提供了两个命令来生成 RDB 快照文件:

  • save : 主线程执行,会阻塞主线程;
  • bgsave : 子线程执行,不会阻塞主线程,默认选项。

什么是 AOF 持久化?

  • 与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。

  • 默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启。

  • 开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件。

  • AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof

  • 在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

    appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
    appendfsync everysec  #每秒钟同步一次,显式地将多个写命令同步到硬盘
    appendfsync no
    

    为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

AOF 日志是如何实现的?

  • 关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志。
  • 为什么是在执行完命令之后记录日志呢?
    • 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查。
    • 在命令执行完之后再记录,不会阻塞当前的命令执行。
  • 这样也带来了风险:
    • 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失。
    • 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)。

AOF 重写了解吗?

  • 当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
  • AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。

如何选择 RDB 和 AOF?

RDB 比 AOF 优秀的地方

  • RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会比 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。
  • 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。

AOF 比 RDB 优秀的地方

  • RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。
  • RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
  • AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。

79.缓存问题

缓存穿透

  • 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
  • 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
  • 缓存无效 key:这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。
  • 布隆过滤器:布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

缓存雪崩

  • 缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。
  • 有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。
  • 针对 Redis 服务不可用的情况:
    • 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
    • 限流,避免同时处理大量的请求。
  • 针对热点缓存失效的情况:
    • 设置不同的失效时间比如随机设置缓存的失效时间。
    • 缓存永不失效。

80.3种常用的缓存读写策略

Cache Aside Pattern(旁路缓存模式)

  • Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。
  • Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。
  • 缓存读写步骤:
    • 写:(1)先更新 DB。(2)然后直接删除 cache 。
    • 读:(1)从 cache 中读取数据,读取到就直接返回。(2)cache中读取不到的话,就从 DB 中读取数据返回。(3)再把数据放到 cache 中。

在写数据的过程中,可以先删除 cache ,后更新 DB 么?

  • 那肯定是不行的!因为这样可能会造成数据库(DB)和缓存(Cache)数据不一致的问题。为什么呢?比如说请求1 先写数据A,请求2随后读数据A的话就很有可能产生数据不一致性的问题。

在写数据的过程中,先更新DB,后删除cache就没有问题了么?

  • 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多!
  • 比如请求1先读数据 A,请求2随后写数据A,并且数据A不在缓存中的话也有可能产生数据不一致性的问题。

缺陷1:首次请求数据一定不在 cache 的问题

可以将热点数据可以提前放入cache 中。

写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。

  • 数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。
  • 可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。

Read/Write Through Pattern(读写穿透)

  • Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。
  • Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。
  • 写(Write Through):
    • 先查 cache,cache 中不存在,直接更新 DB。
    • cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)。
  • 读(Read Through):
    • 从 cache 中读取数据,读取到就直接返回 。
    • 读取不到的话,先从 DB 加载,写入到 cache 后返回响应。

Write Behind Pattern(异步缓存写入)

  • Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。
  • 这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到了这种策略。

81.Redis 内存碎片

什么是内存碎片?

  • 你可以将内存碎片简单地理解为那些不可用的空闲内存。
  • 举个例子:操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。
  • Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗。

为什么会有 Redis 内存碎片?

  • Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
  • 频繁修改 Redis 中的数据也会产生内存碎片。当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统。

如何查看 Redis 内存碎片的信息?

  • 使用 info memory 命令即可查看 Redis 内存相关的信息。

  • Redis 内存碎片率的计算公式:mem_fragmentation_ratio (内存碎片率)= used_memory_rss (操作系统实际分配给 Redis 的物理内存空间大小)/ used_memory(Redis 内存分配器为了存储数据实际申请使用的内存空间大小)

    也就是说,mem_fragmentation_ratio (内存碎片率)的值越大代表内存碎片率越严重。

  • 通常情况下,我们认为 mem_fragmentation_ratio > 1.5 的话才需要清理内存碎片。 mem_fragmentation_ratio > 1.5 意味着你使用 Redis 存储实际大小 2G 的数据需要使用大于 3G 的内存。

如何清理 Redis 内存碎片?

Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。

直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。