Java面试 - 进阶(大厂Java面试题)

HashMap数据结构

  • JDK1.7中底层是数组+链表,JDK1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率

  • JDK1.7中链表插入使用的是头插法,JDK1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历链表统计链表元素个数,所以正好就直接使用尾插法

  • JDK1.7中哈希算法比较复杂,存在各种右移与异或运算,JDK1.8中进行了简化,因为复杂的哈希算法的目的就是提高散列性,来提供HashMap的整体效率,而JDK1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源

HashMap的put过程

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

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

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

    • 3.1、如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
    • 3.2、如果是JDK1.8,则会先判断当前位置上的Node类型,看是红黑树Node,还是链表Node
      • 3.2.1、如果是红黑树Node,则将key和value封裝为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value

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

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

JDK1.7中HashMap死循环的原因

JDK1.7中HashMap的transfer函数如下:在扩容操作的时候,他会重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。【头插法】会将链表的顺序翻转,这也是形成死循环的关键点。

JDK1.8中HashMap线程不安全(数据覆盖)问题

线程判断是否出现hash碰撞的时候,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

HashMap的扩容机制原理

JDK1.7
1、先生成新数组
2、遍历老数组中的每个位置上的链表上的每个元素
3、取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
4、将元素添加到新数组中去
5、所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
JDK1.8
1、先生成新数组
2、遍历老数组中的每个位置上的链表或红黑树
3、如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4、如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置

  • 4.1、统计每个下标位置的元素个数
  • 4.2、如果该位置下的元素个数超过了6,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
  • 4.3、如果该位置下的元素个数没有超过6,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置

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

ConcurrentHashMap的扩容机制

JDK1.7
1、JDK1.7的ConcurrentHashMap是基于Segment分段实现的
2、每个Segment相对于一个小型的HashMap
3、每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
4、先生成新的数组,然后转移元素到新数组中
5、扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
JDK1.8
1、JDK1.8的ConcurrentHashMap不再基于Segment实现
2、当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
3、如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
4、ConcurrentHashMap是支持多个线程同时扩容的
5、扩容之前也先生成一个新的数组
6、在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作

JDK1.8对ConcurrentHashMap做了哪些优化?

插入的时候如果数组元素使用了红黑树,取消了分段锁设计,synchronize替代了Lock锁。

为什么这样优化?
避免冲突严重时链表过长,提高查询效率,时间复杂度从O(N)提高到O(logN)

JDK、 JRE、 JVM之间的区别

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

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

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

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

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

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

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

JVM分配内存如何保证线程安全的?

分配内存一般有两种解决方案:

  • 1、CAS:对分配内存空间的动作做同步处理,采用CAS机制,配合失败重试的方式保证更新操作的线程安全性。【注意⚠️:在每次分配时都需要进行同步控制,这种是比较低效的】

  • 2、TLAB:每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块"私有"内存中再分配,当这部分区域用完之后,再分配新的"私有"内存

String、 String Buffer、 String Builder的区别

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

String 真正不可变的原因

我们知道被 final 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final 关键字修饰的数组保存字符串并不是 String 不可变的根本原因,因为这个数组保存的字符串是可变的(final 修饰引用类型变量的情况,也就是说数组里存储的是引用类型)。

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

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

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

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

  • 如果两个对象的hashCode不相同,那么这两个对象肯定不同的两个对象

  • 如果两个对象的hashCode相同,不代表这两个对象一定是同一个对象,也可能是两个对象

  • 如果两个对象相等,那么他们的hashCode就一定相同

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

所以我们就需要注意,如果我们重写了equals()方法,那么就要注意hashCode()方法,一定要保证能遵守上述规则。

ArrayList

ArrayList就是一个数组,默认是一个长度为0的空数组,第一次add时会进行扩容,第一次扩容的长度是DEFAULT_CAPACITY=10,之后按照oldCapacity + (oldCapacity >> 1)即1.5倍的的增速进行扩容。ArrayList不像HashMap,它没有所谓的负载因子,它只有在elementData装不下时才会进行扩容。ArrayList只会扩容,不会缩容,即使clear了,数组的长度依然不会变。

ArrayList和LinkedList区别

  • 1、底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的

  • 2、由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同

  • 3、ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用

深拷贝和浅拷贝

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

Java 中都有哪些引用类型?

1、强引用:发生 gc 的时候不会被回收。
2、软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
3、弱引用:有用但不是必须的对象,在下一次GC时会被回收。
4、虚引用:主要用来跟踪对象垃圾回收的活动过程。虚引用必须和引用队列关联使用。虚引用的作用主要是管理堆外内存(堆外内存=操作系统内存-jvm内存),当发生GC,虚引用就会被回收,并且会把回收的通知放到ReferenceQueue中。

CopyOnWriteArrayList的底层原理是怎样的

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

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

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

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

Java中有哪些类加载器

JDK自带有三个类加载器: BootstrapClassLoader、 ExtClassLoader、 AppClassLoader.

  • BootStrapClassLoader是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib下的jar包和class文件

  • ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类

  • AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件

类加载器双亲委派模型

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

    1. BootstrapClassLoader
    1. ExtClassLoader
    1. AppClassLoader

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

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

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

怎么打破双亲委派模型?

打破双亲委派机继承ClassLoader类,还要重写loadClass和findClass方法。打破双亲委派,其实就是重写这个方法,来用我们自己的方式来实现即可

如何排查JVM问题

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

对于已经发生了OOM的系统:
1、一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件

-XX+HeapDumpOnOutofMemoryError  XXHeapDumpPath=/usr/local/base)

2、我们可以利用jsisualvm等工具来分析dump文件
3、根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4、然后再进行详细的分析和调试

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

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

JVM分代年龄为什么是15次?

一个对象的GC年龄是存储在对象头里面的,而一个java对象在JVM的内存布局是由三个部分来组成的。分别是对象头、实例数据、对其填充。而在对象头里面,有4个bit位来存储GC年龄。而4个bit位能够存储的最大数值为15,所以从这个角度来说,JVM分代年龄之所以设置成15次,是因为他最大能够存储的数值是15

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

1、引用计数算法:这种方式是给堆内存当中的每个对象记录一个引用个数,引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。

2、可达性算法:这种方式是在内存中,从根对象向下一直找引用,找到的对象就不是垃圾,没找到的对象就是垃圾。

JVM有哪些垃圾回收算法?

1、标记清除算法:

  • a、标记阶段:把垃圾内存标记出来
  • b、清除阶段:直接将垃圾内存回收。
  • c、这种算法是比较简单的,但是有个很严重的问题,就是会产生大量的内存碎片。

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

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

ThreadLocal的底层原理

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

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

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

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

Java死锁如何避免?

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

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

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

线程池工作结构

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:

1、如果此时线程池中的线程数量小于corePoolsize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务

2、如果此时线程池中的线程数量等于corePoolsize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列

3、如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务

4、如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务

5、当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数

核心线程是否能够回收

如果希望核心线程也要回收,可以设置allowCoreThreadTimeOut这个属性为true,一般情况下我们不会去回收核心线程。

因为线程池本身就是实现线程的复用,而且这些核心线程在没有任务要处理的时候是处于阻塞状态并没有占用CPU资源。

线程池为什么是先添加列队而不是先创建最大线程?

当线程池中的核心线程都在忙时,如果继续往线程池中添加任务,那么任务会先放入队列,队列满了之后,才会新开线程。这就相当于,一个公司本来有10个程序员,本来这10个程序员能正常的处理各种需求,但是随着公司的发展,需求在慢慢的增加,但是一开始这些需求只会增加在待开发列表中,然后这10个程序员加班加点的从待开发列表中获取需求并进行处理,但是某一天待开发列表满了,公司发现现有的10个程序员是真的处理不过来了,所以就开始新招员工了。

ReentrantLock中的公平锁和非公平锁的底层实现

首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock0)方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。

不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,并没有体现在线程被唤醒阶段。

另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。

ReentrantLock中tryLock()和lock()方法的区别

1、tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false

2、lock()表示阻塞加锁,线程会阻塞直到加到锁,方法没有返回值

