【面试总结篇】

集合、IO、多线程

1.Sync的底层实现原理

一、synchronized作用在代码块时,它的底层是通过monitorenter、monitorexit指令来实现的。

  • monitorenter:

    每个对象都是一个监视器锁(monitor),当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

    如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

  • monitorexit:

    执行monitorexit的线程必须是object所对应的monitor持有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

    monitorexit指令出现了两次,第1次为同步正常退出释放锁,第2次为发生异步退出释放锁。

二、方法的同步并没有通过 monitorenter 和 monitorexit 指令来完成,不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

三、总结

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

2.ReetrantLock的底层实现原理

ReentrantLock是可重入的,可中断的,即可做公平,也可以做非公平的悲观锁。

它呢基于AQS实现的,AQS即AbstractQueuedSynchronizer的缩写,这个是个内部实现了两个队列的抽象类,分别是同步队列和条件队列。其中同步队列是一个双向链表,里面储存的是处于等待状态的线程,正在排队等待唤醒去获取锁,而条件队列是一个单向链表,里面储存的也是处于等待状态的线程,只不过这些线程唤醒的结果是加入到了同步队列的队尾,AQS所做的就是管理这两个队列里面线程之间的等待状态-唤醒的工作。

在同步队列中,还存在2中模式,分别是独占模式和共享模式,这两种模式的区别就在于AQS在唤醒线程节点的时候是不是传递唤醒,这两种模式分别对应独占锁和共享锁。

AQS是一个抽象类,所以不能直接实例化,当我们需要实现一个自定义锁的时候可以去继承AQS然后重写获取锁的方式和释放锁的方式还有管理state,而ReentrantLock就是通过重写了AQS的tryAcquire和tryRelease方法实现的lock和unlock。

这份ReentrantLock 实现了 Lock 接口,然后里面有3个内部类,其中Sync内部类继承自AQS,另外的两个内部类farilSync和NoFairYSync都继承自Sync,这两个类分别是用来做公平锁和非公平锁的。加锁和释放锁操作,主要是通过内部的一个volatile修改的int类型的 state成员,如果state大于等于1,说明锁资源以及被持有,如果为0,说明锁资源没有被持有,

当需要获取锁的时候呢,执行lock方法,首先呢会去获取锁状态state,如果这个state为0,那么直接基于CAS方式将state改为1,修改成功就表示获取到锁执行,然后线程继续执行任务,如果state为1,那么就进入到阻塞队列排队等待。进入之前呢公平锁,会去判断一下这个队列是否为空,如果为空会尝试去竞争一波锁资源,如果队列不空呢,会直接去排队。非公平锁呢,不管队列是否为空,都会先去尝试竞争一波,竞争不到呢就去排队

通过Sync重写的方法tryAcquire、tryRelease可以知道,ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁。

3.lock和Sync的区别

下面我从 4 个方面来回答

  1. 从功能角度来看,Lock 和 Synchronized 都是 Java 中用来解决线程安全问题的工具。

  2. 从特性来看,Synchronized 是 Java 中的同步关键字,Lock 是 J.U.C 包中提供的接口,这个接口有很多实现类,其中就包括 ReentrantLock

  3. a、Synchronized 可以通过两种方式来控制锁的粒度

    一种是把 synchronized 关键字修饰在方法层面,

    另一种是修饰在代码块上,并且我们可以通过 Synchronized 加锁对象的生命周期来控制锁的作用范围,比如锁对象是静态方法或者类对象,那么这个锁就是全局锁。如果锁对象是普通实例对象或者普通方法,那这个锁的范围取决于这个实例的生命周期。

    Lock 锁的粒度是通过它里面提供的 lock()和 unlock()方法决定的,包裹在这两个方法之间的代码能够保证线程安全性。而锁的作用域取决于 Lock 实例的生命周期。

    b、Lock 比 Synchronized 的灵活性更高,Lock 可以自主决定什么时候加锁,什么时候释放锁,只需要调用 lock()和 unlock()这两个方法就行,同时 Lock 还提供了非阻塞的竞争锁方法、tryLock()方法,这个方法通过返回 true/false 来告诉当前线程是否已经有其他线程正在使用锁。Synchronized 由于是关键字,所以它无法实现非阻塞竞争锁的方法,另外,Synchronized 锁的释放是被动的,就是当 Synchronized 同步代码块执行完以后或者代码出现异常时才会释放。

    d. Lock 提供了公平锁和非公平锁的机制,公平锁是指线程竞争锁资源时,如果已经有其他线程正在排队等待锁释放,那么当前竞争锁资源的线程无法插队。而非公平锁,就是不管是否有线程在排队等待锁,它都会尝试去竞争一次锁。 Synchronized 只提供了一种非公平锁的实现。

    synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并可以细分读写锁以提高效率。

4.Java中的锁升级过程

4、锁升级

Java SE1.6为减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高 获得锁和释放锁的效率。

4.1、偏向锁:

4.1.1、为什么要引入偏向锁?

因为经过HotSpot的作者大量研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。

4.1.2、偏向锁的升级

当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,。

4.1.3、偏向锁的取消

偏向锁是默认开启的,而且开始时间一般是比应用程序启动慢几秒,如果不想有这个延迟,那么可以使用-XX:BiasedLockingStartUpDelay=0;如果不想要偏向锁,那么可以通过-XX:-UseBiasedLocking = false来设置;

4.2、轻量级锁

4.2.1、为什么要引入轻量级锁?

轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。

4.2.2、轻量级锁什么时候升级为重量级锁?

线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;

如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者20次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

4.2.3、自旋

现在假设有这么一个场景:有两个线程A,B在竞争一个锁,假设A拿到了,这个时候B被挂起阻塞,一直等待A释放了锁B才得到使用权。在操作系统中阻塞和唤醒是一个耗时操作,如果A在很短的时间内就释放了锁,当这个时间与阻塞唤醒比较起来更短的时候,我们将B挂起,其实不是一个最优的选择。 ​ 自旋是指某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁。虽然CPU的时间被消耗了,但是比线程下文切换时间要少。这个时候使用自旋是划算的。 ​ 如果是单核处理器,一般建议不要使用自旋锁。因为只有单个处理器,自旋占用的时间片使得代价很高。 ​ 而偏向锁、轻量锁、重量锁也是一个锁围绕着如何使得程序运行的更加“划算”而进行改变的。

*注意:*

为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。

一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态。

5.ThreadLocal实现原理,以及内存泄漏原因

好的,这个问题我从三个方面来回答。

  1. ThreadLocal 是一种线程隔离机制,它提供了多线程环境下对于共享变量访问的安全性。

  2. 在多线程访问共享变量的场景中,一般的解决办法是对共享变量加锁,从而保证在同一时刻只有一个线程能够对共享变量进行更新,并且基于Happens-Before 规则里面的监视器锁规则,又保证了数据修改后对其他线程的可见性。

  3. 但是加锁会带来性能的下降,所以 ThreadLocal 用了一种空间换时间的设计思想,也就是说在每个线程里面,都有一个容器来存储共享变量的副本,然后每个线程只对自己的变量副本来做更新操作,这样既解决了线程安全问题,又避免了多线程竞争加锁的开销。

ThreadLocal 的具体实现原理是,在 Thread 类里面有一个成员变量 ThreadLocalMap,它专门来存储当前线程的共享变量副本,后续这个线程对于共享变量的操作,都是从这个 ThreadLocalMap里面进行变更,不会影响全局共享变量的值。

ThreadLocal 使用场景比较多,比如在数据库连接的隔离、对于客户端请求会话的隔离等等。

我认为,不恰当地使用 ThreadLocal,会造成内存泄漏问题。

主要原因是,线程的私有变量 ThreadLocalMap 里面的 key 是一个弱引用。

弱引用的特性,就是不管是否存在直接引用关系,

当成员 ThreadLocal 没用其他的强引用关系的时候,这个对象会被 GC 回收掉。

从而导致 key 可能变成 null,造成这块内存永远无法访问,出现内存泄漏的问题。

