JAVA八股文

JAVA八股文

未完成部分:

spring

springmvc

springboot

计网

jvm相关(看着搞)

rpc框架dubbo(不搞)

Zookeeper(不搞)

java基础

1、String 和StringBuffer和 StringBuilder的区别?

String 字符串常量(不可变,在更换值时因为创建新对象要丢弃无引用的对象启用gc效率降低)
StringBuffer 字符串变量(线程安全)
StringBuilder 字符串变量(非线程安全,效率比StringBuffer高)

简要的说, String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。
而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接,所以这些时候 String 对象的速度并不会比 StringBuffer 对象慢,而特别是以下的字符串对象生成中, String 效率是远要比 StringBuffer 快的:
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你会很惊讶的发现,生成 String S1 对象的速度简直太快了,而这个时候 StringBuffer 居然速度上根本一点都不占优势。其实这是 JVM 的一个把戏,在 JVM 眼里,这个
String S1 = “This is only a” + “ simple” + “test”; 其实就是:
String S1 = “This is only a simple test”; 所以当然不需要太多的时间了。但大家这里要注意的是,如果你的字符串是来自另外的 String 对象的话,速度就没那么快了,譬如:
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;
这时候 JVM 会规规矩矩的按照原来的方式去做

在大部分情况下 StringBuffer > String
StringBuffer
Java.lang.StringBuffer线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
可将字符串缓冲区安全地用于多个线程。可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含“startle”,而 z.insert(4, “le”) 将更改字符串缓冲区,使之包含“starlet”。
在大部分情况下 StringBuilder > StringBuffer

java.lang.StringBuilder
java.lang.StringBuilder一个可变的字符序列是5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的方法基本相同。

2. sleep() 区间wait()区间有什么区别?

sleep 是Thread中的方法,线程暂停,让出CPU,但是不释放锁

wait()是Object中的方法, 调用次方法必须让当前线程必须拥有此对象的monitor(即锁),执行之后 线程阻塞,让出CPU, 同时也释放锁; 等待期间不配拥有CPU执行权, 必须调用notify/notifyAll方法唤醒,(notify是随机唤醒) 唤醒并不意味着里面就会执行,而是还是需要等待分配到CPU才会执行;

3. Object 中有哪些方法?其中clone(),怎么实现一个对象的克隆,Java如何实现深度克隆?

clone是浅拷贝;只克隆了自身对象和对象内实例变量的地址引用,使用它需要实现接口Cloneable;
使用ObjectStream进行深度克隆; 先将对象序列化;然后再反序列化;

ThreadLocal 相关

内存泄漏:

https://blog.csdn.net/JH39456194/article/details/107304997

4. ThreadLocal作用和实现方式 ?

TL用于保存本地线程的值, 每个Thread都有一个threadLocals属性,它是一个ThreadLocalMap对象,本质上是一个Entry数组;Entry是k-v结构; 并且是WeakReference弱引用, K存的是 ThreadLocal对象,Value是设置的值; 那么每个线程就可以读自己设置的值了;

ThreadLocal会不会发生内存泄漏?

会发生内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

  1. 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏
  2. 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

ThreadLocal为什么使用弱引用?

key是弱引用好歹还可以 GC掉key的对象;强引用则不行
使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

5. InheritableThreadLocal作用和实现方式 ?

InheritableThreadLocal基础 ThreadLocal ; 他跟ThreadLocal区别是 可以传递值给子线程; 每个Thread都有一个inheritableThreadLocals属性, 创建子线程的时候,会把父线程的Entry数组 塞到子线程的Entry数组中; 所以就实现了父子线程的值传递; 注意如果Value是一个非基本类型的对象, 父子线程指向的是相同的引用; 子线程如果修改了值,父线程也是会修改的;

6. InheritableThreadLocal所带来的问题?

线程不安全: 如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量

线程池中可能失效: 在使用线程池的时候,ITL会完全失效,因为父线程的TLMap是通过Thread的init方法的时候进行赋值给子线程的,而线程池在执行异步任务时可能不再需要创建新的线程了,因此也就不会再传递父线程的TLMap给子线程了

HashMap ConcurrentHashMap相关

HashMap为什么线程不安全

https://blog.csdn.net/swpu_ocean/article/details/88917958

https://www.bilibili.com/video/BV1n541177Ea?from=search&seid=13908663008727288935

1.在JDK1.7中,当并发执行扩容操作时会造成环形链的情况。(链表的头插法 造成环形链)

在JDK1.7中,当多个线程同时插入元素时,因为使用头插法,会造成数据丢失

2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。(元素插入时使用的是尾插法)
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

HashMap在jdk7和8中的区别

  1. JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法就是能够提高插入的效率,但是也会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
  2. 扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1) . 而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

HashMap 为啥将链表改成红黑树?

提高检索效率,在链表长度大于8的时候,将后面的数据存在红黑树中,以加快检索速度。复杂度变成O(logn)