CountDownLatch和Semaphore的区别和底层原理

CountDownLatch表示计数器,可以给CountDownLatch设置一个数字,一个线程调用CountDownLatch的await()将会阻塞,其他线程可以调用

CountDownLatch的countDown()方法来对CountDownLatch中的数字减一,当数字被减成0后,所有await的线程都将被唤醒。

对应的底层原理就是,调用await()方法的线程会利用AQS排队,一旦数字被减为0,则会将AQS中排队的线程依次唤醒。

Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使用该信号量,通过acquire()来获取许可,如果没有许可可用则线程阻塞,并通过AQS来排队,可以通过release()方法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第一个线程开始依次唤醒,直到没有空闲许可。

Synchronized的偏向锁、轻量级锁、重量级锁

1、偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了

2、轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程

3、如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞

4、自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和喚醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行
中,相对而言没有使用太多的操作系统资源,比较轻量。

Synchronized和ReentrantLock的区别

1、Synchronized是一个关键字。ReentrantLock是一个类,只是实现了Lock接口

2、Synchronized会自动的加锁与释放锁,不会造成死锁。ReentrantLock需要程序员手动加锁与释放锁,如果使用不当没有unLock()释放锁就会造成死锁。

3、Synchronized的底层是JVM层面的锁,ReentrantLock是API层面的锁

4、Synchronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁

5、Synchronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态

6、Synchronized底层有一个锁升级的过程

7、通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到

8、synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁

谈谈你对AQS的理解,AQS如何实现可重入锁?

1、AQS是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架

2、在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不用的意义

3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1,释放锁state就减1

谈谈你对IOC的理解

通常,我们认为Spring有两大特性IOC和AOP,那到底该如何理解IOC呢?

对于很多初学者来说,IOC这个概念给人的感觉就是我好像会,但是我说不出来。

那么IOC到底是什么,接下来来说说我的理解,实际上这是一个非常大的问题,所以我们就把它拆细了来回答,lOC表示控制反转,那么:
1、什么是控制?控制了什么?
2、什么是反转?反转之前是谁控制的?反转之后是谁控制的?如何控制的?
3、为什么要反转?反转之前有什么问题?反转之后有什么好处?

这就是解决这一类大问题的思路,大而化小。

那么,我们先来解决第一个问题:什么是控制?控制了什么?

我们在用Spring的时候,我们需要做什么:
1、建一些类,比如UserService、 OrderService
2、用一些注解,比如@Autowired

但是,我们也知道,当程序运行时,用的是具体的UserService对象、OrderService对象,那这些对象是什么时候创建的?谁创建的?包括对象里的属性是什么时候赋的值?谁赋的?所有这些都是我们程序员做的,以为我们只是写了类而已,所有的这些都是Spring做的

这就是控制:
1、控制对象的创建
2、控制对象内属性的赋值

如果我们不用Spring,那我们得自己来做这两件事,反过来,我们用Spring,这两件事情就不用我们做了,我们要做的仅仅是定义类,以及定义哪些属性需要Spring来赋值(比如某个属性上加@Autowired),而这其实就是第二个问题的答案,这就是反转,表示一种对象控制权的转移。

那反转有什么用,为什么要反转?

如果我们自己来负责创建对象,自己来给对象中的属性赋值,会出现什么情况?

比如,现在有三个类:
1、A类,A类里有一个属性C c;
2、B类,B类里也有一个属性C c;
3、C类

现在程序要运行,这三个类的对象都需要创建出来,并且相应的属性都需要有值,那么除了定义这三个类之外,我们还得写:
1、A a = new A();
2、B b = new B();
3、C c = new C();
4、a.c = c;
5、b.c =c;

这五行代码是不用Spring的情况下多出来的代码,而且,如果类在多一些,类中的属性在多一些,那相应的代码会更多,而且代码会更复杂。所以我们可以发现,我们自己来控制比交给Spring来控制,我们的代码量以及代码复杂度是要高很多的,反言之,将对象交给Spring来控制,减轻了程序员的负担。

总结一下,lOC表示控制反转,表示如果用Spring,那么Spring会负责来创建对象,以及给对象内的属性赋值,也就是如果用Spring,那么对象的控制权会转交给Spring

单例Bean和单例模式

单例模式表示JVM中某个类的对象只会存在唯一一个。
而单例Bean并不表示JVM中只能存在唯一的某个类的Bean对象。

【 经典单例模式代码 双检锁 ➕ volatile 】

public class Singleton {
     private volatile static Singleton singleton; // 定义对象进行volatile修饰
     private Singleton ( ){}
     public static Singleton getSingleton({
          if(singleton == null){// 第一步 常规的非空判断,没有对象才会去创建对象
                 synchronized (Singleton.class){
                     // 第二次判空 - 防止对象被创建多次,虽然只有一个线程能拿到锁,但是多个线程很有可能已经进入了if代码块,
                     // 此时正在等待,假设a、b两个线程。a先拿到锁,一旦线程a释放,线程b会立即获得锁,然后又进行对象创建,这样对象会被创建多次。
                     if (singleton == null) {
                         singleton = new Singleton(); // 第三步 在指令层面不是一个原子操,所以进行volatile修饰
                     }
                 }
          }
          return singleton;
      }
}

Spring事务传播机制

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

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

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

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

  • 4、REQUIRES_ NEW:创建一个新事务,如果存在当前事务,则挂起该事务。

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

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

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

Spring事务失效的八大场景与原因分析

1、方法内的自调用:Spring事务是基于AOP的,只要使用代理对象调用某个方法时,Spring事务才能生效,而在一个方法中调用使用this.xxx()调用方法时,this并不是代理对象,所以会导致事务失效。

  • a、解放办法1:把调用方法拆分到另外一个Bean中
  • b、解决办法2:自己注入自己
  • c、解决办法3:
AopContext.currentProxy() + @EnableAspectJAutoProxy(exposeProxy = true)

2、方法是private的:Spring事务会基于CGLIB来进行AOP,而CGLIB会基于父子类来失效,子类是代理类,父类是被代理类,如果父类中的某个方法是private的,那么子类就没有办法重写它,也就没有办法额外增加Spring事务的逻辑。

3、方法是final的:原因和private是一样的,也是由于子类不能重写父类中的final的方法

4、单独的线程调用方法:当Mybatis或JdbcTemplate执行SQL时,会从ThreadLocal中去获取数据库连接对象,如果开启事务的线程和执行SQL的线程是同一个,那么就能拿到数据库连接对象,如果不是同一个线程,那就拿到不到数据库连接对象,这样,Mybatis或JdbcTemplate就会自己去新建一个数据库连接用来执行SQL,此数据库连接的autocommit为true,那么执行完SQL就会提交,后续再抛异常也就不能再回滚之前已经提交了的SQL

5、没加@Configuration注解:如果用SpringBoot基本没有这个问题,但是如果用的Spring,那么可能会有这个问题,这个问题的原因其实也是由于
Mybatis或JdbcTemplate会从ThreadLocal中去获取数据库连接,但是ThreadLocal中存储的是一个MAP,MAP的key为DataSource对象,value为连接对象,而如果我们没有在AppConfig 上添加@Configuration注解的话,会号致MAP中存的DataSource对象和Mybatis和jdbcTemplate中的DataSource对象不相等,从而也拿不到数据库连接,导致自己去创建数据库连接了。

6、异常被吃掉:如果Spring事务没有捕获到异常,那么也就不会回滚了,默认情况下Spring会捕获RuntimeException和Error。

7、类没有被Spring管理

8、数据库不支持事务

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

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

1、解析xml配置或注解配置的类
通过BeanDefinitionRegistry接口里的.registry()方法来加载得到BeanDefinition,然后注入到BeanFactory2、通过BeanDefinition反射(或者构造方法创建?)创建Bean对象;
3、对Bean对象成员变量进行属性填充
4、回调实现了Aware接口的方法,如BeanNameAware;【aware:adj: 有意识的】
5、调用BeanPostProcessor的初始化前置方法;
6、调用init初始化方法;    
7、调用BeanPostProcessor的初始化后置方法,此处会进行AOP8、将创建的Bean对象放入容器(一个Map)中;-------  创建好的bean( 循环依赖遍是在这一步发生的问题)
9、业务使用Bean对象
10Spring容器关闭时调用DisposableBeandestory()方法;

在这里插入图片描述

Java面试 - 进阶(大厂Java面试题)_第1张图片

Spring中Bean是线程安全的吗

Spring本身并没有针对Bean做线程安全的处理,所以:

1、如果Bean是无状态的,那么Bean则是线程安全的
2、如果Bean是有状态的,那么Bean则不是线程安全的

另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。

基本上,Spring不允许使用有状态的bean。如果您使用Singleton-您有问题-下一次调用将"看到"旧数据。如果使用Prototype-对该bean的任何方法的每次调用都将导致一个新的类实例,这又不是您想要的。

解决方案似乎是-每当需要状态类时都使用new关键字。

或使用剩余的请求或会话范围,但是这些仅在Web容器中可用。

ApplicationContext和BeanFactory有什么区别

BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了 BeanFactory,所以
ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如
EnvironmentCapable、 MessageSource、 ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功
能,这是BeanFactory所不具备的

Spring中的事务是如何实现的

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

Java面试 - 进阶(大厂Java面试题)_第2张图片

Spring中什么时候@Transactional会失效