规避内存泄漏的方法有两个:

 通过扩大成员变量 ThreadLoca 的作用域,避免被 GC 回收

 每次使用完 ThreadLocal 以后,调用 remove 方法移除也会导致这个内存一直占用不释放,最后造成内存溢出的问题。

所以我认为最好是在使用完以后调用 remove 方法移除对应的数据

第一种方法虽然不会造成 key 为 null 的现象,但是如果后续线程不再继续访问这个 key,

也会导致这个内存一直占用不释放,最后造成内存溢出的问题。

所以我认为最好是在使用完以后调用 remove 方法移除

以上就是我的理解。

6.CAS实现的原理

CAS的原理主要是通过三个参数,当前内存的变量值V、旧的预期值A、即将更新的值B。通过判断是V和A是否相等查看当前变量值是否被其他线程改变,如果相等则变量没有被其他线程更改,就把B值赋予V;如果不相等则做自旋操作。

7.volatile的实现原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  2. 它会强制将对缓存的修改操作立即写入主存;

  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

JVM

1.类加载过程

加载通过类的完全限定名,查找此类字节码文件,利用字节码文件创建 Class 对象.

验证确保 Class 文件符合当前虚拟机的要求,不会危害到虚拟机自身安全.

准备进行内存分配,为 static 修饰的类变量分配内存,并设置初始值(0 或 null).不包含final 修饰的静态变 量,因为final 变量在编译时分配.

解析将常量池中的符号引用替换为直接引用的过程.直接引用为直接指向目标的指针或者相对偏移量 等

初始化主要完成静态块执行以及静态变量的赋值.先初始化父类,再初始化当前类.只有对类主动使用时才会初始化.

2.双亲委派机制,打破情景

完整版:

我们自己写的 java 源文件到最终要运行,必须经过编译和类加载两个阶段。编译的过程就是把.java 文件编译成.class 文件。类加载的过程,就是把 class 文件装载到 JVM 内存中,装载完成以后就会得到一个 Class 对象,我们就可以使用 new 关键字来实例化这个对象。而类的加载过程,需要涉及类加载器。JVM 在运行的时候,会产生 3 个类加载器,这三个类加载器组成了一个层级关系每个类加载器分别去加载不同作用范围的 jar 包,比如

1.Bootstrap ClassLoader,主要是负责 Java 核心类库的加载,也就是 %{JDK_HOME}\lib 下的

rt.jar、resources.jar 等

2.Extension ClassLoader,主要负责%{JDK_HOME}\lib\ext 目录下的 jar 包和 class 文件

3.Application ClassLoader,主要负责当前应用里面的 classpath 下的所有 jar 包和类文件

除了系统自己提供的类加载器以外,还可以通过 ClassLoader 类实现自定义加载器,去满足一些特殊场景的需求。所谓的父委托模型,就是按照类加载器的层级关系,逐层进行委派。比如当需要加载一个 class 文件的时候,首先会把这个 class 的查询和加载委派给父加载器去执行,如果父加载器都无法加载,再尝试自己来加载这个 class。

简化版:

双亲委派模式,即加载器加载类时先把请求委托给自己的父类加载器执行,直到顶层的启动类加载器. 父类加载器能够完成加载则成功返回,不能则子类加载器才自己尝试加载。

这种机制的好处就是: 1.避免类的重复加载 2.避免 Java 的核心 API 被篡改。

双亲委派的目标是在默认情况下,一个限定名的类只会被一个类加载器加载并解析使用,这样在程序中,它就是唯一的,不会产生歧义

在被动的情况下,当一个类加载器收到类加载请求,它不会首先自己去加载。而是传递给父加载器。这样,所有的类首先都会先由最上层的启动类加载器进行加载,只有父加载器无法完成类加载才会由子加载器完成。

打破场景:

第一次破坏:

在《深入理解Java虚拟机》这本书中,记录了怎样破坏双亲委派:因为双亲委派机制原理在java.lang.ClassLoader的loadClass方法中。只要重写loadClass方法就可以破坏。书中还写了一个重写loadClass方法来进行破坏的小样例,这个小样例被称为双亲委派的第一次破坏。

Tomcat场景

tomcat自定义了类加载器,重写loadClass方法使其优先加载自己目录下的class文件,来达到class私有、Tomcat就使用这种方式对双亲委派进行破坏、使用一个web容器部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,还要能保证每一个应用程序的类库都是独立、相互隔离的效果的效果。不过咱们现在流行使用的都是嵌入式的web容器了,将来更多的场景还是一个应用程序使用一个单独的web容器。所以这种破坏双亲委派的价值在降低。

基于SPI的三种经典破坏场景

在《JAVA SPI(Service Provider Interface), 使用SPI的经典三个场景

dbc、Dubbo、Eleasticsearch、通过SPI都使得双亲委派遭到破坏。由启动类加载器加载的DriverManager,初始化时要去加载Driver驱动。jdbc驱动由Serverloader.load加载。而Serverloader.load里优先使用当前线程的类加载器而不是自身使用的类加载器来加载Driver。当前线程是使用方要么是应用类加载器要么是自定义类加载器,总归类A调用类B,B没有使用父类而使用了子类加载器,所以破坏了双亲委派。

3.内存占用很高怎么排查
  1. 给 JVM 虚拟机分配的内存太小,实际业务需求对内存的消耗比较多;

    2.代码漏洞: Java 应用里面存在内存泄漏的问题,或者应用中有大量占用内存的对象,并且没办法及时释放。

通过top指令获取进程id ,然后呢通过jmap相关命令查看内存分配大小、是否有明显的对象分配过多且没有释放情况。

  1. jmap -heap 查看是否内存分配过小

  2. jmap -histo 查看是否有明显的对象分配过多且没有释放情况

  3. jmap -dump 导出 JVM 当前内存快照,使用 JDK 自带或 MAT 等工具分析快照

4.cpu占用很高怎么排查

CPU 飙高的原因有几个方面

1.CPU 上下文切换过多,对于 CPU 来说,同一时刻下每个 CPU 核心只能运行一个线程,如果有多个线程要执行,CPU 只能通过上下文切换的方式来执行不同的线程。

2.CPU 资源过度消耗,也就是在程序中创建了大量的线程,或者有线程一直占用 CPU 资源无法被释放,比如死循环!

针对这些情况呢,我们可以通过 top 命令,找到 CPU 利用率较高的进程,再通过 Shift+H 找到进程中 CPU 消耗过高的线程,这里有两种情况。

1. CPU 利用率过高的线程一直是同一个,说明程序中存在线程长期占用 CPU 没有释放的情况,这种情况直接通过 jstack 获得线程的 Dump 日志,定位到线程日志后就可以找到问题的代码。 2.CPU 利用率过高的线程 id 不断变化,说明线程创建过多,需要挑选几个线程 id,通过 jstack去线程 dump 日志中排查。

5.如果排查线上OOM

1.OOM,全程“out of memory”,表示内存耗尽,当JVM因为没有足够空间来为对象分析空间,并且垃圾回收器也没有空间可用回收时,就会抛这个错误。

出现OOM错误呢,一般时有两种原因:

1. 给 JVM 虚拟机分配的内存太小,实际业务需求对内存的消耗比较多;

2.代码漏洞: Java 应用里面存在内存泄漏的问题,或者应用中有大量占用内存的对象,并且没办法及时释放。

常见的 OOM 异常情况有两种:

1.java.lang.OutOfMemoryError: Java heap space ------>java 堆内存溢出,此种情况最常见,一般由于内存泄漏或者堆的大小设置不当引起。对于内存泄漏,需要通过内存监控软件查找程序中的泄露代码,而堆大小可以通过虚拟机参数-Xms,-Xmx 来修改。

2.java.lang.OutOfMemoryError: PermGen space 或 java.lang.OutOfMemoryError:

MetaSpace ------>java 方法区,溢出了,一般出现在大量 Class,或者采用 cglib 等反射机制的情况,因为这些情况会产生大量的 Class 信息存储于方法区。这种情况可以通过更改方法区的大小来解决,使用类似-XX:PermSize=64m-XX:MaxPermSize=256m 的形式修改。

