java字符串常量池
intern()方法
源码
解释
- 方法区和运行时常量池溢出
- 由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放到一起进行,HotSpot从JDK7开始逐步 去永久代 计划,并在JDK8中完全使用元空间来替代永久代的背景故事,在此我们就以测试代码来观察一下,使用永久代还是元空间来实现方法区,对程序有什么实际影响
- String::intern()是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此String对象的字符串(首次遇到的字符串实例),则返回代表池中这个字符串的String对象的引用,在JDK6或者更早之前的HotSpot虚拟机中,常量池都是分配在永久代中,我们可以通过 -XX:PermSize和 -XX:MaxPermSize限制永久代的大小,即可间接限制其中常量池的容量
代码示例
-
测试
why
测试结果
- 按照代码结果,只要不是java字符串,其他
58tongcheng meituan alibaba
,返回的都是true;java字符串答案为false,必然是两个不同的java,那另外一个java字符串如何加载进来的? - 为什么?
sun.misc.Version
类会在JDK类库的初始化过程中被加载并初始化,而在初始化时,它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时sun.misc.Version.launcher
静态常量字段所引用的java
字符串字面量就被intern
到HotSpot VM的字符串常量池 —StringTable
里了- 这段代码在JDK 6中运行,会得到两个false,而在JDK7中运行,会的带一个true和一个false。产生差异的原因是,JDK6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串对象实例在java堆上,所以不然不可能是同一个引用,结果将返回false
- 而JDK7(及部分其他虚拟机,例如JRockit)的
intern()
方法实现就不需要在拷贝字符串的实例到永久代,既然字符串常量池已经移到Java堆中,拿只需要在常量池中记录一下首次出现的实例引用即可,因此intern()返回的引用和有StringBuilder创建的那个字符串实例就是同一个,而对str2比较返回 false,这是因为java
这个字符串在执行StringBuilder.tostring()
之前就已经出现过了,字符串常量池中已经有它的引用,不符合intern()方法要求的首次遇到原则
- 再来看看方法区的其他部分的内容,方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这部分区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出为止,虽然直接使用Java SE API也可以动态产生类(如反射时的GeneratedConstructorAccessor和动态代理等)
OpenJDK8底层源码说明
递推步骤
1、System代码解析
-
System
—initializeSystemClass
—Version
2、类加载器和rt.jar
-
根加载器提前部署加载
rt.jar
3、OpenJDK8源码
-
加载文件位置
动态获取
version
类中配置的launcher_name
的值 :java
总结
考查点
- 1、字符串
intern()
方法,判断 true/flase? - 2、《深入理解java虚拟机》书中原题,是否读过经典JVM书籍(周志明 著)
LeetCode两数之和
-
题目详情
-
算法复杂度
- 最优:O(1) ,只找一次,参照redis的k-v键值对
- 其次:O(log2N)
- 再次:O(N)
- 差:O(N^2)
-
哈希 k 值 v 下标
JUC
常见juc相关题目
- 1、Synchronized相关问题
- Synchronized用过吗,其原理是什么?
- 你刚刚提到获取对象的锁,这个锁到底是什么?如何确定对象的锁
- 什么是可重入性,为什么说Synchronized是可重入锁?
- JVM对Java的原生锁做了哪些优化?
- 为什么说Synchronized是非公平锁?
- 什么是锁消除和锁粗化?
- 为什么说Synchronized是悲观锁?乐观锁的实现原理是什么?什么是CAS?
- 乐观锁一点就是好的吗?
- 2、可重入锁ReentrantLock以及其他显式锁相关问题
- 跟Synchronized相比,可重入锁ReentrantLock其实现原理有什么不同?
- 那么请谈谈AQS框架是怎么回事?
- 请尽可能详尽的对比下Synchronized和ReentrantLock的异同
- ReentrantLock是如何实现可重入性的?
可重入锁
- 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象,不会因为之前已经获取过还没释放而阻塞)
- Java中
ReentrantLock
和Synchronized
都是可重入锁,可重入锁的一个优点就是可一定程度的避免死锁
可重入锁解释
- 拆开解释
- 可:可以
- 重:再次
- 入:进入
- 锁:同步锁
- 进入什么:进入同步域(即同步代码块/方法或显式锁锁定的代码)
- 一句话解释:
- 一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入
- 自己可以获取自己的内部锁
可重入锁种类
隐式锁(即Synchronized
关键字使用的锁)默认是可重入锁
-
同步块
-
同步方法
-
可重入锁的代码验证(同步代码块/同步方法)
Synchronized
的重入的实现机制
-
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
当执行
monitorenter
时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程时当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁
当执行
monitorexit
时,Java虚拟机则需将锁对象的计数器减1,计数器为零代表锁已被释放
显示锁(即 Lock )也有ReentrantLock
这样的可重入锁
-
ReentrantLock代码验证
-
进一步验证锁的释放(一定要两两匹配,加一次锁就需要释放一次锁)
LockSupport
为什么要学习LockSupport?
-
Java — JVM
-
JUC — AQS — 前置知识(可重入锁、LockSupport)
-
AB — after | before
LockSupport是什么
-
用于创建锁和其他同步类的基本线程阻塞原语
一句话总结
LockSupport
:线程等待唤醒机制(wait/notify的改良加强版)
LockSupport
中的park()
和unpark()
的作用分别是阻塞线程和解除阻塞线程
线程等待唤醒机制(wait/notify)
3种让线程等待和唤醒的方法
- 方式一:使用Object中的
wait()
方法让线程等待,使用Object中的notify()
方法唤醒线程 - 方式二:使用JUC包中Condition的
await()
方法让线程等待,使用signal()
方法唤醒线程 - 方式三:
LockSupport
类的park()
可以阻塞当前线程以及unpark()
唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
-
代码演示
-
1、正常情况
-
2、异常1:
wait
、notify
去掉Synchronized
关键字修饰,会报错 -
3、异常2:将
notify
放在wait
方法前面,程序一直无法结束,无法唤醒
-
-
小总结
wait
和notify
方法必须要在同步块或者方法里面且成对出现使用- 先
wait
后notify
才有效
Condition接口中的await后signal方法实现线程的等待和唤醒
-
代码演示
-
1、正常情况
-
2、异常1:不加
lock
、unlock
锁操作,会报错 -
3、异常2:先唤醒后等待,程序无法完成
-
传统的synchronized和Lock实现等待唤醒通知的约束(*)
- 线程必须先要获得并持有锁,必须在锁块(Synchronized或Lock)中
- 必须要先等待后唤醒,线程才能够被唤醒
LockSupport类中的park等待和unpark唤醒
是什么
-
通过
park()
和unpark(thread)
的方法来实现线程的阻塞和唤醒操作park()
:除非许可证可用,否则禁用当前线程以镜像线程调度unpark(Thread thread)
:如果给定线程尚不可用,则为其提供许可
-
LockSupport
类使用了一种名为Permit
(许可)的概念来做的阻塞和唤醒线程的功能,每个线程都有一个许可(permit
),permit
只有两个值1和0,默认是0可以把许可看成是一种(0,1)信号量(
Semaphore
),但与Semaphore
不同的是,许可的累加上限是1
主要方法
-
阻塞
-
park()/park(Object blocker)
-
阻塞当前线程/阻塞传入的具体线程
permit
默认是0,所以一开始调用park()
方法,当前线程就会阻塞,直到别的线程将当前线程的permit
设置为1时,park
方法会被唤醒,然后会将permit
再次设置为0并返回
-
-
唤醒
unpark(Thread thread)
- 唤醒处于阻塞状态的指点线程
代码演示
-
1、正常+无锁块要求
-
2、之前错误的先唤醒后等待,LockSupport照样支持(*)
重点说明(*)
-
LockSupport
是用来创建锁和其他同步类的基本线程阻塞原语LockSupport
是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法,归根结底,LockSupport
调用的Unsafe
中的native代码 -
LockSupport
提供park()
和unpark()
方法实现阻塞线程和解除线程阻塞的过程LockSupport
和每个使用它的线程都有一个许可(permit)
关联,permit
相当于1,0的开关,默认是0调用一次
unpark
就加1变成1调用一次
park
会消费permit
,也就是将1变成0,同时park
立即返回如再次调用park会变成阻塞(因为
permit
为零了会阻塞在这里,一直到permit
变为1),这是调用unpark
会把permit
设置为1每个线程都有一个相关的
permit
,permit
最多只有一个,重复调用unpark
也不会积累凭证 -
形象的理解
线程阻塞需要消耗凭证
(permit)
,这个凭证最多只有1个-
当调用park方法时
如果有凭证,则会直接消耗掉这个凭证然后正常退出
如果无凭证,就必须阻塞等待凭证可用
-
而
unpark
则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效
-
常见题目
1、为什么可以先唤醒线程后阻塞线程?
- 答:因为
unpark
获得一个凭证(permit加1变成1),之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞
2、为什么唤醒两次后阻塞两次,但最终结果还是会线程阻塞?
- 答:因为同一个线程的凭证数量最多为1,连续调用两次
unpark
和调用一次unpark效果一样,只会增加一个凭证;而两次调用park
却需要消耗两个凭证,凭证数量不够,不能放行,故线程阻塞
AbstractQueuedSynchronizer之AQS
常见题目
-
题目总结
前置知识
CAS(CompareAndSwap)
-
比较并替换,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B
-
目的:利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,其它原子操作都是利用类似的特性完成的;而整个JUC都是建立在CAS之上的,因此对于synchronized阻塞算法,JUC在性能上有了很大的提升
-
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止
-
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁
公平锁非公平锁
-
公平锁(FairSync):多个线程按照申请锁的顺序去获取锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁
-
优点:所有的线程都能得到资源,不会饿死在队列中
-
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程开销会很大
-
公平锁的创建
final ReentrantLock lock = new ReentrantLock(true); 复制代码
-
-
非公平锁(NonfairSync):多个线程去获取锁的时候,会**直接去尝试获取锁,**获取不到,再去进入队列,如果能获取到,就直接获取锁
-
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必去唤醒所有线程,会减少唤醒线程的数量
-
缺点:你们可能也发现了,这样可能导致队列中间的线程一致获取不到锁或者长时间获取不到锁,导致饿死
-
非公平锁的创建
final ReentrantLock lock = new ReentrantLock(); final ReentrantLock lock = new ReentrantLock(false); 复制代码
-
可重入锁
LockSupport
自旋锁
- 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环
- 获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成
busy-waiting
数据结构之链表
设计模式之模板设计模式
是什么
字面意思
-
抽象的队列同步器
-
源代码
技术解释
-
是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态
AQS为什么是JUC内容中最重要的基石
和AQS有关的
-
ReentrantLock
-
CountDownLatch
-
ReentrantReadWriteLock
-
Semaphore
-
等等
进一步理解锁和同步器的关系
- 锁,面向锁的使用者:定义了开发工程师和锁交互的使用层API,隐藏了实现细节,调用即可
- 同步器,面向锁的实现者:比如Java并发大神Dougee,提出了统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等待
能干嘛
加锁会导致阻塞
- 有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
解释说明
- 抢到资源的线程之间使用处理业务逻辑,抢不到资源的必然涉及到一致排队等候机制,抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)
- 既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要用的是
CLH
队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS
的抽象表现。它将请求共享资源的线程封装成队列的结点(Node
),通过CAS
、自旋以及ReentrantLock.park()
等方式,维护state
变量的状态,是并发达到同步的控制效果
AQS初步
AQS初识
-
官方解释
-
有阻塞就需要排毒,实现排队必然需要队列
-
AQS使用一个
volatile
的int
类型的成员变量来表示同步状态,通过内置的FIFO
队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成一个Node
结点来实现锁的分配,通过CAS
完成对State
值的修改
-
AQS内部体系架构
-
体系架构图
AQS自身
-
AQS的int变量
-
AQS的同步状态State成员变量
/** * The synchronization state. */ private volatile int state; 复制代码
-
类似银行办理业务的受理窗口状态
- 等于0 :就是没人,自由状态可以办理
- 大于等于1:有人占用窗口,排队等着去
-
-
AQS的CLH队列
-
CLH队列(三个大牛的名字组成),是一个双向队列
-
类比银行候客区的等待客户
-
-
小总结
- 有阻塞就需要排队,实现排队必然需要队列
- state变量+CLH变种的双端队列
内部类Node(Node类在AQS类的内部)
-
Node的int变量
-
Node的等待状态
waitState
成员变量:volatile int waitStatus
-
等候区的其他客户(没有抢到锁资源的线程)的等待状态
队列中每个排队的个体就是一个Node
-
-
Node此类的讲解
-
内部结构
static final class Node { // 共享 static final Node SHARED = new Node(); // 独占 static final Node EXCLUSIVE = null; // 线程被取消了 static final int CANCELLED = 1; // 后继线程需要被唤醒 static final int SIGNAL = -1; // 等待condition唤醒 static final int CONDITION = -2; // 共享式同步状态获取将无条件的传播下去 static final int PROPAGATE = -3; // 初始为0,状态是上面的几种 volatile int waitStatus; // 前置结点 volatile Node prev; // volatile Node next; // volatile Thread thread; // Node nextWaiter; 复制代码
-
属性说明
-
AQS同步队列的基本结构
-
同步器
从ReentrantLock开始解读AQS
Lock接口的实现类
- 基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
ReenteantLock的原理
-
ReentrantLock架构图
从最简单的lock方法开始看看公平非公平
-
可以明显的看出公平锁与非公平锁的lock()方法唯一区别就在于公平锁在获取同步状态时,多了一个限制条件:hasQueuedPredecessors()
hasQueuedPredecessors()
方法是公平锁加锁时判断等待队列中是否存在有效结点的方法
采用最常用的lock/unlock作为案例突破口
-
公平锁和非公平锁的差异
- 公平锁:公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程再等待,那么当前线程就会进入等待队列中
- 非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占用锁,也就是说队列的第一个排队现在在unpark(),之后还是需要竞争锁(存在线程竞争的情况下)
AQS源码分析走起
-
1、lock()
-
2、acquire()
-
3、tryAcruire(arg)
- nonfairTryAcquire(acquires)
- return true:结束
- return false :继续推进条件,走下一个方法addWaiter
- nonfairTryAcquire(acquires)
-
4、addWaiter(Node.EXCLUSIVE)
- addWaiter(Node mode) B线程
- enq(node)
- 双向链表中,第一个结点为虚结点(也叫做哨兵结点),其实并不存储任何信息,只是占位;真正的第一个有数据的结点,是从第二个结点开始的
- 假如3号ThreadC线程进来
- prev
- compareAndSetTail
- next
B线程抢占锁资源
C线程抢占锁资源
- addWaiter(Node mode) B线程
-
实际问题:AQS的抢占锁
-
1、第一个线程抢占到锁了,第二个线程是否应该进入队列等待?
答:是的
-
2、再问等候队列中的第一个结点,是否就是当前需进入等待的B结点?
答:不是的,第一个结点是哨兵(傀儡结点),负责通知唤醒,出队等
-
-
5、acquireQueued(addWaiter(Node.EXCLUSIVE),arg)
-
假如再抢抢占失败就会进入
-
shouldParkAfterFailedAcquire:如果前驱结点的waitStatus是SIGNAL状态,即shouldParkAfterFailedAcquire方法会返回true;程序会继续向下执行 parkAndCheckInterrupt 方法,用于将当前线程挂起
-
parkAndCheckInterrupt
-
-
-
6、unlock()方法
-
sync.release(1);
唤醒并出队
原来的哨兵结点出队,B线程变成新的哨兵结点
-
三大流程走向
-
AQS acquire 主要有三大流程走向
AQS小总结
-
抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制,抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍然在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要用的是
CLH
队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现,它将请求共享的线程封装成队列的结点(Node
)通过CAS、自旋以及LockSupport.park()的方式维护state变量的状态,是并发达到同步的控制效果 -
重点流程说明
-
addWaiter
:将当前线程封装成Node对象,并加入排队队列中,根据排队队列是否执行过初始化,执行1、2不同处理逻辑-
1、表示排队队列不为空,即之前已经初始化过了,此时只需将新的node加入排队队列末尾即可
-
2、表示排队队列为空,需执行队列初始化,enq会初始化一个空的Node,作为排队队列的head,然后将需要排队的线程,作为head的next节点插入
队列尚未初始化,调用enq方法;该方法生成一个空的Node对象(new Node()),插入到aqs队列头部,然后将参数node,作为其后继节点,插入队列
-
-
-
整个aqs的核心和难点之一:
-
acquireQueued
、shouldParkAfterFailedAcquire
-
注意这里使用的
for(;;)
自旋 -
首先判断node的前辈结点,是不是head,如果是,说明它是下一个可以获得锁的线程,则调用一次tryAcquire,尝试获取锁,若获取到,则将链表关系重新维护下(node设置为head,之前的head从链表移除),然后返回
-
如果node的前辈结点不是head,或获取锁失败,再判断其前辈结点的waitState,是不是SIGNAL,如果是,则当前线程调用park,进入阻塞状态,如果不是
- 1、==0,则设置为SIGNAL;
- 2、>0(==1),则表示前辈结点已经被取消了,将取消的节点,从队列移出,重新维护下排队链表关系
然后再次进入for循环,上面的逻辑重新执行一遍,注意和doAcquireInterruptibly方法对比,二者区别主要在,发现线程被中断过之后的逻辑处理
-
-
parkAndCheckInterrupt
- 两种情况会导致阻塞结束:
-
1、持有锁的线程,释放锁后,将这个线程unpark了,此时该线程,一定排在队列的队头(不包括head节点)
-
2、线程被interrupt了;(注意,在外部interrupt这个线程,不是抛出InterruptException,这一点和sleep、wait阻塞不一样)
-
- 两种情况会导致阻塞结束:
-
Spring
Spring的aop顺序
Aop常用注解
@Before
:前置通知:目标方法之前执行@After
:后置通知:目标方法之后执行(始终执行)@AfterReturning
:返回后通知:执行方法结束前执行(异常不执行)@AfterThrowing
:异常通知:出现异常时候执行@Around
:环绕通知:环绕目标方法执行
常见题问
问题1:AOP的全部通知执行顺序
-
1、Spring4转到Spring5,AOP的全部通知执行顺序; Spring4、5对于AOP的执行顺序是不同的,Springboot1转到Springboot2,其实就是Spring4转到Spring5
-
代码演示
- 想在除法方法前后置入各种通知,引入切面编程
- 新建一个切面类MyAspect,并为切面类新增两个注解
@Aspect
:指定一个类为切面类@Component
:纳入spring容器管理
问题2:AOP常见使用问题
- 2、使用AOP过程中有哪些坑,你遇到过的?
Spring4+springboot1.5.9演示
-
添加依赖,防止启动报错
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.3</version> </dependency> 复制代码
-
切换springboot版本为1.5.9
-
运行结果
-
正常运行结果
-
运行异常时
-
Spring4下Aop正常顺序+异常顺序
- 正常执行:
@Before
(前置通知)—>@After
(后置通知)—>@AfterReturning
(正常返回) - 异常执行:
@Before
(前置通知)—>@After
(后置通知)—>@AfterThrowing
(方法异常)
Spring5+springboot2.3.3演示
-
运行结果
-
正常时
-
异常时
-
Spring5下Aop正常顺序+异常顺序
- 正常执行:
@Before
(前置通知)—>@AfterReturning
(正常返回)—>@After
(后置通知) - 异常执行:
@Before
(前置通知)—>@AfterThrowing
(异常返回) —>@After
(后置通知)
Spring循环依赖
常见提问
- 解释下spring中的三级缓存
- 三级缓存分别是什么?三个Map有什么异同
- 什么是循环依赖?看过spring源码吗?一般说的spring容器是什么?
- 如何检测是否存在循环依赖?日常开发中是否见过循环依赖的异常?
- 多例的情况下,循环依赖问题为什么无法解决?
什么是循环依赖
-
多个bean之间相互依赖,形成了一个闭环;比如:A依赖于B、B依赖于C、C依赖于A
-
通常来说,如果问Spring容器内部如何解决循环依赖,一定是指默认的单例Bean中,属性互相引用的场景
两种注入方式对循环依赖的影响
循环依赖官网说明
总结
- 我们AB循环依赖问题只要A的注入方式是setter且singleton(单例),就不会有循环依赖问题
循环依赖报错演示-BeanCurrentlyInCreationException
循环依赖现象在Spring容器中,两种注入方式
构造器方式注入依赖
-
代码演示
-
ServiceA
-
ServiceB
-
ClientConstructor
-
-
结论:构造器循环依赖式无法解决的,你想让构造器注入支持循环依赖,是不存在的
以set方法注入依赖
-
代码演示
-
ServiceAA
-
ServiceBB
-
ClientSet
-
重要Code案例演示
基础代码
-
A
-
B
-
ClientCode
-
ClientSpringContainer
加入spring容器
-
步骤
-
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd "> <bean id="a" class="com.touch.air.basic.spring.circulardepend.A"> <property name="b" ref="b"></property> </bean> <bean id="b" class="com.touch.air.basic.spring.circulardepend.B"> <property name="a" ref="a"></property> </bean> </beans> 复制代码
正常运行
-
默认单例,修改为原型
scope="prototype"
循环依赖报错
-
ClientSpringContainer
-
循环依赖异常
-
-
结论:
- 默认的单例(singleton)的场景是支持循环依赖的,不报错
- 原型(Prototype)的场景是不支持循环依赖的,会报错
重要结论(Spring内部通过3级缓存来解决循环依赖)
-
DefaultSingletonBeanRegistry
- 第一级缓存(也叫单例池)
singletonObjects
:存放已经经历了完整生命周期的Bean对象 - 第二级缓存:
earlySingletonObjects
,存放早期暴露出来的bean对象,bean的生命周期未结束(属性还未填充完) - 第三级缓存:
singletonFactories
,存放可以生成Bean的工厂
- 第一级缓存(也叫单例池)
-
只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,如果是非单例的bean,每次从容器中获取都是一个全新的对象,都会重新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中
深度Debug循环依赖
前置知识
-
实例化/初始化
- 实例化:内存中申请一块内存空间(租好房子,自己的家具还没有搬进去)
- 初始化属性填充:完成属性的各种赋值(装修、家电家具进场)
-
3个Map和四大方法,总体相关对象
getSingleton
:获取单例对象doCreateBean
:获取不到,则创建populateBean
:创建过程中,挨个填充属性addSingleton
: 最终放入到单例池中,也就是一级缓存第一层singletonObjects存放的是已经初始化好了的Bean
第二层earlySingletonObjects存放的是实例化了,但未初始化的Bean
第三层singletonFactories存放的是FactoryBean,加入A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean
A/B对象在三级缓存中的迁移说明
- 1、A创建的过程中需要B,于是A将自己放到三级缓存里面,去实例化B
- 2、B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A,然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
- 3、B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面
总结Spring是如何解决的循环依赖
-
整体Debug,深入体验Spring解决循环依赖的过程
- 1、调用
doGetBean()
方法,想要获取beanA,于是调用getSingleton()
方法从缓存中查找beanA - 2、在
getSingleton()
方法中,从一级缓存中查找,没有,返回null - 3、
doGetBean()
方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()
的重载方法(参数为ObjectFactory的) - 4、在
getSingleton()
方法中,先将beanA_name
添加到一个集合中,用于标记该bean正在创建中,然后回调匿名内部类的createBean
方法 - 5、进入
AbstractAutowireCapableBeanFactory#doCreateBean
,先反射调用构造器创建出beanA的实例,然后判断;是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中(即是否在第四步的集合中);判断为true则将beanA添加到三级缓存中 - 6、对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
- 7、调
doGetBean()
方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性 - 8、此时beanB依赖于beanA,调用
getSingleton()
获取beanA,依次从一级、二级、三级缓存中查找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject
,此时这个singletonObject
指向的就是上面的doCreateBean()
方法实例化的beanA - 9、这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
- 10、随后beanA继续它的属性填充工作,此时也获取到了beanB,beanA也随之完成创建,回到
getSingleton()
方法中继续向下执行,将beanA从二级缓存移动到一级缓存
- 1、调用
-
流程解释
-
1、Spring创建bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化
-
2、每次创建bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个
-
3、当我们创建beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完beanB填充属性时又发现它依赖了beanA又是同样的流程
不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA,所以不需要继续创建,用它注入beanB,完成beanB的创建
-
4、既然B创建完毕,beanA就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
-
-
Spring解决循环依赖靠的是Bean的**“中间态”这个概念,而这个中间态指的是已经实例化但还没有初始化的状态(半成品);实例化的过程又是通过构造器创建的,如果A还没有创建出来怎么可能提前曝光,所以构造器的循环依赖无法解决**
Spring为了解决单例的循环依赖问题,使用了三级缓存
- 一级缓存为单例池(
singletonObjects
) - 二级缓存为提前曝光对象(
earlySingletonObjects
) - 三级缓存为提前曝光对象工厂(
singletonFactories
)
假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了B,同样的流程也是实例化后放入三级缓存,接着去填充属性的时候发现B自己依赖A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果没有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程
- 一级缓存为单例池(
Redis
redis传统五大数据类型的落地应用
八大类型
- 1、String(字符串类型)
- 2、Hash(散列类型)
- 3、List(列表类型)
- 4、Set(集合类型)
- 5、SortedSet(有序集合类型,简称zset)
- 6、Bitmap(位图)
- 7、HyperLogLog(统计)
- 8、GEO(地理
备注: redis命令不区分大小写,而
key
是区分大小写的;帮助命令:help @ 类型名词
String
最常用
- set key value
- get key
同时设置/获取多个值
- MSET key value [key value ….]
- MGET key [key ….]
数值增减
- 递增数字:INCR key
- 增加指定的整数:INCRBY key increment
- 递减数值:DECR key
- 减少指定的整数:DECRBY key decrement
获取字符串长度
- STRLEN key
分布式锁SETNX
- setnx key value
- set key value[EX seconds] [PX milliseconds] [NX|XX]
- EX:key在多少秒之后过期
- PX:key在多少毫秒之后过期
- NX:当key不存在的时候,才创建key,效果等同于setnx
- XX:当key存在的时候,覆盖key
应用场景
-
商品编号、订单号采用INCR命令生成,是否喜欢的商品
-
是否喜欢的文章:阅读数,只要点击了rest地址,直接可以使用incr key 命令增加一个数字1,完成数字记录
hash
- 对应java中的数据结构:
Map<String,Map<Object,Object>>
一次设置一个字段值
- HSET key field value
一次获取一个字段值
- HGET key field
一次设置多个字段值
- HMSET key field value [field value ….]
一次获取多个字段值
- HMGET key field [field ….]
获取所有字段值
- hgetall key
获取某个key内的全部数量
- hlen
删除一个key
- hdel
应用场景
-
简易版购物车
list
向列表左边添加元素
-
LPUSH key value [value ….]
向列表右边添加元素
- RPUSH key value [value ….]
查看列表
- LRANGE key start stop
获取列表中元素的个数
- LLEN key
应用场景
-
微信订阅公众号:
1、例如订阅了掘金和CSDN的公众号,然后它们分别发布了文章id 11 和 22
2、只要它们发布了新文章,就会装进我的List:
lpush likeArticle:yy1024 11 22
3、查看自己的订阅号全部文章,类似分页:
lrange likeArticle:yy1024 0 10
set
添加元素
- SADD key member [member …]
删除元素
- SREM key member [memeber …]
获取集合中的所有元素
- SMEMBERS key
判断元素是否在集合中
- SISMEMBER key member
获取集合中的元素个数
- SCARD key
从集合中随机弹出一个元素,元素不删除
- SRANDMEMBER key [数字]
从元素中随机弹出一个元素,出一个删一个
- SPOP key [数字]
集合运算
-
集合的差集运算A-B:属于A但不属于B的元素构成的集合
SDIFF key [key]
-
集合的交集运算A∩B:属于A同时也属于B的共同拥有的元素构成的集合
SINTER key [key …]
-
集合的并集运算A∪B:属于A或者属于B的元素合并后的集合
SUNION key [key …]
应用场景
-
1、微信抽奖小程序(SRANDMEMBER、SPOP)
-
2、微信朋友圈点赞
-
3、微博好友关注社交关系(集合运算)
-
共同关注的人(交集)
-
我关注的人也关注他(大家爱好相同)
-
-
4、QQ内推可能认识的人(差集)
zset
向有序集合中加入一个元素和该元素的分数
添加元素
- ZADD key score member [score memeber]
排序,返回索引之间的所有元素
- ZRANGE key start stop [WITHSCORES]
获取元素的分数
- ZSCORE key memeber
删除元素
- ZREM key memebr [memebr …]
获取指定分数范围的元素
- ZRANGEBYSCORE key min max [WITHSCORES][LIMIT OFFSET COUNT ]
增加某个元素的分数
- ZINCRBY key increment member
获取集合中元素的数量
- ZCARD key
获得指定分数范围内的元素个数
- ZCOUNT key min max
按照排名范围删除元素
- ZREMRANGEBYRANK key start stop
获取元素的排名
- 从小到大:ZRANK key memebr
- 从大到小:ZREVRANK key member
应用场景
-
根据商品销售对商品进行排序显示
-
抖音热搜
分布式锁,为何选择redis分布式锁
- 分布式微服务架构,拆分后各个微服务之间为了避免冲突和数据故障而加入的一种锁,分布式锁
- redis分布式锁实现
- 自己手写简单版本:
string -- setnx + lua
脚本 - 集群模式下:
redisson
- 自己手写简单版本:
常见使用场景:防止超卖
常见提问
- 1、Redis除了拿来做缓存,还有哪些用法?
- 2、Redis做分布式锁的时候需要注意什么问题?
- 3、如果Redis是单点部署的,会带来什么问题?怎么解决这些单点问题?
- 4、集群模式下,比如主从模式,有没有什么问题呢?
- 5、简单介绍下RedisLock?谈谈redisson?
- 6、Redis分布式锁如何续期?看门狗知道吗?
Base案例(Springboot+Redis)
使用场景
- 多个服务间+保证同一时刻内+同一用户只能有一个请求(防止关键业务出现数据冲突和并发错误)
代码实现
-
1、建Module:boot_redis01、boot_redis02
-
2、改POM
-
3、写YML
-
4、主启动
-
5、业务代码
存在的问题
1、单机版没加锁
问题
- 没有加锁,并发请求下数字肯定不对,会出现超卖现象
思考
-
加synchronized?
- 不释放,就一直等待
-
加ReentrantLock?
-
tryLock()
,设置过期时间
-
-
还是都可以?
- 根据具体业务需求,选择合适的
解决
-
修改为2.0版
2、nginx分布式微服务架构
问题
-
2.1、分布式部署后,单机锁还是会出现超卖现象,需要分布式锁
-
2.2、Nginx配置负载均衡
本地host文件
-
2.3、启动两个微服务
-
2.4、模拟高并发(jmeter 压测)
解决
-
上redis的分布式锁 setnx
3、始终释放锁(承接2)
问题
- 如果出现异常,可能无法释放锁,必须要在代码层面
finally
释放锁
解决
4、宕机了(承接3)
问题
- 部署了微服务的机器挂了,代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要加入一个过期时间限定key
解决
-
需要对
lockKey
有过期时间设定
5、key和过期时间的设置,并不具备原子性(承接4)
解决
-
合并成一行,加锁并设置过期时间
6、误删锁
问题
-
由于内部业务执行或远程调用超时,当前线程执行时间超过了设置的锁过期时间,导致A线程的锁,因为到过期时间,自动删除了,A线程的执行的代码
finally
,删除锁其实是删除了B线程的锁,删除了别人的锁
解决
-
只能删除自己的锁,不许动别人的
7、误解锁
问题
-
finally
块的判断+del
删除操作不是原子性的
解决
-
用Redis自身的事务
-
事务介绍
-
Redis的事务是通过
MULTI、EXEC、DISCARD和WATCH
这四个命令来完成的 -
Redis的单个命令都是原子性的,所以这里确保事务性的对象是命令集合
-
Redis将命令集合序列化并确保处于一事务的命令集合连续且不被打断的执行
-
Redis不支持回滚的操作
-
-
相关命令
-
MULTI:用于标记事务块的开始
Redis会将后续的命令逐个放入队列中,然后使用EXEC命令原子化地执行这个命令序列
-
EXEC:在一个事务中执行所有先前放入队列的命令,然后恢复正常的连接状态(类似于Commit)
-
DISCARD:清除所有先前在一个事务中放入队列的命令,然后恢复正常的连接状态
-
WATCH:当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控的状态(类似乐观锁)
WATCH key [key ….]
-
-
-
lua脚本(推荐)
8、过期时间怎么定
问题
- 8.1、确保redisLock过期时间大于业务执行时间的问题
- Redis锁如何续期
- 8.2、集群+CAP对比zookeeper
- 对比Zookeeper
- CAP
- Redis(集群) — AP :Redis异步复制造成的锁丢失,比如:主节点没来的及把刚刚set进来的这条数据给从节点,就宕机了
- Zookeeper — CP
9、综上所述
-
Redis集群环境下,我们自己写的分布式锁也不ok,直接上
RedLock(理念:redis分布式锁)
之Redisson落地实现-
Redisson提供的看门狗机制(WATCH DOG)会帮助锁自动续期
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期 -
结论
- 1、watch dog 在当前节点存活时每10s给分布式锁的key续期 30s
- 2、watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期
- 3、从可2得出,如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中
-
-
再次使用jemeter压测
依据redis性能,调整线程参数,我本地虚拟机,0.4s 100个请求才能出现单机锁超卖
使用redisson之后,基本解决超卖问题
-
再次完善
在更大级别的高并发下,简单的调用
redisson
的lock()、unlock()
方法,会出现如下报错:IllegalMonitorStateException:attempt to unlock lock,not locked by current thread by node id:0da6385f-81a5-4edaf9ewa
(尝试去解锁,但当前线程和解锁的线程不是同一个)
Redis分布式锁总结
Synchronized单机版OK
分布式系统下
- 1、nginx分布式微服务,单机锁不行
- 2、取消单机锁,上redis分布式锁setnx
- 3、只加了锁,没有释放锁
- 4、出了异常,可能无法释放锁,必须要在代码层面finally释放锁
- 5、宕机了,部署的微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockkey的过期时间设定
- 6、为redis的分布式锁key,增加过期时间,此外setnx+过期时间必须同一行
- 7、必须规定只能自己删除自己的锁,你不能把别人的锁删除了,A删B的,B删C的
- 8、redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现
redis缓存过期淘汰策略
常见提问
- 1、生产上你们的redis内存设置多少?
- 2、如何配置、修改redis的内存大小
- 3、如果内存满了你怎么办
- 4、redis清理内存的方式?定期删除和惰性删除了解过吗?
- 5、redis缓存淘汰策略
- 6、redis的LRU了解过吗?可否手写一个LRU算法
Redis内存满了怎么办
Redis默认内存多少?在哪里查看?如何设置修改?
查看Redis最大占用内存
redis默认内存多少可以用?
- 如果不设置最大内存大小或者设置最大为0,在64位操作系统下不限制内存的大小,在32位操作系统下最多使用3GB
一般生产上你如何配置?
- 一般推荐Redis设置内存为最大物理内存的四分之三
如何修改redis内存设置?
-
1、通过配置文件修改
-
2、通过命令修改
config get maxmemory
config set maxmemory 104857600
什么命令查看redis内存的使用情况?
-
info memory
超出最大值会怎样?(OOM)
-
改改配置,故意把最大值设为1个byte试试
-
这时候,再尝试set,发现报错:OOM
结论
- 设置了maxmemory的选项,假如redis内存使用达到上限,没有加上过期时间,就会导致数据写满maxmemory,为了避免类似情况,引出了下一章内存淘汰策略
Redis缓存淘汰策略
-
当前版本有八种缓存策略,如果未设置,默认使用
noeviction
往redis里写的数据是怎么没了的
redis过期键的删除策略
三种不同的删除策略
-
定时删除:对CPU不友好,用处理器性能换取存储空间(拿时间换空间)
Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否已经到达过期时间,然后对它进行删除
立即删除能保证内存中数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放,但是立即删除对cpu的最不友好的,因为删除操作会占用cpu的时间,如果刚好碰到cpu很忙的时候,比如正在做交集或者排序等计算的时候,就会给cpu造成额外的压力
这会产生大量的性能消耗,同时也会影响数据的读取操作
-
惰性删除:对memory不友好,用存储空间换取处理器性能(拿空间换时间)
数据到达过期时间,不做处理,等下次访问该数据时,如果未过期返回数据,已过期,删除,返回不存在
缺点:对内存最不友好
如果一个键已经过期,而这个键又仍然保留中,那么只要这个过期键不被删除,它所占用的内存就不会释放
在使用惰性删除策略时,如果这个数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么他们占用的内存将永远不会释放(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看做是一种内存泄露
-
上面两种方案都走极端 — 定期删除
定期删除策略是前两种策略的折中:定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
上述步骤都过堂了,还有漏洞吗?
-
1、定期删除时,从来没有被抽查到
-
2、惰性删除时,也从来没有被点中过
-
上述2步骤 ===> 大量过期的key堆积在内存中,导致redis内存空间紧张或者很快耗尽
必须要有一个好更的兜底方案…
内存淘汰策略登场
淘汰策略有哪些
-
8种策略
-
noeviction
:不会驱逐任何key -
allkeys-lru
:对所有key使用LRU算法进行删除 -
volatile-lru
:对所有设置了过期时间的key使用LRU算法进行删除 -
allkeys-random
:对所有key随机删除 -
volatile-random
:对所有设置了过期时间的key随机删除 -
volatile-ttl
:删除马上要过期的key -
allkeys-lfu
:对所有key使用LFU算法进行删除 -
volatile-lfu
:对所有设置了过期时间的key使用LFU算法进行删除
-
-
总结
LRU(Least Recently Used)
:最近最少使用LFU(Least Frequently Used)
:使用频率最小- 两个维度
- 过期键中筛选
- 所有键中筛选
- 四个方面
- LRU
- LFU
- random
- ttl
平时用哪一种?
allkeys-lru
如何配置、修改
-
配置文件
-
命令:
config set maxmemory-policy
redis的LRU算法简介
Reids的LRU是什么
- LRU是
Least Recently Used
的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的数据予以淘汰
算法来源
-
leetcode
设计思想
-
特性要求
- 必须要有顺序之分,以区分最近使用的和很久没有使用的数据排序
- 写和读操作一次搞定,时间复杂度O(1)
- 如果容量(坑位)满了要删除最不常用的数据,每次新访问还要把新的数据插入到队头
- 查找快、插入快、删除快,且还需要先后排序
-
LRU的算法核心是:哈希链表
本质就是HashMap+DoubleLinkedList,时间复杂度是O(1),哈希表+双向链表的结合体
手写编码如何实现LRU
案例1
-
参考
LinkedHashMap
,依赖JDK
案例2
-
不依赖JDK