  • 因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会失效的。

  • 同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效

Spring容器启动流程是怎样的

  • 1、在创建Spring容器,也就是启动Spring时:
  • 2、首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
  • 3、然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
  • 4、利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
  • 5、单例Bean创建完了之后,Spring会发布一个容器启动事件
  • 6、Spring启动结束
  • 7、在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BeanFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
  • 8、在Spring启动过程中还会去处理@lmport等注解

Java面试 - 进阶(大厂Java面试题)_第3张图片

Spring用到了哪些设计模式

Java面试 - 进阶(大厂Java面试题)_第4张图片

Spring Boot中常用注解及其底层实现

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

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

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

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

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

3、@Controller、 @Service、 @ResponseBody、 @Autowired都可以说

Spring Boot是如何启动Tomcat的

1、首先,SpringBoot在启动时会先创建一个Spring容器

2、在创建Sprng容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean

3、Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat

Mybatis的优缺点

优点:
1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在XML 里,解除 sql 与程序代码的耦合,便于统一管
理;提供XML 标签,支持编写动态 SQL 语句,并可重用。

2、与JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;

3、很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC 支持的数据库 MyBatis 都支持)

4、能够与 Spring 很好的集成;

5、提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点:
1、SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL 语句的功底有一定要求。

2、SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

// TODO 一二级缓存

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

1、#{}是预编译处理、是占位符,${}是字符串替换、是拼接符。

2、Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement 来赋值;

3、Mybatis在处理${}时,会将sql中的${}替换成变量的值,调用Statement 来赋值;

4、使用#{}可以有效的防止 SQL 注入,提高系统安全性。

索引的基本原理

索引用来快速地寻找那些具有特定值的记录。如果没有索引,一般来说执行查询时遍历整张表。

索引的原理:就是把无序的数据变成有序的查询

1、把创建了索引的列的内容进行排序
2、对排序结果生成倒排表
3、在倒排表内容上拼上数据地址链
4、在查询的时候,先拿到倒排表内容,再取出数据地址链,从而拿到具体数据

索引失效的场景

1、模糊查询. 以“%abc”开头的 LIKE 语句
2、数据类型–隐式转换
3、函数,对索引字段使用内部函数
4、索引不存储空值,所以尽量对字段设置一定要not null,否则失效。(索引也是按一页16KB进行存储的)
5、运算,也就是说对索引列进行加减乘除等运算,会导致索引失效
6、最左原则,联合索引,索引顺序非常重要,如果不是按照最左列开始进行查找也会失效
---- 当然,mysql8有个索引跳跃扫描,见下面描述
7、快,全表扫描,数据量不多的时候,innodb会觉得全表扫描比索引更快,也不会使用索引!

MySQL一定会走最左匹配前缀吗?

MySQL 8.0.13开始支持 index skip scan :索引跳跃扫描。该优化方式支持那些SQL在不符合组合索引最左前缀的原则的情况,优化器依然能组使用组合索引。

Extra列中显示增加了Using index for skip scan,表示用到了索引跳跃扫描的优化逻辑

索引设计的原则?

查询更快、占用空间更小
1、适合索引的列是出现在where子句中的列,或者连接子句中指定的列
2、基数较小的表,索引效果较差,没有必要在此列建立索引
3、使用短索引,如果对长字符串列进行索引,应该指定一个前缀长度,这样能够节省大量索引空问,如果搜索词超过索引前缀长度,则使用索引排除不匹配的行,然后检查其余行是否可能匹配。
4、不要过度索引。索引需要额外的磁盘空间,并降低写操作的性能。在修改表内容的时候,索引会进行更新甚至重构,索引列越多,这个时间就会越长。所以只保持需要的索引有利于查询即可。
5、定义有外键的数据列一定要建立索引。
6、更新频繁字段不适合创建索引
7、若是不能有效区分数据的列不适合做索引列(如性别,男女未知,最多也就三种,区分度实在太低)
8、尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
9、对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。
10、对于定义为text、image和bit的数据类型的列不要建立索引。

事务的基本特性和隔离级别

事务基本特性ACID分别是:
1、原子性指的是一个事务中的操作要么全部成功,要么全部失败。

2、一致性指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设A只有90块,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证

2、隔离性指的是一个事务的修改在最终提交前,对其他事务是不可见的。

3、持久性指的是一旦事务提交,所做的修改就会永久保存到数据库中。

隔离性有4个隔离级别,分别是:

  • read uncommit 读未提交,可能会读到其他事务未提交的数据,也叫做脏读。
    用户本来应该读取到id=1的用户age应该是10,结果读取到了其他事务还没有提交的事务,结果读取结果age=20,这就是脏读。

  • read commit 读已提交,两次读取结果不一致,叫做不可重复读。
    不可重复读解决了脏读的问题,他只会读取已经提交的事务。∑用户开启事务读取id=1用户,查询到age=10,再次读取发现结果=20,在同一个事务里同一个查询读取到不同的结果叫做不可重复读。

  • repeatable read 可重复复读,这是mysql的默认级别,就是每次读取结果都一样,但是有可能产生幻读。

  • serializable 串行,一般是不会使用的,他会给每一行读取的数据加锁,会导致大量超时和锁竞争的问题。

什么是MVCC

MVCC (Multi-Version Concurrency Control,多版本并发控制)指的就是在使用READ COMMITTD、 REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程.可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能.READ COMMITTD.
REPEATABLE READ这两个隔离级别的一个很大不同就是:生成Readview的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了.

简述MyISAM和InnoDB的区别

MyISAM:

  • 不支持事务,但是每次查询都是原子的;
  • 支持表级锁,即每次操作是对整个表加锁;
  • 存储表的总行数;
  • 一个MYISAM表有三个文件:索引文件、表结构文件、数据文件;
  • 采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。

InnoDb:

  • 支持ACID的事务,支持事务的四种隔离级别;
  • 支持行级锁及外键约束:因此可以支持写并发;
  • 不存储总行数;
  • 一个InnoDb引|擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表
    空,表大小受操作系统文件大小限制,一般为2G),受操作系统文件大小的限制;
  • 主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。

Explain语句结果中各个字段分表表示什么

列名 描述
id 查询语句中每出现一个SELECT关键字,MySQL就会为它分配一个唯一的id值,某子查询会被优化为join 查询,那么出现的id会一样
select_type SELECT关键字对应的那个查询的类型
table 表名
partitions 匹配的分区信息
type 针对单表的查询方式(全表扫描、索引)
possible_keys 可能用到的索引
key 实际上用到的索引
key_len 实际用到的索引长度
ref 当使用索引列等值查询时,与索引列进行等值匹配的对象信息
rows 预估的需要读取的条数
filtered 某个表经过搜索条件过滤后剩余记录条数的百分比
Extra 一些额外的信息,比如排序等