遇到这类问题,通常的排查方式是,先获取内存的 Dump 文件。Dump 文件有两种方式来生成,第一种是配置 JVM 启动参数,当触发了 OOM 异常的时候自动生成, 第二种是使用 jmap 工具来生成,然后使用 MAT 工具来分析 Dump 文件。

同时呢呢也可以通过top指令获取进程id ,然后呢通过jmap相关命令查看内存分配大小、是否有明显的对象分配过多且没有释放情况。

  1. jmap -heap 查看是否内存分配过小

  2. jmap -histo 查看是否有明显的对象分配过多且没有释放情况

  3. jmap -dump 导出 JVM 当前内存快照,使用 JDK 自带或 MAT 等工具分析快照

6.调优参数设置

4c8G内存的大小JVM调优

1.堆内存:

首先是堆内存大小的设置,当我们的机器只有4c8g的时候,堆内存的大小的肯定不能太大,一般不建议设置的太大,因为我们需要给机器上的其他应用预留一部分内存。所以一般建议都是把JVM的堆内存设置成操作系统内存的一般半,也就是4g。至于初始化内存和最大内存,我们在场景中设置成一样的。这样可以避免JVM在运行过程中频繁进行内容扩容和收缩操作,提高应用程序的性能和稳定性。

-Xms4G -Xmx4G

2.垃圾收集器选择

在设置了堆空间总大小之后,我们需要考虑哪种垃圾收集器。另外,我们需要去分析下这个业务中是否会频繁在新生代创建并销毁对象,那么意味着新生代的GC会比较频繁。所以我们需要选择一种在GC过程中STW时间短的,并且在年轻代的回收过程中也能发挥效果的。在新生代的垃圾收集器中,主要以Serial、 ParNew、Parallel Scavenge以及支持整个堆回收的G1了。

因为新生代采用的是标记复制算法,所以不太考虑碎片的问题,Parallel Scavenge 更加关注吞吐量,而G1作为JDK19中默认的垃圾收集器,他不仅同时具有低暂停时间和高吞吐量的优点,但是他对内存有要求,最小要4G,从使用门槛上来说,G1是可以用的,因为一般来说,内存要大于等于4G的话,才适合使用G1进行GC;

-XX:+UseG1GC

在使用了G1之后,其实他自己有一套自动的预测和调优机制的。我们只需要通过参数来设置最大停顿时间进行了。一般建议设置到100-200之间,一般这个时长对用户来说基本无感知:

-XX:MaxGCPauseMillis = 200;

其次,我们还可以自己调节一些G1的配置,比如设置他的GC线程数,可以先配置4个线程数进行GC,后续根据实际情况再做调整:

-XX:ParallelGCThreads = 4 //设置并行GC线程数为4
​
-XX:ConcGCThreads=2 //设置并发GC线程数为2

各区大小设置得

G1的内存划分是自适应的,它会根据堆的大小和使用情况来动态调整各个区域的大小和比例。但是,我们也可以通过一些JVM参数来手动设置G1的各个分代内存配置。

G1 中的分代和其他垃圾回收器不太一样,它不是严格按照年轻代和老年代划分的,而是通过划分各个区域的存活对象数量来实现垃圾回收的。因此,G1 中不需要像其他垃圾回收器那样设置新生代和老年代的大小比例,而是需要设置一些区域的内存配置。

-XX:G1NewSizePercent和 -XX:G1MaxNewSizePercent分别用于设置年轻代的初始大小和最大大小,它们的默认值分别为 5% 和 60%。针对我们的业务场景,我们其实可以适当的调高一下年轻代的初始大小,5%的比例太小了,我们可以调整到30%。

-XX:G1HeapRegionSize=2m:将 G1 的区域大小设置为 2MB,以提高垃圾回收的效率和精度。
 
-XX:G1NewSizePercent=20:设置年轻代的初始大小为堆的 20%。
 
-XX:G1MaxNewSizePercent=50:设置年轻代的最大大小为堆的 50%。
 
-XX:G1OldCSetRegionThresholdPercent=10:设置老年代的大小为堆的 10%。
 
-XX:G1HeapWastePercent=5:设置垃圾回收后留下的未使用区域的最大比例为 5%。

4、添加必要的日志

因为以上配置都是根据业务大致分析出来的初始配置,所以我们一定是需要不断地调优的,那么必要的日志相关参数就要添加。如:

-XX:MaxGCPauseMillis=100:最大 GC 暂停时间为 100 毫秒,可以根据实际情况调整;
 
-XX:+HeapDumpOnOutOfMemoryError:当出现内存溢出时,自动生成堆内存快照文件;
 
-XX:HeapDumpPath=/path/to/heap/dump/file.hprof:堆内存快照文件的存储路径;
 
-XX:+PrintGC:输出 GC 信息;
 
-XX:+PrintGCDateStamps:输出 GC 发生时间;
 
-XX:+PrintGCTimeStamps:输出 GC 发生时 JVM 的运行时间;
 
-XX:+PrintGCDetails:输出 GC 的详细信息;

####

MYSQL

1.索引为什么B+树用B树

b树:

多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键字范围的子结点;所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;

b+树:

在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;

好的,首先呢b树和b+树都能作为mysql索引的存储类型。

b树呢是一种多路平衡树,用这种结构来存储大量数据的时候呢,它的整棵树的高度会比二叉树矮很多。而对于数据库来说,我们的所有数据都是存储在磁盘上的,而磁盘io的效率实际上是很低的,特别是在随机磁盘io的时候更低。所以存储数据的树高度能够决定磁盘IO次数,树越矮,磁盘IO越少,效率越高,对性能提升也就越大,这就是使用b树作为索引结构的原因。

但是在我们mysql Innodb的存储引擎里面,它用到了一种增强的b树结构:b+树;

我认为呢主要有两个方面的优化:

叶子节点存储数据,非叶子节点只存储索引

叶子节点的数据使用双向链表的方式进行关联

针对这两点优化我认为,用b+树来做索引,我认为有几个方面的原因:1等高能够存储的数据量更多,磁盘Io更少 2.范围查询是一个常用的操作, 3.数据存储在叶子节点,b+树的io次数更加稳定 4.因为叶子节点存储了所有数据,b+树的全局扫描能力更强

2.介绍一下事务

事务一般是指要做的或所做的事情

而在我们计算机术语中事务呢是对数据库的一系列操作,可由一条sql组成,也可以由多条复杂的,事务需遵循ACID四个特性:

A:原子性,事务是一个不可分割的工作单元,一个事务中的sql操作要么都执行成功,要么都执行失败

C:一致性;数据库从一种状态转化为另外一种一致性状态。事务开始之前和结束后,数据库的完整约束性没有被破坏

i:隔离性,事务与事务之间是互不干扰

D:持久性,事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

根据事务类型的不同呢:可以分为这个扁平事务、带有保存点的扁平事务、链事务、嵌套事务嵌套事务、分布式事务;

扁平事务:beginwork开始 由commitwork或者rollback work结束,要么都执行,要么都回滚

带有保持点的扁平事务:允许在事务执行过程中回滚到同一事务中较早的一个状态,这是因为可能某些事务在执行过程中出现的错误并不会对所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点(savepoint)用来通知系统应该记住事务当前的状态,以便以后发生错误时,事务能回到该状态。

链事务:

嵌套事务:

分布式事务:

通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。对于分布式事务,同样需要满足ACID特性,要么都发生,要么都失效。

3.ACID特性如何实现

事务呢是对数据库的一系列操作,可由一条sql组成,也可以由多条复杂的,事务需遵循ACID四个特性:

1.原子性:

实现原子性的关键呢,就是当事务回滚时能够撤销所有成功执行的sql语句。Innodb主要是靠这个undolog,当事务对数据库进行修改的时候呢,InnoDB会生成对应的undolog。如果事务执行失败或调用rollback,导致事务回滚,便可以利用undo log中的信息将数据进行回滚,undo log逻辑日志,记录sql相关执行的信息。需要回滚时,会根据undolog 的内容做与之相反的工作。对于insert,回滚时会执行delete。对于delete,回滚时会执行insert。对于update,回滚时,执行相反的update。