ConcurrentHashMap在jdk7和8中的区别?

ConcurrentHashMap线程安全

可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言

  1. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
  2. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
  3. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
  4. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock; 因为粒度降低了

锁(多线程笔记)

说一下javaSE1.6对synchronized锁的优化?

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能大的影响是阻塞的是实现,挂起 线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力
javaSE1.6引入了偏向锁,轻量级锁(自旋锁)后,synchronized和ReentrantLock两者的性能就差不多了
锁可以升级, 但不能降级. 即: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的.

  • 偏向锁
    偏向锁: HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得; 偏向锁是四种状态中最乐观的一种锁:从始至终只有一个线程请求某一把锁。
    偏向锁的获取: 当一个线程访问同步块并成功获取到锁时,会在对象头和栈帧中的锁记录字段里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,直接进入
    偏性锁的撤销: 偏向锁使用了一种等待竞争出现才释放锁的机制,所以当其他线程竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,并将锁膨胀为轻量级锁(持有偏向锁的线程依然存活的时候)

  • 轻量级锁
    多个线程在不同的时间段请求同一把锁,也就是说没有锁竞争。
    加锁: 线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
    解锁:轻量级锁解锁时, 会使用原子的CAS操作将当前线程的锁记录替换回到对象头, 如果成功, 表示没有竞争发生; 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁.

  • 重量级锁
    Java线程的阻塞以及唤醒,都是依靠操作系统来完成的,这些操作将涉及系统调用,需要从操作系统 的用户态切换至内核态,其开销非常之大。

  • 锁粗化:

    锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁

  • 锁消除:

    锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程, 那么可以认为这段代码是线程安全的,不必要加锁

ReentrantLock和synchronized的区别?

  • 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。

  • synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

  • synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

  • synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁

  • synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

为什么重写equals时候被要求重写hashCode()?

当对象的equals()方法被重写时,通常有必要重写 hashCode() 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码