索引覆盖是什么

索引覆盖就是一个SQL在执行时,可以利用索引来快速查找, 并且此SQL所要查询的字段在当前索引对应的字段中都包含了, 那么就表示此SQL走完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回了

最左前缀原则是什么

当一个SQL想要利用索引是,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则

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

1、Record lock:行锁,行锁是基于索引实现,因为通过索引访问数据

2、Gap lock:间隙锁:锁定一个范围,不包括记录本身。比如:当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,它用于锁定的索引之间的间隙,但是不会包含记录本身。比如语句select * from user where age>1 and age<10 for update,将会锁住age在(1,10)的范围区间,此时其他事务对该区间的操作都会被阻塞。这种锁机制就是所谓的间隙锁(Next-Key锁)。它存在的主要目的有一个是为了解决幻读问题,因为RR作为InnoDB的默认事物隔离级别,是存在幻读问题的。

3、Next-key lock:(序列号加锁机制 record+gap) 临键锁,锁定一个范围,包含记录本身

Innodb是如何实现事务的

Innodb通过Buffer Pool, LogBuffer, Redo Log, Undo Log来实现事务,

以一个update语句为例:
1、Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中
2、执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
3、针对updlate语句生成一个RedoLog对象,并存入LogBuffer中
4、针对update语句生成undolog日志,用于事务回滚
5、如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中
6、如果事务回滚,则利用undolog日志进行回滚

B树和B+树的区别,为什么Mysql使用B+树

B树的特点:

1、节点排序

2、一个节点了可以存多个元素,多个元素也排序了

B+树的特点:
1、拥有B树的特点
2、叶子节点之间有指针
3、非叶子节点上的元素在叶子节点上都冗余了,也就是叶子节点中存储了所有的元素,并且排好顺序

Mysq/索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个Innodb页就是一个B+树节点,一个Innodb页默认16kb,所以一般情况下一颗两层的B+树可以存2000万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等SQL语句。

MySQL中 in和 exists 的区别?

  • exists:exists对外表用loop逐条查询,每次查询都会查看exists的条件语句,当exists里的条件语句能够返回记录行时(无论记录行是的多少,只要能返回),条件就为真,返回当前loop到的这条记录;反之,如果exists里的条件语句不能返回记录行,则当前loop到的这条记录被丢弃,exists的条件就像一个bool条件,当能返回结果集则为true,不能返回结果集则为false

  • in:in查询相当于多个or条件的叠加

一个是大表,则子查询表大的用exists,子查询表小的用in

Mysql锁有哪些,如何理解

按锁粒度分类:
1、行锁:锁某行数据,锁粒度最小,并发度高
2、表锁:锁整张表,锁粒度最大,并发度低
3、间隙锁:锁的是一个区间

还可以分为:
1、共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
2、排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写

还可以分为:
1、乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
2、悲观锁:上面所的行锁、表锁等都是悲观锁

在事务的隔离级别实现中,就需要利用锁来解决幻读

Mysql慢查询该如何优化?

1、检查是否走了索引,如果没有则优化SQL利用索引
2、检查所利用的索引,是否是最优索引
3、检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
4、检查表中数据是否过多,是否应该进行分库分表了
5、检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源

Mysql中多大的表是大表?

我们通常说表太大,另外一层意思就是数据太多了,导致索引的效果都不明显了,只能分表了。所以我们在讨论什么是大表时,需要站在Mysql索引角度来分析,到底多大数据量时是大表。

Java面试 - 进阶(大厂Java面试题)_第5张图片
上图是(mnodb中的一个主键索引1,也就是一颗B+树,树中的每个节点是一个Innodb Page,大小默认为16kb,叶子节点中的每个节点主要存储的就是一条条数据,非叶子节点中存储的是主键和页地址。

所以,我们可以来计算一下,如果B+树的高度为2,能存多少条数据。

Java面试 - 进阶(大厂Java面试题)_第6张图片

  • 假如一条记录为1kb
  • 主键类型为int类型,也就一个主键占4b
  • innodb中一个页地址需要占6b

所以1页中,也就是一个节点中,可以存:

  • 16kb/1kb=16条行数据
  • 16kb/(4b+6b)=1638条索引记录(主键+索引地址)

所以如果B+树的高度为2,那么叶子节点就有1638个,所以能存的行数据条数为:1638*16=26208条记录。

如果B+树的高度为3,那么第一层一个节点,第二层1638个节点,第三层16381638个节点,最终数据条数为:16381638*16 = 42,928,704。也就是差不多4000多万条数据

如果主键的类型为bigint,一个主键占8个字节,所以高度为3时,能存:

  • 16kb/(8b+6b) = 1170
  • 1170117016= 21902400

也就是2000多万行数据。

所以,我们可以通过这种方式来判断一个表的数据量是不是过多(B+树的高度一般不建议超过三层,因为B+树的数据都是存在磁盘中的,树太高了,进行IO的次数就表多了,整体效率也就降低了)。如果表中实际的数据总条数超过了3层能存放的数据量,那么这个表就是大表了,此时索引的效率就不高了,就需要进行分表了。

Redis单线程为什么这么快

Redis基于Reactor模式开发了网络事件处理器、文件事件处理器 fileeventhandler。它是单线程的,所以 Redis才叫做单线程的模型,它采用IO多路复用 机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis内部的线程模型的简单性。

文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)

多个 Socket 可能并发的产生不同的事件,IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中有序、同步取出一个Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。

然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理

  • 1、Redis启动初始化时,将连接应答处理器跟AE_READABLE事件关联。
  • 2、若一个客户端发起连接,会产生一个AE_READABLE事件,然后由连接应答处理器负责和客户端建立连接,创建客户端对应的socket,同时将这
    个socket的AE_READABLE事件和命令请求处理器关联,使 得客户端可以向主服务器发送命令请求。
  • 3、当客户端向Redis发请求时(不管读还是写请求),客户端socket都会产生一个AE_READABLE事件,触发命令请求处理器。处理器读取客户端的命令内容,然后传给相关程序执行。
  • 4、当Redis服务器准备好给客户端的响应数据后,会将socket的AE_WRITABLE事件和命令回复处理器关联,当客户端准备好读取响应数据时,会在socket产生一个AE_READABLE事件,由对应命令回复处理器处理,即将准备好的响应数据写入socket,供客户端读取。
  • 5、命令回复处理器全部写完到 socket 后,就会删除该socket的AE_WRITABLE事件和命令回复处理器的映射。

单线程快的原因:

  • 1、纯内存操作
  • 2、核心是基于非阻塞的IO多路复用机制
  • 3、单线程反而避免了多线程的频繁上下文切换带来的性能问题

什么是RDB和AOF

RDB:

Redis DataBase,在指定的时间问隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

优点:

  • 1、整个Redis数据库将只包含一个文件 dump.rdlb,方便持久化。
  • 2、容灾性好,方便备份。
  • 3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了 redis 的高性能
  • 4、相对于数据集大时,比 AOF 的启动效率更高。

缺点:

  • 1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)
  • 2、由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。

AOF:

Append Only File,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录

优点:

  • 1、数据安全,Redlis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。
  • 2、通过 append 模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis-check-aof 工具解决数据一致性问题。
  • 3、AOF 机制的 rewrite 模式。定期对AOF文件进行重写,以达到压缩的目的

缺点:

  • 1、AOF 文件比 RDB 文件大,且恢复速度慢。
  • 2、数据集大的时候,比 rdb 启动效率低。
  • 3、运行效率没有RDB高

AOF文件比RDB更新频率高,优先使用AOF还原数据,AOF比RDB更安全也更大,RDB性能比AOF好,如果两个都配了优先加载AOF。

Redis的过期键的删除策略

Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。

  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情況下使得CPU和内存资源达到最优的平衡效果。

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

Redis中同时使用了情性过期和定期过期两种过期策略。

简述Redis事务实现

1、事务开始

MULT命令的执行,标识着一个事务的开始。MULTI命令会将客户端状态的 flags 属性中打开 REDIS_MULTI 标识来完成的。