2.持久性实现原理

主要借助这个redo log日志。InnoDB执行引擎呢提供了缓存,来减少磁盘IO,提高我们每次读写数据的效率。当数据读取数据时。当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool。当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏);如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。于是,redo log被引入来解决这个问题。当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作。当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。

既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

  • 刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

  • 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入。而redo log中只包含真正需要写入的部分,无效IO大大减少。

3.隔离性实现原理 隔离性追求的是并发情形下事务之间互不干扰隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面。 第一方面,(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性。 第二方面,(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性。

MVCC隔离机制:

读不加锁,因此读写不冲突,并发性能好,主要是基于隐藏列、基于undo log的版本链、readView

4.一致性实现原理

可以说,一致性是事务追求的最终目标。前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。实现一致性的措施包括:

  • 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证。

  • 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等。

  • 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致。

4.如何解决脏读、不可重复读、幻读问题

脏读:

有两个事务 T1/T2 同时在执行,T1 事务有可能会读取到 T2 事务未提交的数据,但是未提交的事

务 T2 可能会回滚,也就导致了 T1 事务读取到最终不一定存在的数据产生脏读的现象。

不可重复读:设有两个事务 T1/T2 同时执行,事务 T1 在不同的时刻读取同一行数据的时候结果可能不一样,从而导致不可重复读数问题。

幻读:

有两个事务 T1/T2 同时执行,事务 T1 执行范围查询或者范围修改的过程中,事务 T2 插入了一条

属于事务 T1 范围内的数据并且提交了,这时候在事务 T1 查询发现多出来了一条数据,或者在 T1 事务发现这条数据没有被修改,看起来像是产生了幻觉,这种现象称为幻读。

解决不可重复读问题:MySQL的InnoDB引擎,在默认的REPEATABLE READ的隔离级别下,实现了可重复读,同时也解决了幻读问题。它使用Next-Key Lock算法实现了行锁,并且不允许读取已提交的数据,所以解决了不可重复读的问题。另外,该算法包含了间隙锁,会锁定一个范围,因此也解决了幻读的问题。

5.MVCC机制

RR 隔离级别实现原理,就是 MVCC 多版本并发控制,而 MVCC 是通过 ReadView+ Undo Log 实

现的,Undo Log 保存了历史快照,Read View 可见性规则帮助判断当前版本的数据是否可见。

Undo Log 版本链长这样:Read view 的几个重要属性

 m_ids :当前系统中那些活跃(未提交)的读写事务 ID, 它数据结构为一个 List。

 min_limit_id :表示在生成 Read View 时,当前系统中活跃的读写事务中最小的事务 id,即

m_ids 中的最小值。

 max_limit_id :表示生成 Read View 时,系统中应该分配给下一个事务的 id 值。

 creator_trx_id : 创建当前 Read View 的事务 ID

Read view 可见性规则如下:

  1. 如果数据事务 ID trx_id < min_limit_id ,表明生成该版本的事务在生成 ReadView 前,已经提交(因为事务 ID 的递增的),所以该版本可以被当前事务访问。

  2. 如果 trx_id>= max_limit_id ,表明生成该版本的事务在生成 Read View 后才生成,所以该版本不可以被当前事务访问。

  3. 如果 min_limit_id =<trx_id< max_limit_id ,需要分 3 种情况讨论

 如果 m_ids 包含 trx_id ,则代表 Read View 生成时刻,这个事务还未提交,但是如果数

据的 trx_id 等于 creator_trx_id 的话,表明数据是自己生成的,因此是可见的。

 3.2 如果 m_ids 包含 trx_id ,并且 trx_id 不等于 creator_trx_id ,则 Read View 生成时,

事务未提交,并且不是自己生产的,所以当前事务也是看不见的;

 3.3 如果 m_ids 不包含 trx_id ,则说明你这个事务在 Read View 生成之前就已经提交了,

修改的结果,当前事务是能看见的

RR 如何解决不可重复读

查询一条记录,基于 MVCC,是怎样的流程:

  1. 获取事务自己的版本号,即事务 ID

  2. 获取 Read View

  3. 查询得到的数据,然后 Read View 中的事务版本号进行比较。

  4. 如果不符合 Read View 的可见性规则, 即需要 Undo log 中历史快照;

  5. 最后返回符合规则的数据

6.MySQL优化过程

7.sql语句优化

SQL 优化又能分为三部曲

第一,慢 SQL 的定位和排查

我们可以通过慢查询日志和慢查询日志分析工具得到有问题的 SQL 列表。

第二,执行计划分析

针对慢 SQL,我们可以使用关键字 explain 来查看当前 sql 的执行计划.可以重点关注 type key rows filterd 等字段 ,从而定位该 SQL 执行慢的根本原因。再有的放矢地进行优化

第三,使用 show profile 工具

Show Profile 是 MySQL 提供的可以用来分析当前会话中,SQL 语句资源消耗情况的工具,可用于 SQL调优的测量。在当前会话中.默认情况下处于 show profile 是关闭状态,打开之后保存最近 15 次的运行结果

针对运行慢的 SQL,通过 profile 工具进行详细分析.可以得到 SQL 执行过程中所有的资源开销情况.

如 IO 开销,CPU 开销,内存开销等.

以上就是我对 MySQL 性能优化的理解。

好的,看完高手的回答后,相信各位对 MySQL 性能优化有了一定的理解了,最后我再给各位总结一下

常见的 SQL 优化规则:

SQL 的查询一定要基于索引来进行数据扫描

避免索引列上使用函数或者运算,这样会导致索引失效

where 字句中 like %号,尽量放置在右边

使用索引扫描,联合索引中的列从左往右,命中越多越好.

尽可能使SQL 语句用到的索引完成排序,避免使用文件排序的方式

查询有效的列信息即可。少用 * 代替列信息

永远用小结果集驱动大结果集。

Redis

1.Redis实现分布式锁原理

1.

redis呢性能高,线程安全,修改数据是单线程操作,分布式锁的实现的就是通过redis里面的setnx key value ,返回1表示获取锁成功 ,返回0表示获取失败,为了防止出现死锁,还得给锁加个过期时间 expire key 时间,但是这个操作不是原子性的,好在redis里面提供了一个原子性操作的set key value nx ex 时间

1.死锁问题 加过期时间

2.锁过期任务未处理完 守护线程定时查询锁过期时间

3.误删锁问题 value加和标识,线程id

4.单点故障 redlock 部署5台或者更多的redis服务,客户端

2.Redis和mysql如何保证数据的一致性

一般情况下,Redis 用来实现应用和数据库之间读操作的缓存层,主要目的是减少数据库 IO,还可以提升数据的 IO 性能。

这是它的整体架构。当应用程序需要去读取某个数据的时候,首先会先尝试去 Redis 里面加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到 Redis 里面。在这样一个架构中,会出现一个问题,就是一份数据,同时保存在数据库和 Redis 里面,当数据发生变化的时候,需要同时更新 Redis 和 Mysql,由于更新是有先后顺序的,并且它不像 Mysql中的多表事务操作,可以满足 ACID 特性。所以就会出现数据一致性问题。

在这种情况下,能够选择的方法只有几种。

  1. 先更新数据库,再更新缓存

  2. 先删除缓存,再更新数据库

如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis 中的数据不一致。

如果是先删除缓存,再更新数据库,理想情况是应用下次访问 Redis 的时候,发现 Redis 里面的数据是空的,就从数据库加载保存到 Redis 里面,那么数据是一致的。但是在极端情况下,由于删除 Redis和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。

所以,如果需要在极端情况下仍然保证 Redis 和 Mysql 的数据一致性,就只能采用最终一致性方案。比如基于 RocketMQ 的可靠性消息通信,来实现最终一致性。

还可以直接通过 Canal 组件,监控 Mysql 中 binlog 的日志,把更新后的数据同步到 Redis里面。

因为这里是基于最终一致性来实现的,如果业务场景不能接

短期不一致性,那就不能使用这个方案来做。

以上就是我对这个问题的理解。

3.redis内存淘汰策略

当 Redis 使用的内存达到 maxmemory 参数配置的阈值的时候,Redis 就会根据配置的内存淘汰策略。把访问频率不高的 key 从内存中移除。maxmemory 默认情况是当前服务器的最大内存。

第二个方面:

Redis 默认提供了 8 种缓存淘汰策略,这 8 种缓存淘汰策略总的来说,可以归类成五种

 第一种, 采用 LRU 策略,就是把不经常使用的 key 淘汰掉

 第二种,采用 LFU 策略,它在 LRU 算法上做了优化,增加了数据访问次数,从而确保淘汰的是非热点 key。

 第三种,随机策略,也就是说随机删除一些 key

 第四种,ttl 策略,从设置了过期时间的 key 里面,挑选出过期时间最近的 key 进行优先淘汰

 第五种,当内存不够的时候,进行写操作时会直接报错,这是默认的策略。

这些策略可以在 redis.conf 文件中手动配置和修改,我们可以根据缓存的类型和缓存使用的场景来选择合适的淘汰策略

4.讲一下缓存击穿、穿透、雪崩

缓存雪崩:就是存储在缓存里面的大量数据,同一时刻全部过期,原本缓存组件抗住的大部分流量全部请求到了数据库。导致数据库压力增加造成数据库服务崩溃现象。

导致雪崩的主要原因,我认为有两个:

1.缓存中间件宕机,当然可以对缓存中间件做高可用集群来避免。

  1. 缓存中大部分 key 都设置了相同的过期时间,导致同一时刻这些 key 都过期了。对于这样的情况,可以在失效时间上增加一个 1 到 5 分钟的随机值。

缓存穿透:表示短时间内有大量的不存在的 key 请求到应用里面,而这些不存在的 key 在缓存里

面又找不到,从而全部穿透到了数据库,造成数据库压力。

我认为这个场景的核心问题是针对缓存的一种攻击行为,因为在正常的业务里面,即便是出现了这样的情况,由于缓存的不断预热,影响不会很大。

而攻击行为就需要具备时间的持续性,而只有 key 确实在数据库里面也不存在的情况下,才能达到这个目的,所以,我认为有两个方法可以解决:

1.把无效的 key 也保存到 Redis 里面,并且设置一个特殊的值,比如“null”,这样的话下次再来访问,就不会去查数据库了。

2.但是如果攻击者不断用随机的不存在的 key 来访问,也还是会存在问题,所以可以用布

隆过滤器来实现,在系统启动的时候把目标数据全部缓存到布隆过滤器里面,当攻击者用不存在的

key 来请求的时候,先到布隆过滤器里面查询,如果不存在,那意味着这个 key 在数据库里面也不

存在。

布隆过滤器还有一个好处,就是它采用了 bitmap 来进行数据存储,占用的内存空间很少

缓存击穿:表示请求因为某些原因全部打到了数据库,缓存并没有起到流量缓冲的作用。

我认为有 2 种情况会导致缓存击穿。

 在 Redis 里面保存的热点 key,在缓存过期的瞬间,有大量请求进来,导致请求全部打在数据库上。

 客户端恶意发起大量不存在的 key 的请求,由于访问的 key 对应的数据本身就不存在,

所以每次必然都会穿透到数据库,导致缓存成为摆设。

因此,我认为可以通过几种方法来解决。

  1. 对于热点数据,我们可以不设置过期时间,或者在访问数据的时候对数据过期时间进行续期。

  2. 对于访问量较高的缓存数据,我们可以设计多级缓存,尽量减少后端存储设备的压力。

  3. 使用分布式锁,当发现缓存失效的时候,不是先从数据库加载,而是先获取分布式锁,

获得分布式锁的线程从数据库查询数据后写回到缓存里面。

后续没有获得锁的线程就只需要等待和重试即可。

  1. 对于恶意攻击类的场景,可以使用布隆过滤器,应用启动的时候把存在的数据缓存到布隆过滤器里面

5.多台redis抗高并发访问如何设计

6.并发量超过30w,怎么设计redis架构

RocketMQ、KafKa

1.MQ如何保证消息不丢

1.持久化存储。RocketMQ 将消息持久化到磁盘中,默认情况下会将消息存储到本地文件系统或云存储中,确保消息在服务器重启或宕机后能够恢复

2.主从复制。RocketMQ 支持主从架构,可以配置多个Broker节点形成集群。消息发送到主节点后,主节点会将消息同步到从节点,确保主节点宕机时,从节点能够接替主节点的角色继续提供服务,从而避免消息丢失。

3.同步刷盘。RocketMQ 提供了同步刷盘机制,即在消息发送完成后,立即将消息写入磁盘,并等待磁盘写操作完成后再返回发送结果。这样可以保证消息写入磁盘后才返回成功响应,避免因为宕机或其他原因导致消息丢失。 4.备份机制。RocketMQ 支持配置数据备份策略,可以将消息备份到不同的Broker节点上,以提高消息的容错性和可靠性。当某个Broker节点出现故障时,备份节点可以继续提供服务,保证消息不丢失。 5.高可用特性。RocketMQ 支持多个NameServer节点和多个Broker节点的部署,通过配置合适的复制策略和故障切换机制,可以实现高可用性和消息不丢失。

Spring、SpringBoot

1.SpringIOC,AOP

spring呢是一个轻量化级开发框架,为java提供基础框架支持,简化了企业级别应用开发,让开发者只需要关注业务逻辑

1.springIOC和DI的理解

spring ioc 全称控制反转都得意思,在传统的java程序开发过程中,我们只能通过new关键字来创建对象,这样就导致程序中对象的依赖对象比较复杂,耦合度很高的。

而IOC主要作用就是实现对对象的管理,我们设计好的对象呢,交给spring容器控制,需要用到目标对象的时候呢,直接去容器中拿对象就可用了。有了IOC容器来管理bean以后呢,相当于把对象的创建和查找依赖对象的控制权交给了容器,那么这种设计理念呢使得对象与对象之间是一种松耦合状态,极大的提升了程序的灵活性和功能复用性。

然后,DI表示依赖注入,也就是的也就是对于 IOC 容器中管理的 Bean,如果 Bean 之间存在依赖关系,那么IOC 容器需要自动实现依赖对象的实例注入,通常有三种方法来描述 Bean 之间的依赖关系。1.接口注入、2.setter 注入 、3.构造器注入 。 另外呢,为了更加灵活地实现 Bean 实例的依赖注入,Spring 还提供了@Resource 和@Autowired 这两个注解。分别是根据 bean 的 id 和 bean 的类型来实现依赖注入。

(注:IOC的一些扩展呀还需要去看WPS中的面试题目 直接搜索:IOC)

2.SpringAOP的理解

aop是一种面向切面编思想,主要就是能够将那些与业务无关,却为业 务模块所共同调用的逻辑功能封装起来(例如这个事务处理、日志管理、权限控制等),便于减少重复代码,降低模块间的耦合度,并且有利于未来的可扩展性和可维护性。

SpringAop是基于动态代理实现的,如果代理的对象实现了某个接口,那么SpringAop就会默认使用这个JDK动态代理,而对于没有实现接口的对象,就使用CgLIB动态代理生成一个被代理对象的子类来作为代理。

当然也可以使用AspectJ,SpringAOP中已经集成了AspectJ,AspectJ应该算是Java生态系统中最完整的AOP框架了。使用Aop之后我们可用把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样就可以打打简化代码量。我们需要增加新功能也方便,提高了系统的扩展性。

(注:AOP增强的整个过程..可用去扩展一下)

3.那Spring AOP和AspectJ aop有什么区别?

Spring AOP 是属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。

4.spring Bean的生命周期

Spring生命周期全过程大致可以分为五个阶段:创建前准备阶段、创建实例化阶段、依赖注入阶段、容器缓存阶段和销毁实例阶段

1.创建前准备阶段,这个阶段的主要作用是在Bean开始加载之前,需要从上下文和相关配置中解析查找到Bean有关的扩展实现,比如像 init-method 容器在初始化bean时调用的方法、destory-method 容器在销毁bean时调用的方法,以及BeanFactoryPostProcessor对bean加载过程中的前置和后置处理。这些类或者配置其实是spring提供用来实现bean加载过程中的扩展机制。

2.创建实例化阶段,这个阶段主要就是通过反射创建bean的实例对象,并且扫描和解析Bean声明的一些属性。

3.依赖注入阶段,如果被实例化的Bean存在依赖其他bean对象的情况,则需要对这些依赖bean进行对象注入。比如常见的@Autowiredsetter注入等依赖的配置形式,同时,在这个阶段会触发一些扩展的调用,比如常用的扩展类:BeanPostProcessors(用来实现初始化前的扩展回调)、InitinalingBean、BeanFactoryAware等等

4.容器缓存阶段、主要是将bean保存到容器以及spring缓存中,到了这个阶段Bean就以及可用被开发使用了

5.销毁实例阶段、当 Spring 应用上下文关闭时,该上下文中的所有 bean 都会被销毁。如果存在 Bean 实现了 DisposableBean 接口,或者配置了`destory-method`属性, 会在这个阶段被调用。

5.springBean的作用域有哪些?

首先呢,Spring框架里的Ioc容器,可用非常方便地去帮助我们管理应用里面的Bean对象实例。我们只需要安全Spring里面提供的xml或者注解等方式去告诉IOC容器,哪些Bean需要被IOC容器管理就行了。其次呢,既然是Bean对象实例的管理,那意味着这些实例,是存在生命周期,也就是所谓的作用域。理论上来说,常规的生命周期只有两种:

singleton:也就是单例,意味着在整个Spring容器只会有一个Bean实例。

prototype,也就是原型,意味着每次从IOC容器去获取指定Bean的时候,都会返回一个新的实例对象。

但是在基于Spring框架下的web应用里面,增加了一个会话维度来控制Bean生命周期,主要有三个选择

request:针对每一次http请求,都会创建一个新的bean

session,以session会话为维度,同一个session共享一个bean实例,不同的session产生不同的Bean实例

globaSession:针对全局sesssion维度,共享同一个Bean实例

6.Spring 是如何解决循环依赖问题的?

(注:csdn收藏了一篇介绍)

我们都知道,如果在代码中,将两个或多个Bean互相之间持有对方的引用就会发生循环依赖。循环的依赖将会导致陷入死循环。这就是Spring发生循环依赖的原因。

循环依赖有三种形态:第一种就是互相依赖:A依赖B,B又依赖A,第二种三者依赖:A依赖B,B依赖C,第三中,自我依赖,A依赖A形成了循环依赖。

而Spring中设计了三级缓存来解决循环依赖问题,当我们去调用getBean方法的时候,Spring会先从一级缓存中去找到目标bean,如果发现一级缓存中没有便会去二级缓存中去找,而如果一、二级缓存中都没有找到,意味着该目标 Bean 还没有实例化。于是,Spring 容器会实例化目标 Bean(PS:刚初始化的 Bean 称为早期 Bean) 。然后,将目标 Bean 放入二级缓存中,同时,加上标记是否存在循环依赖。如果不存在循环依赖便会将目标 Bean 存入到一级缓存,否则,便会标记该 Bean 存在循环依赖,然后将等待下一次轮询赋值,也就是解析@Autowired 注解。等@Autowired 注解赋值完成后(PS:完成赋值的 Bean 称为成熟 Bean) ,会将目标 Bean 存入到一级缓存。

总结下来呢:Spring 一级缓存中存放所有的完整的 Bean,二级缓存中存放所有的不完整 Bean,先取一级缓存,再取二级缓存。

7.那三级缓存的作用是什么?

三级缓存是用来存储代理 Bean,当调用 getBean()方法时,发现目标 Bean 需要通过代理工厂来创建,此时会将创建好的实例保存到三级缓存,最终也会将赋值好的 Bean 同步到一级缓存中。

8.spring中哪些情况下,不能解决循环依赖问题?

1.多例 Bean 通过 setter 注入的情况,不能解决循环依赖问题

2.构造器注入的 Bean 的情况,不能解决循环依赖问题

3.单例的代理 Bean 通过 Setter 注入的情况,不能解决循环依赖问题

4.设置了@DependsOn 的 Bean 的情况,不能解决循环依赖问题

9.spring中事务的传播行为有哪些?
  1. REQUIRED:默认的 Spring 事务传播级别,如果当前存在事务,则加入这个事务,如果不存在事务,就新建一个事务。

  2. REQUIRE_NEW:不管是否存在事务,都会新开一个事务,新老事务相互独立。外部事务抛出异常回滚不会影响内部事务的正常提交。

  3. NESTED:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一个事务,类似于 REQUIRE_NEW。

  4. SUPPORTS:表示支持当前事务,如果当前不存在事务,以非事务的方式执行。

  5. NOT_SUPPORTED:表示以非事务的方式来运行,如果当前存在事务,则把当前事务挂起。

  6. MANDATORY:强制事务执行,若当前不存在事务,则抛出异常.

  7. NEVER:以非事务的方式执行,如果当前存在事务,则抛出异常。

10.spring声明式事务原理?哪些场景事务会失效?

spring 声明式事务,即 @Transactional ,它可以帮助我们实现事务开启、提交或者回滚的操作,通过 Aop 的方式进行管理在 spring 的 bean 的初始化过程中,就需要对实例化的 bean 进行代理,并且生成代理对象。生成代理对象的代理逻辑中,进行方法调用时,需要先获取切面逻辑,@Transactional 注解的切面逻辑类似于@Around,在 spring 中实现一种类似代理逻辑

失效场景:

  1. 方法的访问权限必须是 public,其他 private 等权限,事务失效

  2. 方法被定义成了 final 的,这样会导致事务失效。

  3. 在同一个类中的方法直接内部调用,会导致事务失效。

  4. 一个方法如果没交给 spring 管理,就不会生成 spring 事务。

  5. 多线程调用,两个方法不在同一个线程中,获取到的数据库连接不一样✁。

  6. 表的存储引擎不支持事务

  7. 如果自己 try...catch 误吞了异常,事务失效。

  8. 错误的事务传播机制

11.Spring中FactoryBean和BeanFactory的区别

BeanFactory 的理解了有两个。BeanFactory 是所有 Spring Bean 容器的顶级接口,它为 Spring 的容器定义了一套规范,并提供像 getBean 这样的方法从容器中获取指定的 Bean 实例。BeanFactory 在产生 Bean 的同时,还提供了解决 Bean 之间的依赖注入的能力,也就是所谓的DI。

FactoryBean 是一个工厂 Bean,它是一个接口,主要的功能是动态生成某一个类型的 Bean 的实例,也就是说,我们可以自定义一个 Bean 并且加载到 IOC 容器里面。它里面有一个重要的方法叫 getObject(),这个方法里面就是用来实现动态构建 Bean 的过程

SpringBoot

1.约定大于配置

我从 4 个方面来回答。

  1. 首先, 约定优于配置是一种软件设计的范式,它的核心思想是减少软件开发人员对于配置项的维护,从而让开发人员更加聚焦在业务逻辑上。

  2. Spring Boot 就是约定优于配置这一理念下的产物,它类似于 Spring 框架下的一个脚手架,通过Spring Boot,我们可以快速开发基于 Spring 生态下的应用程序。

  3. 基于传统的Spring框架开发web应用,我们需要做很多和业务开发无关并且只需要做一次的配置,比如a. 管理 jar 包依赖b. web.xml 维护c. Dispatch-Servlet.xml 配置项维护d. 应用部署到 Web 容器e. 第三方组件集成到 Spring IOC 容器中的配置项维护而在 Spring Boot 中,我们不需要再去做这些繁琐的配置,Spring Boot 已经自动帮我们完成了,这就是约定于配置思想的体现。

  4. Spring Boot 约定优于配置的体现有很多,比如

a. Spring Boot Starter 启动依赖,它能帮我们管理所有 jar 包版本

b. 如果当前应用依赖了 spring mvc 相关的 jar,那么 Spring Boot 会自动内置 Tomcat 容器来

运行 web 应用,我们不需要再去单独做应用部署。

c. Spring Boot 的自动装配机制的实现中,通过扫描约定路径下的 spring.factories 文件来识别

配置类,实现 Bean 的自动装配。

d. 默认加载的配置文件 application.properties 等等。

总的来说,约定优于配置是一个比较常见的软件设计思想,它的核心本质都是为了更高效以及更便捷地

实现软件系统的开发和维护。

以上就是我对这个问题的理解。

2.自动装配机制原理

自动装配,简单来说就是自动把第三方组件的 Bean 装载到 Spring IOC 器里面,不需要开发人员再去写 Bean 的装配配置。例如我们平时项目中经常会用到redis、mq等中间件,那么我们使用它们呢,一般呢就是引入相关依赖,做一些相关的配置,然后呢注入需要的实例对象,然后直接调用实例对象方法实现相关业务。让我们更加聚焦在了业务代码的开发上,而不需要去关心和业务无关的配置。

在 Spring Boot 应用里面,我们要实现自动装配呢。 只需要在启动类加@SpringBootApplication 注解就可以了,@SpringBootApplication 是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration。

自动装配的实现主要依靠三个核心关键技术。

  1. 引入 Starter 启动依赖组件的时候,这个组件里面必须包含@Configuration 配置类,在这个配置类里面通过@Bean 注解声明需要装配到 IOC 容器的 Bean 对象。

  2. 这个配置类是放在第三方的 jar 包里面,然后通过 SpringBoot 中的约定优于配置思想,把这个配置类的全路径放在 classpath:META-INF/spring.factories 文件中。这样 SpringBoot 就可以知道第三方 jar 包里面的配置类的位置,这个步骤主要是用到了 Spring 里面的 SpringFactoriesLoader来完成的。

  3. SpringBoot 拿到第三方 jar 包里面声明的配置类以后,再通过 Spring 提供的 ImportSelector 接口,实现对这些配置类的动态加载。

以上,就是我对 Spring Boot 自动装配

3.SpringBoot全局异常处理

1.使用@ControllerAdvice注解或者@RestControllerAdvice,它们都是 SpringBoot 提供的注解,用于定义全局异常处理器。在使用这两个注解时,需要使用@ExceptionHandler 注解来指定处理的异常类型。

2.使用@ExceptionHandler注解

除了使用@ControllerAdvice注解外,还可以在控制器中定义一个处理异常的方法使用@ExceptionHandler注解来处理异常。这种方式的好处可以针对不同的控制器定义不同的异常处理器。

3.使用HandlerExcrptionResolver接口

除了使用@ControllerAdvice注解和@ExceptionHandler注解外,还可以自定义一个类实现HandlerExceptionResolver接口来处理异常,这种方式比较灵活,可以自定义处理器的实现方式。

SpringCloud相关

Nginx:是一个高性能的http和反向代理web服务器

反向代理、正向代理是什么意思?

看能不能这么理解,nginx代表客户端访问目标服务器的时候呢,属于正向代理,当nginx代表服务端被客户端访问的时候称为反向代理,当然这是个人的简单理解。

正向代理:正向代理是一个位于客户端和目标服务器之间的代理服务器(中间服务器)。为了从目标服务器取得内容,客户端向代理服务器发送一个请求,并且指定目标服务器,之后代理向目标服务器转发请求,将获得的内容返回给客户端。正向代理的情况下,客户端必须要进行一些特殊的设置才能使用。正向代理是代理客户端,为客户端收发请求,使真实客户端对服务器不可见。

反向代理:反向代理是指以代理服务器来接收客户端的请求,然后将请求转发给内部网络上的服务器,将从服务器上得到的结果返回给客户端,此时代理服务器对外表现为一个反向代理服务器。对于客户端来说,反向代理就相当于目标服务器,只需要将反向代理当作目标服务器一样发送请求就可以了,并且客户端不需要进行任何设置。反向代理是代理服务器,为服务器收发请求,使真实服务器对客户端不可见。

动静分离:

gateway:

网关,作为流量的入口,常用的功能包括路由转发、权限校验、限流等。Spring Cloud Gateway 旨在为微服务架构提供一种简单且有效的 API 路由的管理方式,并基于 Filter 的方式提供网关的基本功能,例如说安全认证、监控、限流等等。

nacos:注册中心和配置中心

配置文件动态更新:动态长轮询机制:客户端会轮询向服务端发出一个长连接请求,这个长连接最多30s就会超时,服务端收到客户端的请求会先判断当前是否有配置更新,有则立即返回如果没有服务端会将这个请求拿住“hold”29.5s加入队列,最后0.5s再检测配置文件无论有没有更新都进行正常返回,但等待的29.5s期间有配置更新可以提前结束并返回。

Ribbon和这个Loadbalance:负载均衡器

Docker、k8s基操

Docker 操作篇:

1.查看镜像 docker images

2.查看所有状态容器 dokcer ps -a

3.查看 docker -v docker info docker --help

操作: 搜索镜像仓库 docker search 下载镜像 docker pull 删除镜像:docke rmi

容器创建:docker create

启动容器: docker start docker run

查看容器ip地址 docker inspect

进入容器: docker exec

容器导出/导入 docker export

删除容器: docker rm -f

k8s操作:kubectl create、get、describe、delete、apply 对应的一个操作指令

专题

事务专题:
介绍一下事务

事务一般是指要做的或所做的事情

而在我们计算机术语中事务呢是对数据库的一系列操作,可由一条sql组成,也可以由多条复杂的,事务需遵循ACID四个特性:

A:原子性,事务是一个不可分割的工作单元,一个事务中的sql操作要么都执行成功,要么都执行失败

C:一致性;数据库从一种状态转化为另外一种一致性状态。事务开始之前和结束后,数据库的完整约束性没有被破坏

i:隔离性,事务与事务之间是互不干扰

D:持久性,事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

根据事务类型的不同呢:可以分为这个扁平事务、带有保存点的扁平事务、链事务、嵌套事务嵌套事务、分布式事务;

扁平事务:beginwork开始 由commitwork或者rollback work结束,要么都执行,要么都回滚

带有保持点的扁平事务:允许在事务执行过程中回滚到同一事务中较早的一个状态,这是因为可能某些事务在执行过程中出现的错误并不会对所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点(savepoint)用来通知系统应该记住事务当前的状态,以便以后发生错误时,事务能回到该状态。

链事务:

嵌套事务:

分布式事务:

通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。对于分布式事务,同样需要满足ACID特性,要么都发生,要么都失效。

事务实现原理(ACID特性如何实现)

Mysql 里面的事务,满足 ACID 特性,所以在我看来,Mysql 的事务实现原理,就是 InnoDB 是如何保证 ACID 特性的。

首先,A 表示 Atomic 原子性,也就是需要保证多个 DML 操作是原子的,要么都成功,要么都失败。那么,失败就意味着要对原本执行成功的数据进行回滚,所以 InnoDB 设计了一个 UNDO_LOG 表,在事务执行的过程中,把修改之前的数据快照保存到 UNDO_LOG 里面,一旦出现错误,就直接从 UNDO_LOG 里面读取数据执行反向操作就行了。

其次,C 表示一致性,表示数据的完整性约束没有被破坏,这个更多是依赖于业务层面的保证,数据库本身也提供了一些,比如主键的唯一余数,字段长度和类型的保证等等。

接着,I 表示事务的隔离性,也就是多个并行事务对同一个数据进行操作的时候,如何避免多个事务的干扰导致数据混乱的问题。而 InnoDB 呢提供了四种隔离级别的实现。分别是:

RU(未提交读)RC(已提交读)RR(可重复读)Serializable(串行化)

InnoDB 默认的隔离级别是 RR(可重复读),然后使用了 MVCC 机制解决了脏读和不可重复读的问题,然后使用了行锁/表锁的方式解决了幻读的问题。

最后一个是 D,表示持久性,也就是只要事务提交成功,那对于这个数据的结果的影响一定是永久性的。不能因为宕机或者其他原因导致数据变更失效。理论上来说,事务提交之后直接把数据持久化到磁盘就行了,但是因为随机磁盘 IO 的效率确实很低,所以 InnoDB 设计了Buffer Pool 缓冲区来优化,也就是数据发生变更的时候先更新内存缓冲区,然后在合适的时机再持久化到磁盘。那在持久化这个过程中,如果数据库宕机,就会导致数据丢失,也就无法满足持久性了。所以 InnoDB 引入了 Redo_LOG 文件,这个文件存储了数据被修改之后的值,当我们通过事务对数据。进行变更操作的时候,除了修改内存缓冲区里面的数据以外,还会把本次修改的值追加到 REDO_LOG里面。当提交事务的时候,直接把 REDO_LOG 日志刷到磁盘上持久化,一旦数据库出现宕机,在 Mysql 重启以后可以直接用 REDO_LOG 里面保存的重写日志读取出来,再执行一遍从而保证持久性。

因此,在我看来,事务的实现原理的核心本质就是如何满足 ACID 的,在 InnDB 里面用到了 MVCC、行锁表锁、UNDO_LOG、REDO_LOG 等机制来保证。

spring事务

在业务代码层面去保证我们对数据库一些列操作在同一个事务里面,要么都执行成功,要么都执行失败

分为编程式事务和声明式事务

编程式事务需要我们手动去开启事务、提交或回滚事务

声明式事务在需要的⽅法上添加 @Transactional 注解即可,⽆需⼿动开启事务和提交事务

声明式事务@Transactional 是基于 AOP 实现的,AOP ⼜是使⽤动态代理实现的。如果⽬标对象实现了接⼝,默 认情况下会采⽤ JDK 的动态代理,如果⽬标对象没有实现了接⼝,会使⽤ CGLIB 动态代理。 @Transactional 在开始执⾏业务之前,通过代理先开启事务,在执⾏成功之后再提交事务。如果中途 遇到的异常,则回滚事务。

事务传播机制机制

事务传播失效场景

分布式事务(csdn收藏的一篇文档很好)

分布式事务是指事务的参与者和支持事务的服务器、资源服务器以及事务管理器,分别位于分布式系统的不同节点上,保证不同数据的一致性。

那么基于 CAP ( 一致性、可用性、分区容忍性)定理和 Base(basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性) 理论,我们可以知道,对于上述情况产生的分布式问题,我们要么采用强一致性方案,要么采用弱一致性方案。

所谓强一致性方案,是指通过第三方事务管理器来协调多个节点的事务性,保证每一个节点的事务达到同时成功或者同时失败,为了实现这样一个需求。我们可以引入 XA 协议,基于 2 阶段提交或者 3 阶段提交的方式去实现,但是如果全局事务管理器中的多个节点,任意一个节点在进行事务提交确认的时候,由于网络通信延迟导致了阻塞,就会影响到所有节点的事务提交,而这个阻塞过程呢,也会影响到用户的请求线程,这对于用户体验以及整体的性能影响非常大。

而弱一致性方案就是针对强一致性方案所衍生出来的性能和数据一致性平衡的一个方案。简单来说就是损失掉强一致性,数据在某一个时刻会存在不一致的状态,但是最终这些数据会达成一致,这样的好处是提升了系统的性能。在弱一致性方案中,常见的解决方案有 3 种:

第 1 个:使用分布式消息队列来实现最终的一致性

第 2 个:基于 TCC 事务,通过演进版本的 2 阶段提交去实现最终一致性。

第 3 个:使用 Seata 事务框架,它提供了多种事务模型。比如说 AT、XA 、Saga、TCC 等,不同的模型提供的是强一致性或者弱一致性的支持。

2阶段提交:又叫两阶段提交、是一种强一致性方案设计,两阶段提交呢,引入了这个事务提交协调者来协调管理个参与者的行为,并最终决定这些参与者是否进行真正的事务。

两阶段是将整个事务分为两个阶段:分别是准备阶段和提交阶段。

准备阶段:事务协调者给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数据文件**

提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源

问题:单点故障(协调者故障导致阻塞和数据一致性)、资源阻塞、数据一致性

3阶段提交:3PC 的出现是为 了解决 2PC 的一些问题,相比于 2PC 它在 参与者中也引入了 超时机制,并且 新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。3PC 包含了三个阶段:分别是 准备阶段、预提交阶段和提交阶段,对应的英文就是: CanCommit、 PreCommit 和 DoCommit。

看起来是 把 2PC 的提交阶段变成了预提交阶段和提交阶段,但是 3PC 的准备阶段协调者只是询问参与者的自身状况,比如你现在还好吗?负载重不重?这类的,而预提交阶段就是和 2PC 的准备阶段一样,除了事务的提交该做的都做了,提交阶段和 2PC 的一样。

改进:引入了参与者超时机制; 多了准备阶段

TCC:2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,TCC 是 Try、Confirm、Cancel 三个词语的缩写,TCC 要求每个分支事务实现三个操作: 预处理 Try、 确认 Confirm、 撤销 Cancel:

try 指的是预留,即资源的预留和锁定, 注意是预留 Confirm 指的是业务确认操作,这一步其实就是真正的执行了 Cancel 指的是撤销回滚操作,可以理解为把预留阶段的动作撤销了

TM 首先发起所有的分支事务的 Try 操作,任何一个分支事务的Try操作执行失败,TM 将会发起所有分支事务的 Cancel 操作,若 Try 操作全部成功,TM 将会发起所有分支事务的 Confirm 操作,其中 Confirm/Cancel 操作若执行失败,TM 会进行重试。

事件表(本地消息表):

最大努力通知方案:

锁 :是解决对共享资源访问操作时的线程安全问题,Java中的锁主要有一下两种

sync锁实现

lock锁实现

CAS底层原理

CAS 叫做 CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含 三个操作数:

  1. 变量内存地址,V 表示

  2. 旧的预期值,A 表示

  3. 准备设置的阈值,B 表示

当执行 CAS 指令时,只有当 V 等于 A 时,才会用 B 去更新 V 的值,否则就不会执行更新操作。

多线程

1.多线程修改arraylist有什么问题?

数据覆盖、数据丢失、索引越界

解决:使用线程安全的集合;ConcurrentLinkedQueue、CopyOnWriteArrayList 或者使用sync和lock、ThreadLocal

2.多线程修改hashmap有什么问题?

数据丢失问题、1.8之前还有这个死循环问题

死循环问题:

1.7版本中,底层是数组+链表,并且是采用头插法插入数据,因此HashMap 正常情况下的扩容会把旧 HashMap 的节点做一个反转,链表元素的顺序是 A、B、C,而新 HashMap 使用的是头插法插入,所以,扩容完成后最终在新 HashMap 中链表元素的顺序是 C、B、A

我简单讲下产生死循环的原因:有线程 T1 和线程 T2 都准备对 HashMap 进行扩容操作, 此时 T1 和 T2 指向的都是链表的头节点 A,而 T1 和 T2 的下一个节点分别是 T1.next 和 T2.next,它们都指向 B 节点。,这时候,假设线程 T2 的时间片用完,进入了休眠状态,而线程 T1 开始执行扩容操作,一直到线程 T1 扩容完成后,线程 T2 才被唤醒。因为 HashMap 扩容采用的是头插法,线程 T1 执行之后,链表中的节点顺序发生了改变由原来的A-B-C变成了C-B-A。但线程 T2对于发生的一切还是不可知的,所以它指向的节点引用依然没变。T2 指向的是 A 节点,T2.next 指向的是 B 节点,但是T2.next.next已经发生了改变,以前是指的C,现在指向A了,这里就产生了死循环ABA。

解决:使用线程安全的集合;使用sync和lock、ThreadLocal

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值