如果两个对象相同(即:用 equals 比较返回true),那么它们的 hashCode 值一定要相同
如果两个对象的 hashCode 相同,它们并不一定相同(即:用 equals 比较返回 false
为了提供程序效率 通常会先进性hashcode的比较,如果不同,则就么有必要equals比较了;

什么时候回发生内存泄露?让你写一段内存泄露的代码你会怎么写?

我们知道,对象都是有生命周期的,有的长,有的短,如果长生命周期的对象持有短生命周期的引用,就很可能会出现内存泄露

Vector v = new Vector(10);
for (int i = 0; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
}

ThreadLocal使用不当也可能泄漏

Java内存模型

在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写 - 读内存中的公共状态来隐式进行通信。Java 的并发采用的是共享内存模型

img

Java 内存模型中的 happen-before 是什么?

从 JDK5 开始,java 使用新的 JSR -133 内存模型,提出了 happens-before 的概念
前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。

  1. 程序顺序规则:在一个线程内一段代码的**执行结果是有序的。**就是还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
  2. 监视器锁规则:就是无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
  3. volatile 变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
  4. 传递性:如果 A happens- before B,且 B happens- before C,那么 A happens- before C。

简单聊聊volatile 的特性?以及内存语义

可见性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
禁止重排序:对于一个非原子性的操作,cpu会始终严格按照执行顺序执行

volatile 写的内存语义:当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存
volatile 读的内存语义: 当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量

如何实现的禁止重排序

在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

通过反编译可以看到,有volatile变量修饰的遍历,会有一个lock前缀的指令,lock前缀的指令在多核处理器下会引发了两件事情

将当前处理器缓存行的数据会写回到系统内存。
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

GC垃圾回收

根可达算法

roots:线程栈变量 静态变量 常量池 jni指针

垃圾回收算法:

  • 标记清除:标记哪块是垃圾然后直接清楚

    • 优点:简单
    • 缺点:碎片化,浪费内存,可能导致无法找到一个较大的空间来存放较大的对象
  • 拷贝算法:对于一块空间,设置一半可以存放对象,一半禁止访问,当前一半有对象后,将前一半的所有对象移到禁止访问的那一半空间去,就不会出项碎片化

    • 优点:效率高,防止碎片化
    • 缺点:浪费空间
  • 标记压缩:在标记清楚后,将剩余对象压缩在一起,就不会出现碎片化

    • 缺点:效率较低

Minor GC:回收年轻代

Major GC:回收老年代

Full GC:全回收

对象在内存中的移动过程

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

10种垃圾回收器:(对上述三种算法的排列组合)

垃圾回收主要是针对 内存区的哪些区域?

主要追对的是 Java堆 和 方法区 ;
java栈、程序计数器、本地方法栈都是线程私有的,线程生就生,线程灭就灭,栈中的栈帧随着方法的结束也会撤销,内存自然就跟着回收了。所以这几个区域的内存分配与回收是确定的,我们不需要管的。但是java堆和方法区则不一样,我们只有在程序运行期间才知道会创建哪些对象,所以这部分内存的分配和回收都是动态的。一般我们所说的垃圾回收也是针对的这一部分。

  • Java堆:这个是虚拟机管理内存中最大的一部分。也是回收器管理的主要区域。Java堆区的内存由所有线程共享,在虚拟机启动的时候创建。主要保存的是:对象实例和数组。由于现在收集器采用的分代回收,故将Java堆中对象分为:新生代和老年代。这个会在后面根据不同的状态使用不同的收集器回收垃圾。
  • 方法区:这个区也是内存共享区域。它的作用是存储已经被加载的类信息、常量、静态变量、即时编译后的代码等数据。这个区域很少被收集器回收,一般我们称为”永久代“。

垃圾检查有哪些算法?

  1. 引用计数法 :给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。
    好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收,所以还有另一种方法:
  2. 可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常量池中引用的对象

垃圾回收方法有哪些?

  1. 标记-清除(Mark-sweep):标记清除算法分为两个阶段,标记阶段和清除阶段。标记阶段任务是标记出所有需要回收的对象,清除阶段就是清除被标记对象的空间。优缺点:实现简单,容易产生内存碎片

  2. 复制(Copying)将可用内存划分为大小相等的两块,每次只使用其中的一块。当进行垃圾回收的时候了,把其中存活对象全部复制到另外一块中,然后把已使用的内存空间一次清空掉。 优缺点:不容易产生内存碎片;可用内存空间少;存活对象多的话,效率低下。

  3. 标记-压缩(Mark-Compact)先标记存活对象,然后把存活对象向一边移动,然后清理掉端边界以外的内存 优缺点:不容易产生内存碎片;内存利用率高;存活对象多并且分散的时候,移动次数多,效率低下

    分代收集算法(目前大部分JVM的垃圾收集器所采用的算法)
    年轻代(Young Generation)的回收算法 (回收主要以Copying为主)
    年老代(Old Generation)的回收算法(回收主要以Mark-Compact为主)

什么时候会触发Full GC

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区(1.8之后改为元空间)空间不足
(4)创建大对象,比如数组,通过Minor GC后,进入老年代的平均大小大于老年代的可用内存(为了避免新生代晋升到老年代失败)
(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。(就是eden—》s1–>s2–>old都放不下就会full gc)

GC机制简要说明一下,不同区使用的算法。

  • 年轻代:是所有新对象产生的地方。年轻代被分为3个部分——Enden区和两个Survivor区(From和to)当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。
  • 年老代:在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。
  • 持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响

两个对象循环引用会不会被被GC?

GC里边在JVM当中是使用的ROOT算法,ROOT算法 也就是根; 只要看这个两个对象有没有挂在 根 上, 挂在根上了 就不会被回收; 没有挂在根上就会回收;

哪些可以算作根节点?

  1. 方法区中的静态属性
  2. 方法区的中的常量
  3. 虚拟机中的局部变量
  4. 本地方法栈中JNI

CMS垃圾回收器:

  1. 初始标记(CMS-initial-mark) ,会导致STW(stop-the-world);
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行
  4. 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
  5. 重新标记(CMS-remark) ,会导致STW; 这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
    这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了
  6. 并发清除(CMS-concurrent-sweep),与用户线程同时运行;

JVM相关

暂时不看

多线程

  1. 请描述synchronized和reentrantlock的底层实现及重入的底层原理

    synchronized是用来保证线程同步,用的锁存在java对象头中,利用monitorenter和monitorexit指令实现,monitorenter指令是在编译后插入到同步代码块开始位置,而monitorexit是插入到方法结束后和异常处。jdk1.6之后引入了大量的优化,这其中又涉及到锁的四种升级状态:new(无锁) →偏向锁→轻量级锁(自旋锁)→重量级锁。而底层,synchrnoized是利用操作系统的Mutex Lock(互斥锁)来实现的

    ReentrantLock(可重入锁)是基于AQS的,AQS是Java并发包中众多同步组件的构建基础,它通过一个int类型的状态变量state和一个FIFO队列来完成共享资源的获取,线程的排队等待等

  2. 请描述锁的四种状态和升级过程

    :无锁、偏向锁、轻量级锁、重量级锁,new–偏向锁(当第一个线程调用这个对象时就上了这把锁,里面记录了线程的id号)–轻量级锁(当有另一个线程竞争该对象时,升级为轻量级锁)(无锁,自旋锁,自适应自旋)–重量级锁(等待的线程会进入该锁(非公平锁,队列不有序)的等待队列进行wait,不消耗cpu资源,而轻量级锁是要消耗资源的)

  3. CAS的ABA问题如何解决

    :类似乐观锁的做法,给变量加一个版本号,当其他线程修改变量时,版本号++,从原来的对比变量的值改为对比版本号。

  4. 请谈一下AQS,为什么AQS的底层是CAS+volatile

    1. 使用 volatile 关键字修饰一个int类型的同步标志位state,初始值为0;
    2. 加锁/释放锁时使用CAS操作对同步标志位state进行更新;
      • 加锁成功,同步标志位值为 1,加锁状态;
      • 释放锁成功,同步标志位值为0,初始状态;
    3. 这样就实现了一把乐观锁,就能解决一系列的并发编程
  5. 请谈一下你对volatile的理解

    :volatile主要有两个作用一个是保证线程可见性,一个是禁止指令重排

  6. volatile的可见性和禁止指令重排序时如何实现的

    :可见性是通过cpu多级缓存,当标识该变量volatile时,线程每次从主存中重新读取该数据。禁止指令重排序是通过添加内存屏障实现。

  7. CAS是什么

    :CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。

  8. 请描述一下对象的创建过程

    1. 开辟内存空间,将变量设为默认值

    2. 调用构造函数修改变量的值

    3. 将变量名(指针)指向该对象。

  9. 对象在内存中的内存布局

    :对象头,类型指针,实例数据,对齐。

  10. DCL(double-checked locking)单例为什么要加volatile

    :在DCL中。创建对象的步骤有三步:

    1. 开辟内存空间,将变量设为默认值
    2. 调用构造函数修改变量的值
    3. 将变量名(指针)指向该对象。

    如果上述步骤2,3调换,即执行了1,3步骤,此时跑进来一个线程,因为对象不为空则返回该对象,所以该线程就获得了一个未初始化完成的变量。这样便会出问题,所以需要给变量加上volatile关键字。

  11. Object o = new Object() 在内存中占了多少字节

    :16字节

  12. 请描述synchronized和ReentrantLock的异同

    • 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。

    • synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。

    • synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。

    • synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁

    • synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

  13. 聊聊你对as-if-serial和happens-before语义的理解

  14. 你了解ThreadLocal吗?你知道ThreadLocal中如何解决内存泄漏问题吗?

    :通过弱引用解决内存泄漏问题,追到源码中去看,会将值设置为Entry类型,而该Eentry继承了弱引用类。

常用排序算法

  • 冒泡排序(将最大数冒泡到数组最后)

    public class BubbleSort {
        public static void BubbleSort(int[] arr) {
            int temp;//定义一个临时变量
            for (int i = 0; i < arr.length - 1; i++) {//冒泡趟数
                for (int j = 0; j < arr.length - i - 1; j++) {
                    if (arr[j + 1] < arr[j]) {
                        temp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = temp;
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            int arr[] = new int[]{1, 6, 2, 2, 5};
            BubbleSort(arr);
            System.out.println(Arrays.toString(arr));
        }
    }
    
  • 选择排序(找出最小的放最前面)

    public class SelectionSort {
        public static void main(String[] args) {
            int[] arr={1,3,2,45,65,33,12};
            System.out.println("交换之前:");
            for(int num:arr){
                System.out.print(num+" ");
            }
            //选择排序的优化
            for(int i = 0; i < arr.length - 1; i++) {// 做第i趟排序
                int k = i;
                for(int j = k + 1; j < arr.length; j++){// 选最小的记录
                    if(arr[j] < arr[k]){
                        k = j; //记下目前找到的最小值所在的位置
                    }
                }
                //在内层循环结束,也就是找到本轮循环的最小的数以后,再进行交换
                if(i != k){  //交换a[i]和a[k]
                    int temp = arr[i];
                    arr[i] = arr[k];
                    arr[k] = temp;
                }
            }
            System.out.println();
            System.out.println("交换后:");
            for(int num:arr){
                System.out.print(num+" ");
            }
        }
    }
    
  • 插入排序(将数组分为左右两个数组,左边数组有序,右边无序,将右边数组的第一位一个一个挪到左边的有序数组中)

    import java.util.Arrays;
    
    public class InsertSort {
        public static void main(String[] args) {
            int[] array={12,73,45,69,35};
            int i,j,temp;
            for(i=1;i<array.length;i++) {
                /*
                 * 第一个for循环
                 * 把数组分成两部分,右边为未排序,左边为已排序
                 * 记录排序与未排序分割点temp(temp为下一个排序对象)
                 */
                temp=array[i];
                for(j=i-1;j>=0;j--) {
                    /*
                     * 第二个for循环
                     * 将排序对象temp与已排序数组比较
                     * 当temp比最近左边的数大时(按从小到大循序排列时)
                     * 直接结束本次循环,进行下一个数排序
                     * 否则比左边这个数小时将这个数后移,腾出这个数的位置
                     */
                    if (temp > array[j]) {
                        break;
                    }else{
                        array[j+1] = array[j];
                    }
                }
                array[j+1]=temp;
            }
            System.out.println(Arrays.toString(array));
        }
    }
    
  • 希尔排序(对插入排序的改进)

    import java.util.Arrays;
    
    
    public class Shell {
        public static void main(String[] args) {
            int[] arr = SortTestHelper.getRandomArray(15, 0, 10);
            System.out.println("希尔排序前:"+Arrays.toString(arr));
            shellSort(arr);
            System.out.println("希尔排序后:"+Arrays.toString(arr));
        }
    
        /**
         * 希尔排序
         * @param arr 待排数组
         */
        public static void shellSort(int[] arr) {
            for(int gap=arr.length/2; gap>0; gap/=2) { /*步长逐渐减小*/
                for(int i=gap; i<arr.length; i++) { /*在同一步长内*/
                    //同一步长内排序方式是插入排序
                    int temp = arr[i], j; //待排元素
                    //j-gap代表有序数组中最大数的下标,j-pag表示有序数组的前一个元素,减pag是减去偏移量就是步长
                    for(j=i; j>=gap && temp<arr[j-gap]; j-=gap)
                        arr[j] = arr[j-gap]; //原有序数组最大的后移一位
                    arr[j] = temp; //找到了合适的位置插入
                }
            }
        }
    }
    
    class SortTestHelper {
    
        /**
         *
         * @param n 生成n个元素的随机数组
         * @param rangeL 随机范围[rangeL
         * @param rangeR	rangeR]
         * @return 返回一个随机 int 型数组
         */
        public static int[] getRandomArray(int n, int rangeL, int rangeR) {
            int[] arr = new int[n];
            for(int i=0; i<n; i++) {
                arr[i] = (int)(Math.random() * (rangeR - rangeL +1)) + rangeL;
            }
            return arr;
        }
    }
    
  • 归并排序(将数组分成两个有序数组并合并)

    public class MergeSort {
        public static void main(String []args){
            int arr[] = {6,1,7,2,4,3,8,5};
            sort(arr);
            System.out.println(Arrays.toString(arr));
        }
        public static void sort(int []arr){
            int []temp = new int[arr.length];//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
            sort(arr,0,arr.length-1,temp);
        }
        private static void sort(int[] arr,int left,int right,int[] temp){
            if(left<right){
                int mid = (left+right)/2;
                sort(arr,left,mid,temp);//左边归并排序,使得左子序列有序
                sort(arr,mid+1,right,temp);//右边归并排序,使得右子序列有序
                merge(arr,left,mid,right,temp);//将两个有序子数组合并操作
            }
        }
        private static void merge(int[] arr,int left,int mid,int right,int[] temp){
            int i = left;//左序列指针
            int j = mid+1;//右序列指针
            int t = 0;//临时数组指针
            while (i<=mid && j<=right){
                if(arr[i]<=arr[j]){
                    temp[t++] = arr[i++];
                }else {
                    temp[t++] = arr[j++];
                }
            }
            while(i<=mid){//将左边剩余元素填充进temp中
                temp[t++] = arr[i++];
            }
            while(j<=right){//将右序列剩余元素填充进temp中
                temp[t++] = arr[j++];
            }
            t = 0;
            //将temp中的元素全部拷贝到原数组中
            while(left <= right){
                arr[left++] = temp[t++];
            }
        }
    }
    
  • 快速排序

    import java.util.Arrays;
    
    public class QuickSort {
        public static void quickSort(int[] arr,int low,int high){
            int i,j,temp,t;
            if(low>high){
                return;
            }
            i=low;
            j=high;
            //temp就是基准位
            temp = arr[low];
    
            while (i<j) {
                //先看右边,依次往左递减
                while (temp<=arr[j]&&i<j) {
                    j--;
                }
                //再看左边,依次往右递增
                while (temp>=arr[i]&&i<j) {
                    i++;
                }
                //如果满足条件则交换
                if (i<j) {
                    t = arr[j];
                    arr[j] = arr[i];
                    arr[i] = t;
                }
    
            }
            //最后将基准为与i和j相等位置的数字交换
            arr[low] = arr[i];
            arr[i] = temp;
            //递归调用左半数组
            quickSort(arr, low, j-1);
            //递归调用右半数组
            quickSort(arr, j+1, high);
        }
    
    
        public static void main(String[] args){
            int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
            quickSort(arr, 0, arr.length-1);
            System.out.println(Arrays.toString(arr));
        }
    }
    

RPC框架 DUBBO

暂时不看

Zookeeper

暂时不看

Redis

https://www.cnblogs.com/javazhiyin/p/13839357.html

redis是单线程还是双线程?

4.0之前是单线程,4.0之后引入了多线程的概念

Redis有哪些优缺点

优点

  • 读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
  • 支持数据持久化,支持AOF和RDB两种持久化方式。
  • 支持事务,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
  • 数据结构丰富,除了支持string类型的value外还支持hash、set、zset、list等数据结构。
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。

缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

为什么要用 Redis /为什么要用缓存

高性能:

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

Redis 常见面试题(2020最新版)

为什么要用 Redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。

为了解决缓存一致性问题。

Redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路 I/O 复用模型,非阻塞 IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

Redis有哪些数据类型

Redis 常见面试题(2020最新版)

Redis持久化?

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 的持久化机制是什么?各自的优缺点?

Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:

RDB:是Redis DataBase缩写快照

RDB是Redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中,对应产生的数据文件为dump.rdb。通过配置文件中的save参数来定义快照的周期。

Redis 常见面试题(2020最新版)

优点:

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

缺点:

  • 1、数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候)

AOF:持久化

AOF持久化(即Append Only File持久化),则是将Redis执行的每次写命令记录到单独的日志文件中,当重启Redis会重新将持久化的日志中文件恢复数据。

当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

Redis 常见面试题(2020最新版)

优点:

  • 1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次 命令操作就记录到 aof 文件中一次。
  • 2、通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
  • 3、AOF 机制的 rewrite 模式。AOF 文件没被 rewrite 之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的 flushall))

缺点:

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

优缺点是什么?

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

如何选择合适的持久化方式

  • 一般来说, 如果想达到足以媲美PostgreSQL的数据安全性,你应该同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
  • 如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使用RDB持久化。
  • 有很多用户都只使用AOF持久化,但并不推荐这种方式,因为定时生成RDB快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比AOF恢复的速度要快,除此之外,使用RDB还可以避免AOF程序的bug。
  • 如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

Redis做缓存怎么做扩容?

  • 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。(https://www.bilibili.com/video/BV1Hs411j73w?from=search&seid=5865420720045958963)。用redis做缓存时,缓存需要保证不发生缓存雪崩,即同一时刻大量并发因为在缓存找不到数据而跑去后端请求,就会照成缓存雪崩。如果redis需要扩容的时候,只是对需要存储的对象进行hash计算,再模上计算机数量就能决定缓存在哪台计算机上。但如果增加一台机器进行扩容,模上的计算机数量就会改变使得大量的缓存失效。而采用一致性hash就能避免这个问题,在一个hash环上,有232个节点,将计算机进行hash运算再模上232就能获得该计算机在hash环上对应的点,用同样的方法对要存储的数据进行计算也能得到一个节点,数据会存储在自己那个节点顺时针方向上遇到的第一台计算机节点上,当增加一台计算机时,就只会导致部分数据失效。这样又出现了hash偏斜的问题,即计算机节点出现在环上较为相近的地方,可以采用虚拟节点解决这个问题,计算机的虚拟节点越多偏斜的概率就会越低。

过期键的删除策略

Redis的过期键的删除策略

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

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
    (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

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

Redis key的过期时间和永久有效分别怎么设置?

EXPIRE和PERSIST命令。expire key 100 (设置该key 100秒过期) persist key(移除该key的过期时间,使得key永不过期)

我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:

  1. 定时去清理过期的缓存;
  2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

内存相关

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

Redis的内存淘汰策略有哪些

redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。

(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
(3)volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
(4)volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰。
(5)allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
(6)allkeys-lfu:从数据集中挑选使用频率最低的数据淘汰。
(7)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
(8) no-enviction(驱逐):禁止驱逐数据,这也是默认策略。意思是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失。

Redis主要消耗什么物理资源?

内存。

Redis的内存用完了会发生什么?

如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。

Redis如何做内存优化?

可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面

线程模型

Redis线程模型

Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。

事务

什么是事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

  • multi:入队
  • exec:执行事务
  • discard:取消事务
  • watch: 当watch 某一个key时,并开启一段事务修改此key,若在事务执行过程中有另一个进程修改了改key,则此次事务执行失败,无法执行。(乐观锁)
  • unwatch:取消监视

以multi开始,之后的命令将会被加入队列,若在加入队列的过程中,有某一条命令编译错误,当执行exec命令时就会提示事务提交失败,全部回滚。若在编译阶段没有错误,即入队的时候都成功,但在提交事务exec执行的的时候发生错误则不会回滚,该成功的成功,该失败的失败。

乐观锁:(CAS):用watch关键字实现,可以解决秒杀中的超卖问题(会照成库存遗留问题)

redis事务特性

  • 单独的隔离操作:事务中的所有命令都会序列化,按顺序地执行。事务在执行过程中,不会被其它客户端发送来的命令请求所打断
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际地被执行,因为事务提交前任何指令都不会被执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到“这个让人万分头疼的问题
  • 不保证原子性:redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

事务管理(ACID)概述

  • 原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

  • 一致性(Consistency)

事务前后数据的完整性必须保持一致。

  • 隔离性(Isolation)
    • 读未提交
    • 读已提交
    • 可重复度
    • 串行化

多个事务并发执行时,一个事务的执行不应影响其他事务的执行

  • 持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响

Redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在_AOF_持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。

Redis事务支持隔离性吗

Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的

Redis事务保证原子性吗,支持回滚吗

Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

Redis事务其他实现

  • 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,
    其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完
  • 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

集群方案

哨兵模式

Redis 常见面试题(2020最新版)

哨兵的介绍

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

  • 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

哨兵的核心知识

  • 哨兵至少需要 3 个实例,来保证自己的健壮性。
  • 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
  • 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

官方Redis Cluster 方案(服务端路由查询)

Redis 常见面试题(2020最新版)

redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?

简介

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使用一致性hash,而是采用slot(槽)的概念,一共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行

方案说明

  1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
  2. 每份数据分片会存储在多个互为主从的多节点上
  3. 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
  4. 同一分片多个节点间的数据不保持一致性
  5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
  6. 扩容时时需要需要把旧节点的数据迁移一部分到新节点

在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

节点间的内部通信机制

基本通信原理

集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。

分布式寻址算法

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • redis cluster 的 hash slot 算法

优点

  • 无中心架构,支持动态扩容,对业务透明
  • 具备Sentinel的监控和自动Failover(故障转移)能力
  • 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • 高性能,客户端直连redis服务,免去了proxy代理的损耗

缺点

  • 运维也很复杂,数据迁移需要人工干预
  • 只能使用0号数据库
  • 不支持批量操作(pipeline管道操作)
  • 分布式逻辑和存储模块耦合等

基于客户端分配

Redis 常见面试题(2020最新版)

简介

Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据的key进行散列,通过hash函数,特定的key会映射到特定的Redis节点上。Java redis客户端驱动jedis,支持Redis Sharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPool

优点

  • 优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强

缺点

  • 由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。
  • 客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化

基于代理服务器分片

Redis 常见面试题(2020最新版)

简介

客户端发送请求到一个代理组件,代理解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端

特征

  • 透明接入,业务程序不用关心后端Redis实例,切换成本低
  • Proxy 的逻辑和存储的逻辑是隔离的
  • 代理层多了一次转发,性能有所损耗

业界开源方案

  • Twtter开源的Twemproxy
  • 豌豆荚开源的Codis

生产环境中的 redis 是怎么部署的?

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。

机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。

5 台机器对外提供读写,一共有 50g 内存。

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

说说Redis哈希槽的概念?

Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

Redis集群会有写操作丢失吗?为什么?

Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

Redis集群之间是如何复制的?

异步复制

Redis集群最大节点个数是多少?

16384个

Redis集群如何选择数据库?

Redis集群目前无法做数据库选择,默认在0数据库。

分区

Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

为什么要做Redis分区?

分区可以让Redis管理更大的内存,Redis将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使Redis的计算能力通过简单地增加计算机得到成倍提升,Redis的网络带宽也会随着计算机和网卡的增加而成倍增长。

你知道有哪些Redis分区实现方案?

  • 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
  • 代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy
  • 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

Redis分区有什么缺点?

  • 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
  • 同时操作多个key,则不能使用Redis事务.
  • 分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set)
  • 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
  • 分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。

分布式问题

Redis实现分布式锁

https://www.bilibili.com/video/BV1d4411y79Y?p=5&spm_id_from=pageDriver

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。

当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

返回值:设置成功,返回 1 。设置失败,返回 0 。

Redis 常见面试题(2020最新版)

使用SETNX完成同步锁的流程及事项如下:

使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功

为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间

释放锁,使用DEL命令将锁数据删除

如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!

推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

参考:https://www.jianshu.com/p/8bddd381de06

分布式Redis是前期做还是后期规模上来了再做好?为什么?

既然Redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让Redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。

一开始就多设置几个Redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。

这样的话,当你的数据不断增长,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

什么是 RedLock

Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:

  1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务

缓存异常

缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
  4. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
  5. 设置热点数据永远不过期。

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案

  1. 设置热点数据永远不过期。
  2. 加互斥锁,互斥锁

缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

解决方案

  1. 直接写个缓存刷新页面,上线时手工操作一下;
  2. 数据量不大,可以在项目启动的时候自动进行加载;
  3. 定时刷新缓存;

缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:

  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据

热点数据,缓存才有价值

对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存

对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。

数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

缓存热点key(缓存击穿)

缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询

Spring

Spring是什么?

Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。主要包括以下七个模块:

  • Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等);
  • Spring Core:核心类库,所有功能都依赖于该类库,提供IOC和DI服务;
  • Spring AOP:AOP服务;
  • Spring Web:提供了基本的面向Web的综合特性,提供对常见框架如Struts2的支持,Spring能够管理这些框架,将Spring的资源注入给框架,也能在这些框架的前后插入拦截器;
  • Spring MVC:提供面向Web应用的Model-View-Controller,即MVC实现。
  • Spring DAO:对JDBC的抽象封装,简化了数据访问异常的处理,并能统一管理JDBC事务;
  • Spring ORM:对现有的ORM框架的支持;

Spring 的优点?

(1)spring属于低侵入式设计,代码的污染极低;

(2)spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;

(3)spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。

(4)spring对于主流的应用框架提供了集成支持。

Spring的IoC理解:

(1)IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。

(2)最直观的表达就是,以前创建对象的主动权和时机都是由自己把控的,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。

(3)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。

Spring的AOP理解:

OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。

AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。

AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。

(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

BeanFactory跟ApplicatioinContext的区别?

ApplicatioinContext是BeanFactory的子接口

  • ApplicatioinContext饿汉式加载Bean,启动慢占内存
  • BeanFactory懒汉式加载Bean,

Bean生命周期

未解决

Bean作用域

  1. singleton:单例
  2. prototype:原型,一次getBean()创建一个Bean
  3. request:一次http请求共享一个Bean
  4. session:一次会话共享一个Bean,一次会话可能包含对此request
  5. globalSession:一般用于Portlet应用环境,该作用域仅适用于WebApplicationContext环境

单例Bean线程安全吗?

不安全,spring没有对bean进行多线程的封装处理

要安全的话,可以改为原型模式,或者使用Threadlocal

Spring的事务管理器是通过线程相关的ThreadLocal来保存数据访问基础设施(也即Connection实例)

Spring中的设计模式

Spring事务:

  1. 编程式事务,通过编程的方式实现事务的begin,roolback或commit
  2. 声明式事务,就是加上了@Transactional注解,spring会为该类创建代理对象做一些事情,若调用到带有@Transactional注解方法的时候就会先将自动提交设为false,然后执行完无异常就提交,有出现运行时异常就会回滚(如空指针异常,除0异常呀)

Spring事务传播机制

A–>B 以下属性仅加在B的@Transactional上

required:A无,B自建,A有,B加入A

supports:A有,B加入A,A无则B也无

mandatory:A有,B加入A,A无抛异常

required-new:A有,A被挂起,B自建,A,B互不影响,B回滚A可能不回滚

not_supported:B以非事务运行

never:B以非事务方式运行,若存在事务抛异常

nested:A没,B开,A有,A为父事务,B为子事务

spring什么时候会事务失效?

首先spring的事务是基于aop实现的,通过生成代理对象进行增强操作。

  1. 发生自调用的时候,类里面用this调用本类的方法,此时这个this不是指向代理类,所以事务会失效
  2. 方法不是public
  3. 数据库本身不支持事务,spring的事务依托于数据库
  4. 没有被spring管理,spring未扫描到这个类使其未被加入ioc容器
  5. 异常被捕获,申明式事务会在抛出运行时异常时进行回滚操作

bean自动装配的方式有哪些

开启自动装配,需要在xml文件中定义“autowire”属性

  1. no:默认,手动配置。通过ref属性注入
  2. byName:找到IOC中id为“xxx”的自动注入,通过setter注入
  3. byType:找到IOC中类型为“xxx”的自动注入,通过setter注入
  4. constructor:类似byType,但是通过构造器注入
  5. autodetect:有默认构造器时用constructor注入,没有则用byType

SpringMVC

SpringMVC的工作流程

  1. 用户发送请求到DispatcherServlet
  2. DispatcherServlets收到请求后将请求传给处理器映射器HandlerMapping
  3. 处理器映射器找到具体的处理器(可以根据注解或xml),生成处理器及处理器拦截器(如果有),一并返回给DispatcherServlet
  4. DispatcherServlet调用处理器适配器HandlerAdapter
  5. HandlerAdapter经过适配调用具体的处理器(Controller)
  6. Controller执行完返回ModelAndView(mv)
  7. HandlerAdapter将该mv返回给DispatcherServlet
  8. DispatcherServlet将mv传给ViewReslover视图解析器
  9. 视图解析器解析后返回具体View
  10. DispatcherServlet根据View进行视图渲染
  11. DispatcherServlet将处理完的结果返回给用户

SpringBoot

springboot自动装配原理

去看雷神源码

JAVA八股文_第1张图片

如何理解Spring的starter

使用spring+springmvc构建项目时,如果需要引入mybatis等框架,需要到xml中定义mybatis的bean

starter就是定义了一个starter的jar包,写了一个@Configuration配置类,将这些bean定义在里面,然后再starter包的META-INF/spring-factories中写入给配置类的全路径,springboot会按照约定来加载该配置类,完成自动配置

计网面试

  1. 请详细介绍下TCP的三次握手机制,为什么要三次握手?
    1.1 为什么要有握手?
    1.2 为什么是三次?
  2. 简单介绍下HTTP协议中缓存的处理流程?
    2.1 缓存的应用流程是什么?
    2.2 与缓存相关的HTTP头部有哪些?
  3. 在地址栏键入URL后,网络世界发生么什么?
  4. 使用HTTP长连接有哪些优点?
    1. 减少握手次数
    2. 减少慢启动的影响(因为有拥塞控制在里面,如果短连接,则每次都是从慢到快再到慢再到平衡这很费时间)
    3. 缺点:tcp协议叫做字符流协议
  5. CLOSE_WAIT状态产生的原因?
  6. 介绍下多播是怎样实现的?
  7. 服务器的最大并发连接数是多少?
  8. TCP和UDP协议该如何选择?
  9. TLS/SSL协议是如何保障信息安全的?
  10. HTTP2协议有哪些优点?

你可能感兴趣的:(java)