2、命令入队

当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作。如果客户端发送的命令为MULTI、 EXEC、 WATCH、
DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复

  • 如果客户端发送的命令为 EXEC、 DISCARD、 WATCH、 MULTI 四个命令的其中一个,那么服务器立即执行这个命令。

  • 如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令。

首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态 (redisClient)的 flags 属性关闭 REDIS_MULTI 标识,并且返回错误信息给客户端。如果正确,将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复

事务队列是按照FIFO的方式保存入队的命令

3、事务执行

客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。

  • 如果客户端状态的 flags 属性不包含 REDIS_MULTI 标识,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 标识,那么就直接取消事务的执行。
  • 否则客户端处于事务状态(flags 有 REDIS_MULTI 标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端。

Redis 不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。

Redis 事务不支持检查那些程序员自己逻辑错误。例如对 String 类型的数据库键执行对 HashMap 类型的操作!

  • WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

  • MULTI命令用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放
    到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。

  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值nil。

  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出。

  • UNWATCH命令可以取消watch对所有key的监控。

Redis 主从复制的核心原理

通过执行slaveof命令或设置slaveof选项,让一个服务器去复制另一个服务器的数据。主数据库可以进行读写操作,当写操作号致数据变化时会自动将数据同步给从数据库。而从数据库一般是只读的,并接受主数据库同步过来的数据。一个主数据库可以拥有多个从数据库,而一个从数据库只能拥有一个主数据库。

全量复制:

  • 1、主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的
  • 2、主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗
  • 3、从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof, 也会带来额外的消耗

部分复制:

1、复制偏移量:执行复制的双方,主从节点,分别会维护一个复制偏移量offset

2、复制积压缓冲区:主节点内部维护了一个固定长度的、先进先出(FIFO)队列 作为复制积压缓冲区,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。

3、服务器运行ID(runid):每个Redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。从节点Redis断开重连的时候,就是根据运行ID来判断同步的进度:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);

  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

Redis的主从复制流程:

Redis的主从复制是提高Redis的可靠性的有效措施

  • 1、集群启动时,主从库间会先建立连接,为全量复制做准备

  • 2、主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB

  • 3、在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则redis的的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer,记录RDB文件生成收到的所有写操作。

  • 4、最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replication buffer中修改操作发送给从库,从库再执行这些操作。这样一来,主从库就实现同步了

  • 5、后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步

Redis有哪些数据结构?分别有哪些典型的应用场景?

Redis的数据结构有:

  • 1、字符串:可以用来做最简单的数据,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID

  • 2、哈希表:可以用来存储一些key-value对,更适合用来存储对象

  • 3、列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据

  • 4、集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能

  • 5、有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能

Redis分布式锁底层是如何实现的?

  • 1、首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁。setNx是一个天然的原子性操作
  • 2、然后还要利用lua脚本来保证多个redis操作的原子性
  • 3、同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约
  • 4、同时还要考虑到redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到

缓存穿透、缓存击穿、缓存雪崩分别是什么

缓存中存放的大多都是热点数据,目的就是防止请求可以直接从缓存中获取到数据,而不用访问Mysql

  • 1、缓存穿透:假如某一时刻访问redis的大量key都在redis中不存在(比如黑客故意伪造一些乱七八糟的key),那么也会给数据造成压力,这就是缓存穿透,解决方案是使用布隆过濾器,它的作用就是如果它认为一个key不存在,那么这个key就肯定不存在,所以可以在缓存之前加一层布隆过滤器来拦截不存在的key

  • 2、缓存击穿:和缓存雪崩类似,缓存雪崩是大批热点数据失效,而缓存击穿是指某一个热点key突然失效,也导致了大量请求直接访问Mysql数据库,这就是缓存击穿,解决方案就是考虑这个热点key不设过期时间或用分布式锁

  • 3、缓存雪崩:如果缓存中某一时刻大批热点数据同时过期,那么就可能导致大量请求直接访问Mysql了,解决办法就是在过期时间上增加一点随机值,另外如果搭建一个高可用的Redis集群也是防止缓存雪崩的有效手段

Redis的持久化机制

RDB:
Redis DataBase 将某一个时刻的内存快照 (Snapshot),以二进制的方式写入磁盘。

手动触发:

  • save命令,使 Redis 处于阻塞状态,直到 RDB 持久化完成,才会响应其他客户端发来的命令,所以在生产环境一定要慎用

  • bgsave命令,fork出一个子进程执行持久化,主进程只在fork过程中有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了

自动触发:

  • save mn:在m 秒内,如果有 n 个键发生改变,则自动触发持久化,通过bgsave执行,如果设置多个、只要满足其一就会触发,配置文件有默认配置(可以注释掉)

  • flushall:用于清空redis所有的数据库,flushdb清空当前redis所在库数据(默认是0号数据库),会清空RDB文件,同时也会生成dump.rdb、内容为空

  • 主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点

优点:

  • 1、整个Redis数据库将只包含一个文件 dump.rdb,方便持久化。
  • 2、容灾性好,方便备份。
  • 3、性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了 redis 的高性能
  • 4、相对于数据集大时,比 AOF的启动效率更高。

缺点:

  • 1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
  • 2、由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟,会占用cpu

AOF:
Append Only File 以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘

  • 1、所有的写命令会追加到 AOF 缓冲中。

  • 2、AOF 缓冲区根据对应的策略向硬盘进行同步操作。

  • 3、随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩目的。

  • 4、当 Redis 重启时,可以加载 AOF 文件进行数据恢复。同步策略:

  • 每秒同步:异步成,效率非常高,一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失

  • 每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢一条 不同步:由操作系统控制,可能丢失较多数据

优点:

  • 1、数据安全

  • 2、通过 append 模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过 redis- check-aof 工具解决数据一致性问题。

  • 3、AOF 机制的 rewrite 模式。定期对AOF文件进行重写,以达到压缩的目的

缺点:

  • 1、AOF 文件比 RDB 文件大,且恢复速度慢。

  • 2、数据集大的时候,比 rdb 启动效率低。

  • 3、运行效率没有RDB高

对比:

  • AOF文件比RDB更新频率高,优先使用AOF还原数据。AOF比RDB更安全也更大
  • RDB性能比AOF好
  • 如果两个都配了优先加载AOF

什么是BASE理论

由于不能同时满足CAP,所以出现了BASE理论:

  • 1、BA: Basically Available,表示基本可用,表示可以允许一定程度的不可用,比如由于系统故障,请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的

  • 2、S: Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步…

  • 3、E: Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一段时间后再达到一致,在达到一致过程中,系统也是可用的

什么是RPC

RPC,表示远程过程调用,对于Java这种面试对象语言,也可以理解为远程方法调用,RPC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP协议来实现RPC,在Java中,我们可以通过直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有一种说法是RPC协议是HTTP协议之上的一种协议,也是可以理解的。参考理解:外语书和英语书的关系

分布式ID是什么?有哪些解决方案?

在开发中,我们通常会需要一个唯一ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护一个自增数字来作为ID都是可以的,但对于一个分布式系统,就会有可能会出现ID冲突,此时有以下解决方案:

  • uuid,这种方案复杂度最低,但是会影响存储空间和性能

  • 利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID长度较之uuid更短,但是受到单机数据库性能的限制,并发量大的时候,此方案也不是最优方案

  • 利用redis、zookeeper的特性来生成id,比如redis的自增命令、zookeeper的顺序节点,这种方案和单机数据库(mysql)相比,性能有所提高,可以适当选用

  • 雪花算法,一切问题如果能直接用算法解决,那就是最合适的,利用雪花算法也可以生成分布式ID,底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统id唯一,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。

分布式锁的使用场景是什么?有哪些实现方案?

在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使用。

而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用ReentrantLock、synchronized等技术是没办法來控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。

目前主流的分布式锁的实现方案有两种:

  • zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是
    CP,所以由它实现的分布式锁更可靠,不会出现混乱

  • redis:利用redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(一旦redlis中的数据出现了不一致),可能会出现多个客户端同时加到锁的情况

什么是分布式事务?有哪些实现方案?

在分布式系统中,一次业务处理可能需要多个应用来实现,比如用户发送一次下单请求,就涉及到订单系统创建订单、库存系统减库存,而对于一次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要用到分布式事务。常用解决方案有:

1、本地消息表:创建订单时,将减库存消息加入在本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调用库存系统

2、消息队列:目前RocketMQ中支持事务消息,它的工作原理是:

  • a、生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的

  • b、再创建订单,根据创建订单成功与否,向Broker发送commit或rollback

  • c、并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功

  • d、一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束

  • e、如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理

3、Seata:阿里开源的分布式事务框架,支持AT、TCC等多种模式,底层都是基于两阶段提交理论来实现的

什么是ZAB协议

ZAB协议是Zookeeper用来实现一致性的原子广播协议,该协议描述了Zookeeper是如何实现一致性的,分为三个阶段:

  • 1、领导者选举阶段:从zookeeper集群中选出一个节点作为Leader,所有的写请求都会由Leader节点来处理
  • 2、数据同步阶段:集群中所有节点中的数据要和Leader节点保持一致,如果不一致则要进行同步
    图灵课堂
  • 3、请求广播阶段:当Leader节点接收到写请求时,会利用两阶段提交来广播该写请求,使得写请求像事务一样在其他节点上执行,达到节点上的数
    时一致

但值得注意的是,Zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性的。

为什么Zookeeper可以用来作为注册中心

可以利用Zookeeper的临时节点和watch机制来实现注册中心的自动注册和发现,另外Zookeeper中的数据都是存在内存中的,并且Zookeeper底层采用了nio,多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来作为注册中心,但是如果考虑到注册中心应该是注册可用性的话,那么Zookeeper则不太合适,因为Zookeeper是CP的,它注重的是一致性,所以集群数据不一致时,集群将不可用,所以用Redis、Eureka、Nacos来作为注册中心将更合适。

Zookeeper中的领导者选举的流程是怎样的?

对于Zookeeper集群,整个集群需要从集群节点中选出一个节点作为Leader,大体流程如下:

  • 1、集群中各个节点首先都是观望状态(LOOKING),一开始都会投票给自己,认为自己比较适合作为leader

  • 2、然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先比较zxid,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜

  • 3、一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自己的投票箱中,并
    将新的选票发送给其他节点

  • 4、如果pk是平局则将接收到的选票放入自己的投票箱中

  • 5、如果pk赢了,则忽略所接收到的选票

  • 6、当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自己所投的节点是一样的,如果超过半
    数,那么则认为当前自己所投的节点是leader

  • 7、集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader也是一样的,这样集群的leader就选举出来了

Zookeeper集群中节点之间数据是如何同步的

  • 1、首先集群启动时,会先进行领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer
  • 2、然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式
  • 3、集群在工作过程中,所有的写请求都会交给Leader节点来进行处理,从节点只能处理读请求
  • 4、Leader节点收到一个写请求时,会通过两阶段机制来处理
  • 5、Leader节点会将该写请求对应的日志发送给其他Follower节点,并等待Follower节点持久化日志成功
  • 6、Follower节点收到日志后会进行持久化,如果持久化成功则发送一个Ack给Leader节点
  • 7、当Leader节点收到半数以上的Ack后,就会开始提交,先更新Leader节点本地的内存数据
  • 8、然后发送commit命令给Follower节点,Follower节点收到commit命令后就会更新各自本地内存数据
  • 9、同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执行更新本地内存数据
  • 10、最后Leader节点返回客户端写请求响应成功
  • 11、通过同步机制和两阶段提交机制来达到集群中节点数据一致

Dubbo支持哪些负载均衡策略

  • 1、随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率
  • 2、轮询:依次选择服务提供者来处理请求,并支持按权重进行轮询,底层采用的是平滑加权轮询算法
  • 3、最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
  • 4、一致性哈希:相同参数的请求总是发到同一个服务提供者

Dubbo是如何完成服务导出的?

  • 1、首先Dubbo会将程序员所使用的@DubboService注解或@Service注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服* 务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
  • 2、然后调用ServiceBean的export方法进行服务导出
  • 3、然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
  • 4、将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
  • 5、还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat、Netty等

Dubbo的架构设计是怎样的?

Dubbo中的架构设计是非常优秀的,分为了很多层次,并且每层都是可以扩展的,比如:

  • 1、Proxy服务代理层,支持JDK动态代理、javassist等代理机制
  • 2、Registry注册中心层,支持Zookeeper、Redis等作为注册中心
  • 3、Protocol远程调用层,支持Dubbo、Http等调用协议
  • 4、Transport网络传输层,支持netty、mina等网络传输框架
  • 5、Serialize数据序列化层,支持JSON、Hessian等序列化机制

分布式架构下,Session 共享有什么方案

1、采用无状态服务,抛弃session

2、存入cookie(有安全风险)

3、服务器之间进行 Session 同步,这样可以保证每个服务器上都有全部的 Session 信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失
4、IP 绑定策略

使用 Nginx(或其他复杂均衡软硬件)中的P 绑定策略,同一个 P 只能在指定的同一个机器访问,但是这样做失去了负载均衡的意义,当挂掉一台服务器的时候,会影响一批用户的使用,风险很大
5、使用 Redis 存储

把 Session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次 Redis,但是这种方案带来的好处也是很大的:

  • 实现了 Session 共享;
  • 可以水平扩展(增加 Redis 服务器);
  • 服务器重启 Session 不丢失(不过也要注意 Session 在 Redis 中的刷新/失效机制)
  • 不仅可以跨服务器 Session 共享,甚至可以跨平台(例如网页端和 APP 端)。

如何实现接口的幂等性

  • 唯一id。每次操作,都根据操作和内容生成唯一的id,在执行之前先判断ic是否存在,如果不存在则执行后续操作,并且保存到数据库或者redis等。
  • 服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时,把token携带过去,务器判断token是否存在redis中,存在表示
    第一次请求,可以继续执行业务,执行业务完成后,最后需要把redis中的token删除
  • 建去重表。将业务中有唯一标识的字段保存到去重表,如果表中存在,则表示已经处理过了
  • 版本控制。增加版本号,当版本号符合时,才能更新数据
  • 状态控制。例如订单有状态已支付 未支付 支付中 支付失败,当处于未支付的时候才允许修改为支付中等

有哪些分布式ID

  • UUID:简单、性能好,没有顺序,没有业务含义,存在泄漏mac地址的风险
  • 数据库主键:实现简单,单调送增,具有一定的业务可读性,强依赖db、存在性能瓶颈,存在暴露业务 信息的风险
  • redis, mongodb,zk等中间件:增加了系统的复杂度和稳定性
  • 雪花算法

雪花算法原理

Java面试 - 进阶(大厂Java面试题)_第7张图片

第一位符号位固定为0,41位时间戳,10位workld,12位序列号,位数可以有不同实现。
优点:每个毫秒值包含的ID值很多,不够可以变动位数來增加,性能佳(依赖worklc的实现)。时间戳值在高位,中间是固定的机器码,自增的序列在低
位,整个ID是趋势递增的。能够根据业务场景数据库节点布置灵活调整bit位划分,灵活度高。
缺点:强依赖于机器时钟,如果时钟回拨,会导致重复的ID生成,所以一般基于此的算法发现时钟回拨,都会抛异常处理,阻止ID生成,这可能导致服务不可用。

如何解决不使用分区键的查询问题

  • 映射:将查询条件的字段与分区键进行映射,建一张单独的表维护(使用覆盖索引)或者在缓存中维护

  • 基因法:分区键的后x个bit位由查询字段进行hash后占用,分区键直接取x个bit位获取分区,查询字段进行hash获取分区,适合非分区键查询字段只有一个的情况

  • 冗余:查询字段冗余存储

分布式系统中常用的缓存方案有哪些

  • 客户端缓存:页面和浏览器缓存,APP缓存,H5缓存,localStorage 和 sessionStorage CDN缓存:内容存储:数据的缓存,内容分发:负载均衡
  • nginx缓存:静态资源
  • 服务端缓存:本地缓存,外部缓存
  • 数据库缓存:持久层缓存 (mybatis, hibernate多级缓存),mysql查询缓存操作系统缓存:PageCache、 BufferCache

分布式缓存寻址算法

  • hash算法:根据key进行hash函数运算、结果对分片数取模,确定分片 适合固定分片数的场景,扩展分片或者减少分片时,所有数据都需要重新计
    算分片、存储

  • 一致性hash:将整个hash值得区间组织成一个闭合的圆环,计算每台服务器的hash值、映射到圆环中,使用相同的hash算法计算数据的hash值,映射到圆环,顺时针寻找,我到的第一个服务器就是数据存铺的服务器。新增及减少节点时只会影响节点到他逆时针最近的一个服务器之间的值 存在hash环倾斜的问题,即服务器分布不均匀,可以通过虚拟节点解决

  • hash slot: 将数据与服务器隔离开,数据与slot映射,slot与服务器映射,数据进行hash决定存放的slot,新增及删除节点时,将slot进行迁移即可

什么是服务雪崩?什么是服务限流?

  • 1、当服务A调用服务B,服务B调用C,此时大量请求突然请求服务A,假如服务A本身能抗住这些请求,但是如果服务C抗不住,导致服务C请求堆积,从而服务B请求堆积,从而服务A不可用,这就是服务雪崩,解决方式就是服务降级和服务熔断。

  • 2、服务限流是指在高并发请求下,为了保护系统,可以对访问服务的请求进行数量上的限制,从而防止系统不被大量请求压垮,在秒杀中,限流是非常重要的。

什么是服务熔断?什么是服务降级?区别是什么?

1、服务熔断是指,当服务A调用的某个服务B不可用时,上游服务A为了保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复。

2、服务降级是指,当发现系统压力过载时,可以通过关闭某个服务,或限流某个服务来減轻系统压力,这就是服务降级。

  • 相同点:

    • 1、都是为了防止系统崩溃
    • 2、都让用户体验到某些功能暂时不可用
  • 不同点:熔断是下游服务故障触发的,降级是为了降低系统负载

怎么拆分微服务?

拆分微服务的时候,为了尽量保证微服务的稳定,会有一些基本的准则:

  • 1、微服务之间尽量不要有业务交叉。

  • 2、微服务之前只能通过接口进行服务调用,而不能绕过接口直接访问对方的数据。

  • 3、高内聚,低耦合。

怎样设计出高内聚、低耦合的微服务?

  • 高内聚低耦合,是一种从上而下指导微服务设计的方法。实现高内聚低耦合的工具主要有 同步的接口调用 和 异步的事件驱动 两种方式。

有没有了解过DDD领域驱动设计?

  • 什么是DDD:在2004年,由Eric Evans提出了,DDD是面对软件复杂之道。Domain-Driven- Design -Tackling Complexity in the Heart of Software

  • 大泥团:不利于微服务的拆分。大泥团结构拆分出来的微服务依然是泥团机构,当服务业务逐渐复杂,这个泥团又会膨胀成为大泥团。

DDD只是一种方法论,没有一个稳定的技术框架。DDD要求领域是跟技术无关、跟存储无关、跟通信无关。

什么是中台?

所谓中台,就是将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用的组件。大体上,中台可以分为三类 业务中台、数据中台和技术中台。大数据杀熟-数据中台中台跟DDD结合:DDD会通过限界上下文将系统拆分成一个一个的领域,而这种限界上下文,天生就成了中台之间的逻辑屏障。DDD在技术与资源调度方面都能够给中台建设提供不错的指导。DDD分为战略设计和战术设计。上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建。

你的项目中是怎么保证微服务敏捷开发的?

  • 开发运维一体化。
  • 敏捷开发:目的就是为了提高团队的交付效率,快速迭代,快速试错
  • 每个月固定发布新版本,以分支的形式保存到代码仓库中。快速入职。任务面板、站立会议。团队人员灵活流动,同时形成各个专家代表
  • 测试环境一生产环境-开发测试环境SIT-集成测试环境-压测环境STR-预投产环境-生产环境PRD
  • 晨会、周会、需求拆分会

epoll和poll的区别

  • select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了IO事件

  • poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了IO事件

  • epoll模型,epol和poll是完全不同的,epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行IO操作,不需要像poll模型那样主动去轮询

浏览器发出一个请求到收到响应经历了哪些步骤?

  • 1、浏览器解析用户输入的URL,生成一个HTTP格式的请求
  • 2、先根据URL域名从本地hosts文件查找是否有映射P,如果没有就将域名发送给电脑所配置的DNS进行域名解析,得到P地址
  • 3、浏览器通过操作系统将请求通过四层网络协议发送出去
  • 4、途中可能会经过各种路由器、交换机,最终到达服务器
  • 5、服务器收到请求后,根据请求所指定的端口,将请求传递给绑定了该端口的应用程序,比如8080被tomcat占用了tomcat接收到请求数据后,按照http协议的格式进行解析,解析得到所要访问的servlet
  • 6、然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的Controller中的方法,并执行该方法得到结果
  • 7、Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过网络发送给浏览器所在的服务器
  • 8、浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染

跨域请求是什么?有什么问题?怎么解决?

跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否一致,如果不一致则浏览器会进行限制,比如在www.baidu.com的某个网页中,如果使用ajax去访问www.jd.com是不行的,但是如果是img、 iframe、script等标签的src属性去访问则是可以的,之所以浏览器要做这层限制,是为了用户信息安全。但是如果开发者想要绕过这层限制也是可以的:

  • 1、response添加header,比如resp。setHeader(“Access-Control-Allow-Origin“,“大“);表示可以访问所有网站,不受是否同源的限制

  • 2、jsonp的方式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的

  • 3、后台自己控制,先访问同域名下的接口,然后在接口中再去使用HTTPClient等工具去调用目标接口

  • 4、网关,和第三种方式类似,都是交给后台服务来进行跨域访问

零拷贝是什么

零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外一块内核区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,币直接实现转移。

Java面试 - 进阶(大厂Java面试题)_第8张图片

MySQL里有2000w数据Redis中只存20w的数据,如何保证 redis 中的数据都是热点数据?

首先我们可以看到Redis的空间实际上比我们MySQL少的多,那么Redis如何能够筛选出热点数据,这道题主要考察的是Redis的数据淘汰策略(这里有个误区,很多人容易混淆把数据淘汰策略当做数据过期策略),在Redis 4.0之后是为我们提供了8种淘汰策略,4.0之前则是提供的6种,主要是新增了LFU算法。其实说说是有8种,但是真正意义上是5种,针对random、 Iru、ffu是提供了两种不同数据范围的策略,一种是针对设置了过期时间的,一种是没有设置过期时间的。具体的五种策略分别为:

  • noeviction 选择这种策略则代表不进行数据淘汰,同时它也是redis中默认的淘汰策略,当缓存写满时redis就不再提供写服务了,写请求则直
    接返回失败。

  • random 随机策略这块则是分为两种,一种是volatile,这种是设置了过期时间得数据集,而另外一种是allkeys,这种是包含了所有的数据,
    当我们缓存满了的时候,选用这种策略就会在我们的数据集中进行随机删除。

  • volatile-ttl 这种策略是针对设置了过期时间的数据,并且按照过期时间的先后顺序进行删除,越早过期的越先被删除
    图灵课堂

  • Iru 这里的Iru策略和我们上面random策略一样也是提供了两种数据集进行处理,LRU算法全程为(最近最少使用)简单一句话来概括就是“如
    果数据最近被访问过,那么将来被访问的几率也就越高”。这种算法其实己经比较符合我们的实际业务需求了,但是还是存在一些缺陷。

  • Ifu 最后一种策略就是我们的LFU算法,它是在我么LRU算法基础上增加了请求数统计,这样能够更加精准的代表我们的热点数据。

我们再回看我们的这个问题,我们能很清楚的知道,我们需要的策略是LFU算法。选择volatile还是allkeys就要根据具体的业务需求了。

高并发下我们如何去保证接口的幂等性?

首先普及下幂等的概念

“在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数”那么在我们的实际业务场景中幂等是一个非常高频的场景,比如:

  • 电商场景中用户因网络问题多次点击导致重复下单问题
  • MQ消息队列的重复消费
  • RPC中的超时重试机制
  • 等等

那么我们有那些方案可以解决我们的幂等性问题呢?

数据库唯一主键实现幂等性
  • 其实现方式是使用分布式ID充当主键,不使用MySQL中的自增主键
乐观锁实现幂等性
  • 在表中增加版本号标识,只有版本号标识一直才更新成功
分布式锁
  • 简单来说就是分布式的排他锁,但是我们可以控制锁的粒度以提高程序的执行性能
获取token
  • a、服务端提供获取 Token 的接口,请求前客户端调用接口获取 Token

  • b、然后将该串存入 Redis 数据库中,以该 Token 作为 Redis 的键(注意设置过期时间),

  • c、将 Token 返回到客户端,在执行业务请求带上该 Token

  • d、服务端接收到请求后根据 Token 到 Redis 中查找该 key 是否存在(注意原子性),

  • e、如果存在就将该 key 删除,然后正常执行业务逻辑。如果不存在就抛异常,返回重复提交的错误信息。

Redis如何保证与数据库的双写一致性

我们来分析一下这道面试题,这道题主要是偏实际应用的

缓存可以提升性能,减轻数据库压力,在获取这部分好处的同时,它却带来了一些新的问题,缓存和数据库之间的数据一致性问题。

想必大家在工作中只要用了咱们缓存势必就会遇到过此类问题,那这道题该如何回答呢?

首先我们来看看一致性:

  • 强一致性:任何一次读都能读到某个数据的最近一次写的数据。

  • 弱一致性:数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

解决双写一致性方案:

延迟双删

  • 延时双删策略是分布式系纷中毁据库存馆和缓存数据保特一致性的常用策略。但它不是强一致。

  • 实现思路:也是非常简单的,先删除缓存然后更新DB在最后延迟 N 秒去再去执行一次缓存删除

  • 弊端:小概率会出现不一致情况、耦合程度高

通过MQ进行重试删除

  • 更新完DB之后进行删除,如果删除失败则向MQ发送一条消息,然后消费者不断进行删除尝试。

binlog异步删除

  • 实现思路:低耦合的解决方案是使用canal。canal伪装成mysql的从机,监听主机mysql的二进制文件,当数据发生变化时发送给MQ。最终消费进行删除

灰度发布

  • 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

全量发布

  • 回滚周期长
  • BUG导致服务集群雪崩
  • 服务可用性差,影响用户体验

灰度发布

  • 降低发布影响面,提升用户体验
  • 可以做到不停机迁移
  • 回滚速度快

Java面试 - 进阶(大厂Java面试题)_第9张图片

金丝雀发布

  • 据说以前有个典故,矿工开矿前,会先放一只金丝雀下去,看金丝雀是否能活下来,用来探测是否有毒气,金丝雀发布也是由此得名。

  • 灰度发布是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。

全链路灰度发布

  • 全链路灰度治理策略主要专注于整个调用链,它不关心链路上经过具体哪些微服务,流量控制视角从服务转移至请求链路上,仅需要少量的治理规则即可构建出从网关到整个后端服务的多个流量隔离环境,有效保证了多个亲密关系的服务顺利安全发布以及服务多版本并行开发,进一步促进业务的快速发展。

Redisson实现分布式锁原理

首先我们来讲讲实现一个分布式锁我们需要考虑那些东西?

  • 互斥性 setNx
  • 防死锁
  • 可重入性
  • 高性能

一张图带你弄懂Redisson分布式锁原理

Java面试 - 进阶(大厂Java面试题)_第10张图片

怎么用Java代码写一个死锁吗?

这道面试题是考察面试者对死锁的了解程度,在这个角度来提问也是很有趣。首先,我们如果要实现死锁的话有四个必要的条件:

  • 互斥使用(也就是说当我们的资源被一个线程占有时,那别的线程也就不能使用了)
  • 不可抢占(请求线程不能强制从占用线程夺取资源,只能由占有线程主动去释放)
  • 请求和保持(当请求线程在请求其他资源的时候要保持对原有资源的占有)
  • 循环等待(要存在一个循环等待,也就是说A线程要B资源,而B线程要A资源。这就形成了一个等待环路)

那么我们解决死锁由那些方案呢?

  • 设置超时时间(如果我们在某个时间段里面没有拿到锁的话,我们就不再死等了,去做别的事情,比如说用JUC包里面Lock接口提供的
    tryLock方法,而不是像用synchronized那样去一直等待。)
  • 降低锁粒度(比如说一个类如果使用一个锁来保护的话,第一个问题是效率低,还有就是死锁风险非常大,只要能够满足我们业务的要求就
    去尽量减少锁的使用)
  • 避免嵌套锁(我们前面演示的例子就是一个嵌套锁,那么在多线程环境使用时,一旦获取锁的顺序反了,就势必会造成死锁)
  • 专锁专用(不要很多功能都去用同一把锁,去避免锁冲突,如果很多线程都用同一把锁,就很容易造成死锁)
  • 银行家算法(这种思想有一个著名的算法叫做银行家算法,其核心就是分配资源时先看能不能收回来资源(也就是说在分配之前先算一下如果分配出去了会不会造成死锁,要是收不回来造成死锁就不分配锁给这个线程,要是能收回来就分配。)

MySQL8隐藏索引

  • MySQL 8.0 支持了 Invisible Indexes 隐藏索引 这个特性,可以把某个索引设置为对优化器不可见,生成查询计划时便不使用这个索引了,但这个索引还是被正常维护的,例如表数据变更后还是会更新索引。

  • 我们都知道维护一个索引带来的成本是很高的,当数据量越大的时候成本越高。不知道大家有没有遇到过一种场景就是做数据库优化时,我们可能会去删除一些多余的索引,我们要去线上禁用掉一个索引然后去观察对查询效率的一个影响,在8.0之前我们可能只能将数据导出到一个新的环境,然后再去删除掉这个索引,如果观察发现影响面比较大,那么就不能去删。还有一种方案是直接删线上的索引不行再加回来,稍微体量大一点的公司都是不被允许的。

  • 那么在8.0 之后我们就不用这么麻烦了,官方为我们提供了一个新的功能叫隐藏索引,或者你可以理解成为灰度索引,它的作用类似于回收站功能。虽然我删除了某个文件但是这个文件其实还在。如果碰到上面的这种场景,就可以直接使用这个隐藏索引。

索引设置为隐藏索引

alter table xxx alter index xxx invisible;

索引设置为非隐藏索引

alter table xxx alter index xxx visible;

private修饰的方法可以通过反射访问,那么private的意义是什么?

  • 在Java当中,如果为了我们不想让别人访问某些属性、方法,我们通常的做法是使用private关键字进行修饰。但是Java语言在设计时却允许通过反射来进行访问,只需要关掉访问检查就可以了。
import java.lang.reflect.Field;
public class Test {
	public static void main(String [] args) {
		C c= new C();
		try {
			Field f = C.class.getDeclaredField("a");
			f.setAccessible(true);
			Integer i= (Integer)f.get(c);
			System.out.println(i);
		} catch (Exception e) {}
	}
}

class C {
	private Integer a = 6;
}

那么private修饰符不是失去了它原本的意义了吗?

  • 其实不尽然,本身private修饰符是基于OOP思想下封装概念的一种体现,对于使用者而言是一种约束,并不是一种安全机制。就好比说,你在道路上行 驶,导航时不时提醒你限速,这是道路交通法的规定,但是救护车碰上情况是不是也会超速对吧,这个就是这两者的差异。

  • 你在用spring的loC的时候,你知道你要“注入”,不管它是不是private的,都能够注入进去对吧。

  • 如果你按照遵守这套规则,开发者在不考虑bug的情况下可以保证不出问题,否则就很有可能在你意想不到的地方带来灾难性的后果。

你可能感兴趣的:(Java,面试,java,经验分享,后端,分布式,中间件)