秋招面试基础总结,Java八股文基础(串联知识),四万字大全
目录
值传递和引用传递
静态变量和静态代码块的执行顺序
Java集合的框架,Set,HashSet,LinkedHashSet这三个底层是什么
多线程篇
Java实现多线程的方式
假设一个线程池,核心线程数是2,最大线程数是3,阻塞队列是4,10个并发,介绍一下处理过程
10个并发处理结束后,线程池从3变成2的机制(总结就是核心线程,阻塞队列,救急线程,拒绝策略,假如核心线程为0)
sleep,wait,join,yield的区别以及作用
说说你对线程安全的理解
Thread和Runnable
ThreadLocal的原理和使用场景
ThreadLocal内存泄漏的原因,如何避免
为什么不推荐Exectors
并发,并行,串行的区别
并发三大特性
为什么使用线程池,解释一下线程池的参数
线程池中阻塞队列的作用,为什么先添加到队列而不是先创建最大线程
线程池中线程复用原理
Java 面试题:如何保证集合是线程安全的? ConcurrentHashMap 如何实现高效地线程安全?
分析ConcurrentHashMap在Java 7和Java 8中的实现差异
无锁编程-CAS机制
ReentrantLock拆解
线程周期状态
synchronized底层(悲观锁-锁的优化)
Volatile(内存可见性和有序性)
悲观锁和乐观锁
Spring篇
Spring是什么
AOP理解
IOC理解
如何实现一个IOC容器
BeanFactory和ApplicationContext有什么区别
Spring bean的生命周期
解释一下Spring支持的几种bean的作用域
Spring框架的单例Bean是线程安全的吗
Spring框架中用到了那些设计模式
Spring事务的实现方式和原理以及隔离级别
事务传播机制
Spring事务什么时候失效
Bean的自动装配
Spring循环依赖以及三级缓存
代理与反向代理(JAVA中常用的模式代理模式)
Mybatis的一级,二级缓存
Redis篇
缓存穿透:查询一个不存在的数据,mysql查不到数据,也不会直接写入缓存,就会导致每次请求都查数据库
布谷鸟过滤器
缓存击穿:当热点的key设置的过期时间,恰好这个时间点对大量的key有并发的请求,这些并发请求可能会瞬间把DB压垮。
缓存雪崩:指大量的缓存key同时失效,或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
双写应用
延迟双删
互斥锁(强一致性>延迟双删) 编辑
redis持久化问题
Redis过期之后,会立即删除吗
Redis数据淘汰策略
数据淘汰策略的其他问题
假如数据库1000万数据,Redis只能缓存20w数据,如何保证Redis中数据都是热点数据
Redis的内存使用完了发生什么?
Redis分布式锁 如何实现?
Redission实现的分布式锁(setnx和lua脚本,脚本是串行的)
提供的分布式锁(redis提供了redission这个分布式锁,他会提供一个看门狗,每隔多少s看一下,如果还持有锁,延长生存时间)
MYSQL篇
如何定位慢查询
如何优化慢查询
SQL优化:
避免select * 在表查询中,需要哪些字段必须明确写明,不许写*,
了解过索引吗,什么是索引
聚簇索引,二级索引
并发事务的问题,以及隔离界别
多版本并发控制(MVCC)
ReadView(一致性视图)
MVCC是否可以解决幻读/不可重复读
了解过分库分表吗
结合业务来分库分表
微服务
nacos与eureka的区别
服务雪崩
数据结构
数组为什么查询效率是快
O(n)计数排序, 基数排序, 桶排序
ArrayList源码分析
ArrayList底层实现原理
HashMap实现原理
为什么HashMap扩容是两倍
倘若现在有一个文件20亿整形在磁盘上,如何对文件数据进行排序
假如不重复的话,O(n)情况
HashMap常见面试题
红黑树
哈希表中最重要的一个数据结构:散列表,拉链法
HashMap的put的流程
HashMap的扩容机制
HashMap的寻址算法,为什么数组长度一定是2的n次幂
HashMap在1.7情况下,多线程死循环的问题
算法
归并排序编辑
快速排序
分布式
CAP和Base是分布式系统的理论
BASE理论
为什么使用MQ
场景题
统计用户在线时长
JVM篇章
OOM哪些区域会出现
JVM三大问题
一、JVM内存区域划分
编辑
二、JVM类加载机制
双亲委派模型(常考)
类加载的格式,类卸载
三、垃圾回收(GC)
具体垃圾回收GC步骤
1.判定对象是否为垃圾
方案1:引用计数(不属于是java的方案,java并没有使用该方案)
方案2:可达性分析(JAVA实际采用的方案)
2.释放对象的内存
1.标记-清除(直接释放)
2.复制算法
3.标记整理(压缩)
4.JVM的垃圾回收机制
Po,Vo是什么意思
PO是持久化对象
Vo值对象
Bo业务对象
Redis分布式锁需要注意哪些问题
直播弹幕设计
IO通信(多路复用模型)
设计模式
单例模式
RabbitMQ消息队列
RabbitMQ如何保证消息不丢失
RabbitMQ消息的重复消费问题如何解决的
RabbitMQ中死信交换机(RabbitMQ延迟队列了解过吗)
延迟队列有了解过吗
RabbitMQ如果有100万的消息堆积,如何解决(消息堆积如何解决)
RabbitMQ高可用机制
仲裁队列(用来代替景象集群):主从模式,主从同步基于Raft协议,强一致性(即选择投票主节点),声明队列时候加一个参数即可
值传递和引用传递
https://segmentfault.com/a/1190000016773324(这个写的很好)
public static void valueCrossTest(int age,float weight){ System.out.println("传入的age:"+age); System.out.println("传入的weight:"+weight); age=33; weight=89.5f; System.out.println("方法内重新赋值后的age:"+age); System.out.println("方法内重新赋值后的weight:"+weight); } //测试 public static void main(String[] args) { int a=25; float w=77.5f; valueCrossTest(a,w); System.out.println("方法执行后的age:"+a); System.out.println("方法执行后的weight:"+w); }
静态变量和静态代码块的执行顺序
静态变量(字段)和静态代码块是从类加载时候就开始执行的,实例化对象之后先声明实例化变量再执行构造函数,子类继承父类,则先执行父类的静态变量和静态代码块,再执行子类的静态变量和静态代码块。同样,接着在执行父类和子类非静态代码块和构造函数。
Java集合的框架,Set,HashSet,LinkedHashSet这三个底层是什么
map接口,Collection,List
- HashSet:是Set的主要实现类,数据结构哈希表,底层使用了HashMap
- LinkedHashSet:是HashSet的子类,底层使用了LinkedHashMap,在HashSet的哈希表数据结构基础之上,增加了一个双向链表用来记录元素添加的顺序,能按照添加顺序遍历输出。需要频繁遍历的话效率可能高于HashSet
- TreeSet:自然排序和定制排序
多线程篇
Java实现多线程的方式
1.继承Thread类
public class Threadooo extends Thread{ @Override public void run(){ } public static void main(String[] args) { Threadooo a=new Threadooo(); Threadooo b=new Threadooo(); a.start(); b.start(); } }
2.实现Runnable接口
public class Threadooo implements Runnable{ @Override public void run(){ } public static void main(String[] args) { Thread a=new Thread(new Threadooo()); Thread b=new Thread(new Threadooo()); a.start(); b.start(); } }
3.Callable接口并且配合Future与ExecutoService使用,前两种没有返回值
public class Threadooo implements Callable<Integer> { public static void main(String[] args) { ExecutorService executor= Executors.newFixedThreadPool(2); Future<Integer>future=executor.submit(new Threadooo()); Future<Integer>future1=executor.submit(new Threadooo()); try{ System.out.println("Result of future:"+ future.get()); System.out.println("Result of future:"+ future1.get()); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public Integer call() throws Exception { return null; }
假设一个线程池,核心线程数是2,最大线程数是3,阻塞队列是4,10个并发,介绍一下处理过程
首先它就是10个并发,前两个请求来了之后,都会创建一个核心线程去处理请求,第三个请求来了,直接放阻塞队列去,假如任务都比较堵塞,阻塞队列也都满了,看一下是不是到达最大线程数,可以相当于创建一个临时线程,假如任务丢丢比较慢,就会回收临时线程,假如丢的快,此时都满了,就看你的拒绝策略,大概是拒绝。
10个并发处理结束后,线程池从3变成2的机制(总结就是核心线程,阻塞队列,救急线程,拒绝策略,假如核心线程为0)
主要是从阻塞队列取出下一个任务过程/字段,他会判断该线程是否被回收,因为核心线程数也可以被回收,因为有一个设置可以是核心也被回收,看你上次获取是否超时,假如超时就会进行回收的操作
sleep,wait,join,yield的区别以及作用
锁池:所有需要竞争同步锁的线程都会放在锁池中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池中进行等待,当前面的线程释放同步锁之后,锁池中的线程去竞争同步锁,当某个线程得到之后会进去就绪队列等待cpu资源分配.
等待池:当我们调用wait()方法后,线程会放到等待池中,等待池的线程是不会去竞争同步锁,只有调用了notify()或者notifyAll()后等待池的线程才会开始去竞争锁,notfiy()是随机从等待池选出来一个放入锁池,notifyall()将等待池的所有线程放到锁池子中。
sleep是Thread类的静态方法,wait则是Object类的本地方法
sleep方法不会释放lock(),但是wait会释放,而且会加入等待队列中
sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
sleep不需要被唤醒(休眠后推出阻塞),但是wait需要(不指定时间需要被别人中断)
sleep一般用于当前线程休眠,或者轮询暂停操作,wait则是多用于多线程之间的通信
sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会竞争到锁继续执行的.
yield()执行后线程直接进入就绪状态,马上释放了cpu执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次线程调度还会让这个线程获取到执行权继续执行.(当前把cpu先给别人一会,一会再进入cpu问他的那种状态)
join():执行后进去阻塞状态,例如线程B调用A的join,那么线程B会进入到阻塞队列,直到线程A结束或者中断线程。(相当于join就是执行到这里,先等A完事了,才到你。
说说你对线程安全的理解
线程安全,不如说是内存安全,堆是共享内存,可以被所有线程访问
当多个线程访问一个对象时候,如果不用进行额外的同步控制或者其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象的线程安全的。
堆是进程和线程共有的空间,分全局堆和局部堆,全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的分配,运行过程可以找系统要额外的堆。用完了需要还给他,不然会内存泄漏。
栈:每个线程中栈相互独立,栈是线程安全的,操作系统切换线程时候会自动切换栈。
Thread和Runnable
thread和runnable实质是继承关系,并没有可比性,都会newThread,然后执行run方法,如果有复杂的线程操作需求,那么就去继承Thread,如果只是简单的执行任务那么就去实现Runable.
ThreadLocal的原理和使用场景
每一个Thread对象都含有ThraedLocalMap类型的成员变量,threadLocals,他存储本线程中所有ThreadLocal对象及其对应的值
ThreadLocalMap由Entry对象组成
Entry由一个ThreadLocal对象和object构成,由此Entry的key是ThreadLocal对象
执行set方法或者get方法时候,会ThreadLocal获取当前线程的对象,然后获取ThreadLocal MAP对象,再以当前ThreadLoacl为key,把值存入ThreadLocalMap对象中
每个线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
1.在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
2.线程间数据隔离
3.进行事务操作,用于存储线程事务信息
4.数据库连接,Session会话管理
ThreadLocal内存泄漏的原因,如何避免
内存泄漏为程序在申请内存后,无法释放已经申请的内存空间,一次内存泄漏危害可以忽略,但是内存泄漏堆积后果很严重无论多少内存,迟早被占光,不会再被使用的对象或者变量占用的内存无能被回收就是内存泄漏,比如拥有10个线程的池子假如不会被回收掉,一旦存储很多东西,就会造成内存泄漏(手动调用remove方法)
强引用:使用最普遍的引用(new)一个对象具有强引用,不会被垃圾回收器回收,当内存空间不足,java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想要取消引用和某个对象之间的关联,可以给对象赋值null。
弱引用:JVM进行垃圾回收,无论内存是否充足,都会回收弱引用关联的对象
ThreadLocal实现原理:每一个Thread维护一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
key使用强引用时候:假如values引用了强引用,但是key连接断掉了,key势必会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在强引用,只有Thread线程退出以后,value的强引用链条才会断掉,但是如果当前线程迟迟不结束,那么这些key为null的Entry的value就会一直存在一条强引用链
弱引用:当ThreadLocalMap是弱引用,即使没有手动删除,也会被GC,当key为null时候,在下一次调用set,get方法时候会被清除value值。
ThreadLoacl内存泄漏的根源:是由于ThreadLoaclMap生命周期和Thread一样长,假如没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
正确使用方法:每次使用完ThreadLocal之后调用一下他的remove()方法清除数据。
为什么不推荐Exectors
内置的线程池缺点:使用Exectors创建FixedThreadPool时候,对应的构造方法
new FixedThreadPool(){
return new ThreadPoolExecutor(nThread,nThread,0L,TimeUnit,MILLISECONDS,new LinkedBlockingQueue<Runnable>():
}
LinkedBlockingQueue:是一个无界阻塞队列,如果使用该线程池执行任务,任务过多,会源源不断的添加到队列中,任务越多占用的内存越多,最多可能耗尽内存,
并发,并行,串行的区别
串行:时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
并行:时间上是重叠的,两个任务同一时刻互不干扰的同时执行
并发允许两个任务彼此干扰,统一时间点,只有一个任务运行,交替运行。(对于cpu来说其实也是串行)
并发三大特性
原子性:在一个cpu中不可以中途暂停然后再调度,即不被中断操作,要不全部执行完成,要么都不去执行
比如A向B转账1000元,A-1000,B+1000两个操作必须全部完成
可见性:当多个线程访问同一个变量时候,一个线程修改了这个变量的值,其他线程能够立即看到修改的值,就叫可见性volatile
可见性(总线LOCK,MESI协议:使用这两个协议来保证java可见性)
这样这四步就是原子性的了
有序性:
虚拟机在代码编译时候,对于那些改变顺序之后不会对最终结果产生影响的代码,虚拟机不一定会按照我们写的代码顺序执行,有可能他们进行重排序,事实上有些代码重排序之后,虽然对变量值没有影响,但是可能出现线程安全问题
因为下面代码有可能依赖前面代码的顺序,那么我们如何保证不发生指令重排
volatile适用于与该场景,当然用锁也可以。
为什么使用线程池,解释一下线程池的参数
1.降低资源的消耗,提高资源利用率,线程的创建和销毁都是浪费资源的
2.提升响应速度,任务来了,直接由线程可用可执行,而不是先创建线程,再去执行.
3.提高线程的可管理性,线程是稀缺的资源,使用线程池可用统一分配调优监控
corePoolsize:代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后不会销毁,而是常驻线程
maxinumPoolSize:最大线程数,与核心线程数相对应,常驻5个线程,但是可能高峰期,来声明10个线程,只在高峰期保持线程,最大的允许被创建的线程数
keepAliveTime,unit表示超出核心线程数之外的线程空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程,如果一直空闲会被扣除,通过setKeepAliveTime()来设置线程空闲时间
workQueue:用来存放代执行的任务,假设我们现在核心线程都已经被使用,还有任务进来全部放入队列,直至整个队列满了,才会接着创建新的线程。
ThreadFactory:线程工厂,用来生产线程执行任务,我们可以使用默认的创建工厂,产生的线程都在一个组内,拥有相同的优先级,且都不是守护线程,当然我们也可以选择自定义线程工厂,一般我们会根据业务来指定不同的工厂。
Handler:任务拒绝策略,当我们调用shutdown方法关闭线程池后,这个时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再想线程池提交任务就会遭到拒绝,另一种则是,当前到达了最大线程数,线程池已经没有能力继续处理新提交的任务时候,就会拒绝
线程池中阻塞队列的作用,为什么先添加到队列而不是先创建最大线程
一般队列只能保证一个有限长度到缓冲区,但是如果超出缓冲长度,就无法保留当前任务了,阻塞队列通过阻塞可以留住当前想要继续入队的任务。
阻塞队列可以保证人物队列中没有任务时阻塞获取任务的线程,是线程进入wait,释放cpu资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时候,线程池利用阻塞队列的take方法挂起,从而维持核心线程的话,不至于一直占用cpu资源
2.在创建新线程的时候,是获取全局锁的,这个时候其他的就会阻塞,会影响整体的效率
比如一个企业有十个正式工,最多招10个正式工,部门肯定开始一两个,等任务,来了个任务超人了,再去招人,招满10个的话,再来任务就赶不过来,所以需要招临时工,这样新来的任务就会被领导拒绝了。
线程池中线程复用原理
线程池中将线程和任务解耦(我们写的时候就不一样了,Thread基本和代码绑定一起的)
摆脱了之前通过Thread创建线程的一个线程必须对应一个任务的限制
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,他的核心原理在于对线程池Thread进行的封装,并不是每次执行任务都会调用Thread.start()方法,来创建新线程,而是让每一个线程都去执行一个循环任务,在这个循环任务中不断检查,是否有任务需要被执行,如果有直接执行,也就是调用任务中的run方法(不用start()的方法,在线程池里面不同,正常的都是start()才是多线程执行),将run方法,当成一个普通方法执行。
Java 面试题:如何保证集合是线程安全的? ConcurrentHashMap 如何实现高效地线程安全?
最简单的方法是使用 Collections.synchronizedXXX 方法来包装集合,例如 Collections.synchronizedList 和 Collections.synchronizedMap。然而,这种方式的性能较低,因为它在每个操作上都添加了同步锁。
为了解决性能问题,Java 提供了一系列并发集合类,如 ConcurrentHashMap、CopyOnWriteArrayList 等。这些类通过细粒度锁和无锁算法来提高并发性能。特别是 ConcurrentHashMap,它通过分段锁(Segment Locking)机制,将整个哈希表分成多个段,每个段独立加锁,从而实现更高效的并发访问。同步容器:如
Hashtable
,其所有方法都被synchronized
修饰,确保线程安全CAS(Compare-And-Swap)操作:Java 8中采用CAS操作(无锁算法)来更新某些字段,如计数器,减少锁的开销。
分离锁:使用不同类型的锁来保护不同的数据结构。例如,在Java 8中,使用ReentrantLock和synchronized关键字结合来实现高效并发控制。
分布式桶:使用多个桶来存储数据,每个桶都有自己的锁,减少锁的竞争。
树化结构:当单个桶中的元素数量过多时,采用红黑树结构来替代链表,提高查询效率。传统的HashTable里面所有方法都使用了synchronized。
- 局限性:
分析ConcurrentHashMap在Java 7和Java 8中的实现差异
- 性能瓶颈:由于每个操作都需要获取锁,在高并发环境下性能较低。
- 竞争激烈:当多个线程频繁访问和修改集合时,锁竞争会变得非常激烈,导致系统性能下降。
Java 7中的实现
分段锁(Segmented Locking):将整个Map分成多个段(Segment),每个段独立加锁,提高并发性能。
工作原理:每个Segment内部使用类似于Hashtable的实现,但不同Segment之间可以并行操作。
示例:// Java 7中的ConcurrentHashMap ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>(16, 0.75f, 16);
Java 8中的实现
无锁和CAS操作:引入CAS操作(无锁算法)来更新某些字段,减少锁的开销。
分离锁:使用不同类型的锁来保护不同的数据结构,结合ReentrantLock和synchronized关键字实现高效并发控制。
树化结构:当单个桶中的元素数量超过阈值时,采用红黑树结构来替代链表,提高查询效率。
示例:// Java 8中的ConcurrentHashMap ConcurrentHashMap<Integer, String> concurrentHashMap = new ConcurrentHashMap<>(); concurrentHashMap.put(1, "value1"); concurrentHashMap.put(2, "value2");
无锁编程-CAS机制
比较并且交换,对于共享变量原子性,加同步锁导致性能损耗,CAS比较内存地址偏移量,
CAS:假设多线程操作一个对象,用互斥锁,但是互斥锁是悲观的,会十分严格,只供一个资源使用,在大部分条件下都是读操作,不锁定资源,也对线程协调。
Unsafe:替换内存中某个位置的值,会造成额外消耗(自选锁)
缺点:只能保证对一个变量的操作是原子性的,无法实现多行代码实现原子性。
如何实现CAS的原子性呢?(避免线程的挂起)
假如启动三个线程,进行从1加到1000
AtomicInteger-JAVA自己的计数器(轮子)
synchronized-monitorenter -monitorexit编译后是这两个指令
monitor-管程/监视器
理解为只能住一个人的小房子
无锁:没有对资源进行锁定,这也会出现几种情况,无竞争,存在竞争(非锁方式同步线程)
但是我还是想通过一些机制控制多线程的话,使用了CAS:多个线程修改同一个值,其他修改失败的值,会一直重试,这就是CAS
偏向锁:最好的情况是,对象认识这个锁,只要是这个锁,就直接交出去。
ReentrantLock拆解
Java中提供的锁,synchronized,lock锁(ReentrantLock锁,Reentrant
ReentrantLock是一个互斥锁,可以让多线程执行期间,只有一个线程在执行的一段代码.
使用方式: ->CAS,volatile
lock.lock();
try{
//执行业务
lock.lock();
}finally{
//释放锁
lock.unlock()};
//执行事务
lock.unlock();
线程周期状态
synchronized底层(悲观锁-锁的优化)
通过JVM内置的Monitor监视器实现的,监视器是依赖于操作系统的Mutex实现的
无锁状态,偏向锁状态,轻量级锁,重量级锁,不可降级
锁升级过程
无锁:没有锁,new 一个时候,不用synchronized
偏向锁:在锁对象的对象头记录当前获取锁的线程ID,该线程下次如果又来获取该锁,就可以直接获取到,支持锁重入
轻量级锁:当两个或以上线程交替获取锁,但是并没有在对象上并发的获取锁时候,偏向锁升级为轻量级锁。在此阶段,线程采取CAS自旋(用户态并不会消耗太多资源)的方式尝试获取锁,避免阻塞线程造成cpu在用户态和内核态直接转换的消耗
两个或者以上并发在同一个对象上进行同步时候,为了避免无用自旋消耗cpu,轻量级锁会变成重量级锁
Volatile(内存可见性和有序性)
对volatile变量写操作时候,JVM对处理器发送一条lock前缀的指令,将这个缓存的变量强制写回到系统主存中,所以当一个变量被volatile所修饰的话,每次数据变化之后,值会被强制写入主存,而其他处理器的缓存由于遵守缓存一致性协议,就会把变量的值从主存读取到自己的工作内存中,保持可见性,volatile能够保证内存可见性,会强制从主内存中读取数据,此时如果其他线程修改被volatile修饰的变量,可以第一时间读取到最新的值。
有序性:禁止指令重排优化,JVM为了优化执行效率,会把指令进行重新排序,假如加上这个就可以禁止重排
悲观锁和乐观锁
悲观锁:认为并发一定会引发冲突,因此访问共享资源的之前,会先去获取锁来保护共享资源的完整性,执行方法执行完才会释放锁,悲观锁一般使用互斥实现,即访问共享资源之前会进行加锁操作,确保同一时间只有一个线程能够访问共享资源
乐观锁:乐观锁认为并发访问不会引发冲突,因此读数据时候,不去立即加锁,而是在更新数据时候,检查上次读取依赖数据是否已经被其他人修改过
一般使用版本号机制实现,通过版本号来比较检测是否发生冲突
当一个线程要更新共享资源时候,首先会读取当前版本号和时间戳,然后进行计算更新
主要区别:
加锁的时机不同:悲观锁是在访问共享资源之前就会将其锁定,防止发生冲突,乐观锁只有在操作数据时候,才会通过版本对比来检查冲突的发生,而不会一上来就加锁
加锁实现不同:悲观锁通过阻塞和排队等待来保证同一时刻只有一个线程可以访问数据,可能会带来线程的阻塞和性能消耗,而乐观锁通过版本号或表计等机制来避免阻塞,但是需要进行冲突检测和处理
性能不同:悲观锁适用并发竞争比较大的情况,可以保证数据安全性,乐观锁适用于并发比较小。
Spring篇
Spring是什么
轻量级的J2EE框架,用来装javabean(java对象)中间层框架,可以起连接作用
spring是一个轻量级的(IOC)+面向切面的(AOP)容器框架,通过控制反转(IOC)技术达到送耦合目的,提供了面向切面变成的丰富支持,允许通过分离应用的业务逻辑与系统级服务进行内聚性开发,包含并且管理应用对象(Bean)的配置和生命周期。将简单的组建配置组合成复杂的应用,本质是一个框架.
AOP理解
AOP,系统由许多不同的组件组成的,每一个组件负责一块特定功能,除了实现自身核心功能之外,这些组件还经常承担着额外指责,比如日志,事务处理和安全这样的核心服务经常融入到自身具有业务逻辑的组件中去,这些系统服务经常被称为横切关注点,因为会跨越多个组件。
比如打日志,需要好多都要继承接口
AOP是把程序中交叉点业务逻辑(安全,日志,事务等)封装成一个切面,然后注入目标对象(具体业务逻辑中去),AOP可以对某个对象或者某些对象的功能进行增强,比如对象中的方法进行增强,可以执行某个方法之前额外做的一些事情。
IOC理解
IOC容器:容器概念,控制反转,依赖注入
IOC本身是容器,用来存储java中各种对象,本质上是一个map(里面存储等xml里面配置等bean节点,@repository,@service,@controller,@component)项目启动时候会读取配置文件中的bean节点,根据全限定类使用反射创建对象,放到map里面,扫描到上述注解的类,还是反射创建对象,放入到map里面.
当代码里面需要用到里面的对象,就会通过DI注入,比如autowired,resource等注解
控制反转
当没有引入IOC之前,比如对象A依赖于对象B,当对象A代码里面去new一个B,我们需要主动的去创建B对象,或者去找之前创建好的B对象
引入了IOC容器以后,A对象放到IOC容器,B对象也放入IOC容器,A与B此时就会解耦
,A需要B时候,IOC会主动创建一个B,然后给A,
全部的对象控制权,都上传给IOC,所以IOC也成了整个系统的关键核心,相当于他是控制中枢
依赖注入:
实现控制反转的方法,IOC容器运行期间,动态的将某种依赖关系注入到对象中。
如何实现一个IOC容器
1.配置文件,扫描路径
2.递归包扫描获取.class文件
3.反射,确认需要交给IOC管理的类
4.对需要注入的类进行依赖注入
配置文件中指定需要扫描的包路径
定义一些注解,分别表示访问控制层,业务服务层,数据持久层,依赖注入注解,获取配置文件注解
从配置文件中获取需要扫描的包路径,获取当前路径下的文件信息和文件夹信息,我们将当前路径下,所有以.class结尾的文件添加到一个Set集合中进行存储
遍历这个Set集合,获取类上有指定注解的类,并将其交给IOC容器,定义一个安全的MAP用来存储这些对象
遍历这个IOC容器,获取到每一个类的实例,判断里面是否有依赖其他类的实例,然后进行递归注入。
字节码是什么?采用字节码的好处
JVM:在机器和编译程序之间,加入一层抽象的虚拟机器,这台虚拟机器在任何平台上都提供给编译程序一个共同的接口。
为神马jar包在windows能跑,linux也能跑,是因为JVM帮助我们做适配
在java中,这种供虚拟机理解的代码叫做字节码(即拓展名字是.class的文件),它不面向任何特定的处理器,只面向虚拟机.
每一种平台的解释器是不同的,但是实现的虚拟机是相同的,JAVA源程序经过编译后形成字节码文件,它不面向任何处理器,只面向虚拟机。
Java源程序经过编译器后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译给特定机器上的机器码,然后在特定的机器上运行,
Java源代码->编译器->jvm可执行的java字节码(虚拟指令)->jvm->jvm中编译器->机器可执行二进制机器码->程序运行
使用字节码的好处
JAVA语言通过字节码的方式,在一定程度上解决了传统解释语言效率低的问题,同时又保留了解释型语言可移植的特点,因此java程序的运行时也比较高效
BeanFactory和ApplicationContext有什么区别
ApplicationContext是BeanFactory的子接口
ApplicationContext提供了更完整的功能
1.继承MessageSource,因此支持国际化
2.统一资源文件访问方式
3.提供在监听器中注册bean的事件
4.同时加载多个配置文件
5.载入多个(有继承关系的)上下文,使每一个上下文都专注于一个特定的层次,比如应用的Web层。
BeanFactory采用的是延迟加载的形式来注入Bean的,即只有在使用某个Bean(getBean())的时候,才会对该Bean进行加载类实例化,这样,我们就不能发现一些存在的Spring的配置问题,如果Bean的某一个属性没有注入,BeanFactory加载后,直至第一次调用getBean方法才会抛出异常
ApplicationContext,他是容器启动时候,一次性创建了所有的Bean,这样在容器启动时候,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖的属性是否注入,ApplicationContext启动后预载入所有的单实例Bean,通过预加载单实例Bean,确保当你需要的时候,你就不用等待,因为他们已经创建好了
相对于基本的BeanFactory,ApplicationContext唯一的不足就是占用内存空间,当应用程序配置Bean较多时候,程序启动比较慢。
BeanFactory常常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader
BeanFactory和ApplicationContext都需要支持BeanPostProcessor,BeanFactoryPostProcessor的使用,但是两者之间的区别是,BeanFactory需要手动注册,而ApplicationContext是自动注册.
Spring bean的生命周期
1.解析类得到BeanDefinition
2.如果有多个构造方法,则需要推断构造方法
3.确定好构造方法后,进行实例化一个对象
4.对对象的加了@Autowired注解的属性进行属性填充
5.回调Aware方法,比如BeanNameAware,BeanFactoryAware
6.调用BeanPostProcessor的初始化前的方法
7.调用初始化方法
8.调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
9.如果当前创建的bean是单例则会把bean放入单例池
10.使用bean
11.spring容器关闭时候调用DisposableBean中的destory()方法。
解释一下Spring支持的几种bean的作用域
singletion:默认,每个容器中只有一个bean的实例,单例模式由BeanFactory自身来维护,该对象的生命周期与Spring IOC容器是一致的(但是第一次被注入的时候,才会被创建)
prototype:为每一个bean请求提供一个实例,在每次注入时候,都会创建一个新的对象
request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说单个请求中都会复用这个一个单例对象.
session:与request范围类似,确保每个session中有一个bean的实例,在session过期之后,bean会随之失效
application:bean被定义为在ServletContext(Servlet的上下文)的生命周期中复用一个单例对象。
websocket:bean被定义为websocket的生命周期中复用一个单例对象。
Spring框架的单例Bean是线程安全的吗
Spring中Bean并非线程安全的,框架并未对bean进行多线程处理.
有状态就是有数据存储功能。(Service里面的count,用来计数)
无状态就是不会保存数据,controller,service,dao层本身并不是线程安全的,只是如果调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制遍历,这是自己的线程的工作内存,是安全的。
碰到Controller,service,dao层什么来个static全局共享推荐是自己去单独加锁,或者ThreadLocal.
不要在bean生命任何有状态的实例变量或者类变量,如果必须如此,那么就使用ThreadLocal把变量变成线程私有的,如果bean的实例变量或者类变量,需要在多个线程之间共享,那么使用synchronized,local,CAS等线程同步的方法
Spring框架中用到了那些设计模式
简单工厂:由一个工厂类根据传入的参数动态决定应该创建哪一个产品类,
Spring的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的表示来获得Bean对象,但是否在传入参数后,创建还是传入参数前创建这个要根据具体情况来定。
工厂方法:
实现了FactoryBean接口的Bean是一类叫做factory的bean,其特点是,spring会使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory的这个bean,而是bean.getObject(0方法的返回值
单例模式:保证一个类仅有一个实例,并提供一个访问他的全局访问点。
spring对单例的实现,spring中单例完成了后半句,提供了全局的访问点BeanFactory,但是没有构造器级别去控制单例,这是因为Spring管理的事任意的java对象
适配器模式
spring定义了一个适配接口,使得每一种Controller有一种对应的适配器(HandlerAdapter实现类,让适配器代替Controller执行相应的方法,这样扩展Controller时候,只需要增加一个适配器类就完成了SpringMVC的扩展了
装饰者模式
动态的给一个对象添加一些额外的职责,就增加功能来说,Decorator模式相比生成子类更为灵活。Spring包装类模式在类名中两种表现一种类名含有wrapper,另一种Decorator.
动态代理
切面在应用运行的时候被织入,一般情况下,在切面时候,AOP容器会为目标对象创建一个代理对象,SpringAOP就是以这种方式织入切面的(织入:把切面应用到目标对象并创建新的代理对象的过程)
观察者模式:
spring的事件驱动模型,使用的是观察者模式,spring中Observer模式常用的地方是listener的实现
策略模式:
Spring框架的资源访问Resource接口,该接口提供了更强的资源访问能力,Spring框架本身使用Resource接口来访问底层资源。
Spring事务的实现方式和原理以及隔离级别
一种是编程事务,一种是申明式的,@Transactional注解就是申明式的
事务这个概念是数据库层面的,Spring只是基于数据库中的业务进行扩展,以及提供了一些能让程序员更加方便操作事务的方式
比如通过在某个方法上@Transactional注解,就可以开始事务,这个方法中所有的sql都会在一个事务中执行,统一成功或者失败,在一个方法上加上@Transactional注释,Spring会基于这个类生成一个代理对象,会将代理对象,作为bean,当在使用这个代理对象的方法,如果这个方法存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中会将事务进行提交,如果执行逻辑方法出现异常,那么会事务回滚。(切记别去catch)
可以利用@Transactional注解里面的rollbackFor属性进行配置
read uncommitted(未提交读)
read committed(提交读,不可重复读)
repeatable read(可重复读)
serializable(可串行化)
事务传播机制
多个事务方法相互调用时,事务如何在这些方法间传播
(方法A是一个事务方法,方法A执行过程中国调用了方法B,那么方法B有无事务以及方法B对事务的要求不同,都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B对事务执行也有影响,这种影响具体由事务的传播类型决定
REQUEST(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前有事务,则加入这个事务
SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY:当前存在事务,则加入当前事务,如果当前没有事务,那么就抛异常
REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER:不使用事务,如果当前事务存在,则抛出异常
NESTED:如果当前事务存在,则嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
Spring事务什么时候失效
Spring事务原理是AOP,进行了切面增强,失效的原因是AOP不起作用了,有以下几种情况
1.发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是UserService本身
解决方法就是直接属性注入@Autowired
2.方法不是public
@Transactional只能用于public,否则事务不会失效,要是用在非public上,可以开启Aspectj代理模式
3.数据库不支持事务(myisam 而不是innodb)
4.没有被spring管理
5.异常被吃掉,事务不会回滚
Bean的自动装配
autowire属性由五种装配模式
byName-根据bean的属性名称进行自动装配
byType:根据bean的类型进行自动装配
constructor:类似于byType,不过是应用于构造器的参数,如果一个bean与构造器参数的类型形同,则进行自动装配,否则导致异常
autodetect-如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType的方式进行自动装配
Spring循环依赖以及三级缓存
循环依赖(除了这个@Resource注入之后,还有一种是构造器注入,但是构造器注入,就是反射的时候,拿他的构造器把参数传递进去,进行反射出一个实例出来,但此时用构造器注入时候这个参数需要是有的才反射出来,不能说你传递一个null,进去给你反射出来。(构造器注入的话,用三级缓存无法解决这个问题)
@Service TestBeanService @Resource TestService @Service test Service @Resource TestBeanService
三级缓存为了解决循环依赖,
SingletonObjects:存储的是已经创建好的单例对象
EarlySingletonObjects:这里面存储的是一个引用(里面的属性全是空的)
SingletonFactory:一个工厂,通过一个工厂去创建上面那个
当我们有一层依赖的时候,只有SingletonFactory是起到作用的。
当两层依赖的时候,她EarlySingletonObjects时候才会起作用
比如A-B-A A->c—>A
使用二级缓存也可以,使用三级缓存的真正原因是避免在正常情况下提前创建代理对象.
代理与反向代理(JAVA中常用的模式代理模式)
他是通过创建一个代理对象来实现对目标对象的访问,以达到保护和增强目标对象的目的
静态代理:从编译前就已经确定要做的事情,无法运行的时候动态新增(手动为每一个需要代理的对象编写一个代理类)区别是静态代理是编译器就已经确定了,但是动态代理是运行时候确定的。
相当于老板雇佣秘书处理不重要的事务
无法代理全部方法,而是需要一个方法一个方法去代理
一个目标类就需要一个代理类。
动态代理:相当于来了个人力公司(因为张总需要代理,王总也要代理)
两家人力公司:
JDK:需要实现目标类的接口,通过反射来实现,实现JDK动态代理的核心是Proxy类和InvocationHandler接口(JDK1.8对反射优化,使用时候大差不差) (实现接口用)
CGLib:不需要,因为生成的class是继承目标类 (他的底层是通过ASM字节码技术实现的,它可以在运行时候动态生成一个目标对象子类,从而实现目标对象的代理,CGlib动态代理的前提是目标对象要能够被继承,即不能被final关键字修饰,未实现接口用)
使用JDK动态代理的前提,被代理的类必须实现一个或者多个接口,所以被代理的类没有实现接口,则可以考虑使用CGlib动态代理。
Mybatis的一级,二级缓存
一级缓存:基于PrepetualCache的HashMap本地缓存,其存储的作用域为Session,当Session进行flush或者close之后,该Session中的所有Cache就将清空,默认打开一级缓存
比如
UserMapper userMapper1=sql.getMapper(UserMapper.class) UserMapper userMapper=sql.getMapper(userMapper.class) User user=userMapper.selectById(6); //下次执行时候,则不会再次执行,因为他会保存到本地缓存。 User user=userMapper.selectById(6);
二级缓存基于namespace和mapper的作用域起作用的,不是依赖SQL session默认也是采用PerpetualCache,HashMap存储,需要单独开启,一个核心配置,一个mapper映射文件
二级缓存默认是关闭的
开启两步,1.全局配置文件,2映射文件
<setting name="cacheEnabled value "true">
当摩伊作用域(一级/二级缓存)进行的增删改,默认作用域上所有select缓存被clead
二级缓存:需要缓存的数据实现Serializable接口,只有会话提交或者关闭之后,一级缓存才会转移到二级缓存中
Redis篇
数据结构:String,List,Set,Zset,Hash,Geo,Hyperloglog,Bitmap。
使用场景:缓存:穿透,雪崩,双写一致,持久化,数据过期,淘汰策略
分布式锁:setnx redisson
计数器,
保存token,
消息队列,-数据类型
延迟队列
2.其他面试题:跟业务有关-集群,主从 哨兵
事务
redis为什么这么快
如果发生了缓存穿透,击穿,雪崩如何解决
api/new/getById1->查找redis(查找不到)->查找DB
缓存穿透:查询一个不存在的数据,mysql查不到数据,也不会直接写入缓存,就会导致每次请求都查数据库
解决方案一:缓存空数据,返回查询数据为null(可能发生数据不一致)(但是有可能发生我们这个有人后面添加了,但是缓存还是为null,数据不一致)
方案二:
根据id查询数据库
位图只可以处理整形,优点就是快
布隆过滤器根据bitmap位图产生(使用redission实现的布隆,底层是先去初始化一个比较大的数组里面放着二进制的0或者1,一开始都是0,然后一个key来经过3次hash计算,模数组的长度找到对应的下标,把原先的0改成1,这样的话数组就可以表示一个key的存在
假如为空,就去过滤那些不存在的数据,但是可能出现误判,误判无法避免,一般可以设置误判率0.05以内即可,优点占用的内存比较少,没有多余的key,布隆过滤器不支持删除操作。查询性能也不是非常高
布谷鸟过滤器
他的操作相当于是有两个hash算法,把新来的元素映射到对应的槽里面,倘若两个位置有一个位置是空,直接放进去,假如有一个有地方,就把原先有的给踢跑,踢到随机一个地方,然后假如下一个还有,就接着踢,当然他会有一个阈值,假如踢的过多,会导致效率过低,因此会进行扩容
缓存击穿:当热点的key设置的过期时间,恰好这个时间点对大量的key有并发的请求,这些并发请求可能会瞬间把DB压垮。
方案一:互斥锁:(setnx)
数据强一致性:性能差
热点key不设置过期时间,新增一个过期字段,维护该过期时间即可
逻辑过期
数据弱一致性:高可用,性能优
缓存雪崩:指大量的缓存key同时失效,或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案
给不同的key的TTL添加随机值
利用Redis集群提高服务可用性(哨兵模式,集群模式)
给缓存业务添加限流策略(nginx,或者spring cloud gateway)
给业务添加多级缓存Guava
小总结
穿透无中生有key,布隆过滤null隔离
缓存击穿过期key,锁与非期解难题
雪崩大量过期key,过期时间要随机
面试必考三兄弟,可用限流来保底
双写应用
redis作为缓存,mysql数据如何和redis进行同步呢(双写一致性)
当修改了数据库的数据,也要同时更新缓存的数据,缓存和数据库的数据要保持一致
读操作:缓存命中,直接返回,缓存未命中查询数据库,写入缓存,设定超时时间
写操作:延迟双删:删除缓存,再去操作数据库
以下这两种情况都会导致无法一致性
先删除缓存,再去操作数据库
此时缓存还是10
正常是应该线程1保持串化
先操作数据库,再删除缓存 ,过期了
写操作:降低脏数据的出现风险(做不到决定强一致)
延迟双删
为什么延时双删,因为一般情况mysql是主从的,需要从主节点同步到从节点。(这种情况也不是很好,因为他的操作,我们不是非常容易控制时间)
读多写少,利用读写锁进行控制(强一致性)
共享锁:readLock,加锁之后,其他线程可用共享读操作
排他锁:独占writeLock也叫,加锁之后,可以阻塞其他线程进行一个读写操作
互斥锁(强一致性>延迟双删)
异步通知:使用延迟一致的业务
使用MQ中间件,更新数据之后,通知缓存删除
利用canal中间件,不需要修改业务代码,伪装成mysql的一个从节点,canal通过读取binlog数据更新缓存
redis持久化问题
RDB(Redis数据备份文件)也叫做Redis快照,把内存中所有的数据都记录到磁盘,当Redis实例故障重启之后,从磁盘读取快照文件,恢复数据
save:由Redis主进程来执行RDB,会阻塞所有命令
bgsave:开启子进程RDB,避免主进程受到影响 (redis.conf里面有触发RDB机制)
save 900 1 900s内,至少一个key被修改则bgsave
RDB执行原理:
AOF:Append Only File(追加文件),Redis处理的每一个写命令都会记录在AOF文件,默认AOF关闭
Redis也会在触发阈值时候自动重写AOF.
AOF是记录命令,AOF文件比RDB文件大的多,AOF会对同一个key多次写操作,但是只有最后一次操作才有意义,执行bgrewwriteof命令,可以让AOF文件执行重写功能,用最少的命令达到相同的效果。(通常结合二者来使用)
Redis过期之后,会立即删除吗
惰性删除:设置该key过期时间后,不去管他,当需要这个key时候,检查是否过期,过期就删除 (对cpu友好,不会浪费时间检查一些用不到的key,缺点就是对内存不友好,一个key过期但是没一直用,那么内存永远不会释放)
定期删除:每个一段时间,对一些key进行检查,删除了吗过期的key,
两种模式:
SLOW:定时任务,执行频率,默认是10hz,每次不超过25ms,通过配置文件redis.conf的hz来调整次数
FAST:执行频率不固定,但是两次间隔不低于2ms,每次耗时不超过1ms.
优点:限制删除操作的执行时长和频率减少删除操作对cpu的影响,另外定期删除,也能有效释放过期键占用的内存
缺点:难以确定删除操作执行时长和频率
Redis:两者结合
Redis数据淘汰策略
LRU:最近最少使用,当前时间剪去最后一次的访问时间,值越大,淘汰优先级越高。
LFU:最少频率使用,统计每个key访问频率,值越小,优先级越高
当Redis的内存不够用时候,此时向redis添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称为内存的淘汰策略
Redis支持8种不同策略来选择删除的key
noeviction:不淘汰任何key,但是内存满时候,不允许写入新数据,默认就是这种策略。
volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小,越先被淘汰
allkeys-random:对全体key,随机进行淘汰
volatile-random:对设置TTL的key,进行随机淘汰
allkeys-lru:对全体key,使用LRU算法进行淘汰
(LRU:最近最少使用,用当前时间减去最后一次访问的时间,值越大,淘汰的优先级越高
LFU:最少频率使用,统计每个key的访问频率,值越小淘汰优先级越高。)
volatile-lru:对设置TTL的key,基于LRU算法进行淘汰
allkeys-lfu:对全体的key,基于LFU算法进行淘汰
volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰。
1.对业务没有啥要求,充分利用LRU,如果业务有明显冷热区分,建议使用。
(有时候,不能按照频率判断,比如说过年和平时来比)
2.业务访问频率相差不大,没有明显冷热数据之分,使用allkeys-random随机选择淘汰。
3.如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据
4.如果业务有短时间,高频访问的数据,可以使用allkeys-lfu或者volatile-lfu策略。
数据淘汰策略的其他问题
假如数据库1000万数据,Redis只能缓存20w数据,如何保证Redis中数据都是热点数据
使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据。
Redis的内存使用完了发生什么?
主要看数据淘汰策略是什么,如果默认配置的(noeviction,不删除任何数据)会直接报错。
Redis分布式锁 如何实现?
集群情况下的定时任务
在集群的情况下不能加synchronized,因为synchronized属于JVM,一个集群享受一个JVM
分布式锁
Redission实现的分布式锁(setnx和lua脚本,脚本是串行的)
提供的分布式锁(redis提供了redission这个分布式锁,他会提供一个看门狗,每隔多少s看一下,如果还持有锁,延长生存时间)
setnx(set if not exists),这里通常设置了时间
假如不去设置时间,那么假设开始的时候,获取锁,但是还没释放,执行业务时候,突然宕机,这也就会导致其他线程无限阻塞。
那么我们该如何控制好时间呢,不能让业务执行一半去释放锁啊?
在redisson的分布式锁中,提供一个看门狗WatchDog(看门狗)->一个线程获取锁成功之后,WatchDog会给持有的线程续期(默认是每隔10s续一次)
redission实现的分布式锁利用hash结构记录线程id和重入次数
假如可以获取到,value+1,unlock()并非删除该锁,而是value-1;前提是要求同一个线程。
MYSQL篇
如何定位慢查询
调试工具:Arthas可以根据这个工具,查看方法执行时间
运维工具:Skywalking
方法二:Mysql自带慢日志
慢查询日志记录了所有执行时间超过指定参数(long_query_time,单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MYSQL的配置文件中(/etc/my.cnf)中配置如下消息
slow_query_log=1 #开启MYSQL慢日志查询开关 long_query_time=2 #设置慢日志的时间为2s,SQL语句执行超过2S,被视为慢查询,记录慢查询日志
如何优化慢查询
1.聚合查询
2.多表查询
SQL执行计划(找到慢的原因)
EXPLAIN或者DESC命令获取MYSQL如何执行SELECT语句的信息
possible_key:当前sql可能会使用到的索引
key:当前sql实际命中的索引 -
key_len:索引占用的大小 -通过这两个查看是否有可能命中索引
Extra额外的优化建议
SQL优化:
避免select * 在表查询中,需要哪些字段必须明确写明,不许写*,
需要将*号转化成表的所有字段,然后再查询,他会增加查询解析器成本
一般*不走覆盖索引会产生大量回表查询
文本数据,大字段数据传播,增加网络消耗
小表驱动大表,
join:将多张表通过特定条件连接起来,避免使用join,要使用也是小表驱动大表(当然也很大一部分是维护不好维护,难度会很大)
student表前15条数据读到join buffer.
然后scores表去匹配join buffer中前15条
记录一下匹配结果
清空join buffer
再把student表后15条读取join buffer中,然后再用scores表去匹配join buffer中后15条,记录一下匹配结果
连接查询代替子查询(第一种通过连表)
提升group by的效率:使用group by的列没有索引,查询有可能会很慢,因此可以创建一个或者多个索引加速查询
select remarks from scores group by remarks
使用limit
提高查询速率:一个查询返回成千上万的数据行,不仅占用了大量的系统资源,也会占用更多的网络带宽,影响查询效率,使用limit可以限制返回的行数,减轻系统负担,提高查询效率
避免过度提取数据:从数据库中提取大量数据,可能导致系统崩溃,使用limit可以限制提取的数量,避免过度提取数据,保护系统不受影响
优化分页查询:分页需要查询所有的分页数据,这会浪费大量的系统资源和时间,使用Limit优化分页,可以只查询需要的数据行,缩短查询时间,减少资源浪费
减少交互次数,减少服务器CPU以及内存开销。
了解过索引吗,什么是索引
帮助Mysql高速获取数据的数据结构,在数据之外,数据库系统还维护着满足特定查找算法的数据结构(B+树),这些数据结构是以某种方式引用(指向)数据,这样可以在这些数据结构中查找高级查找算法,这种结构就是索引。
二叉树(二叉搜索树,不太稳定)红黑树(如果存了1000w,会特别高,所以会导致效率也不高)
Innodb-B+树在BTree优化,更适合外存存储索引结构,Innodb存储引擎就是B+Tree实现其索引结构。
区别一:B树(特征:大的在左边,小的在右边,根据这个规则快速检索到其中想要的,B树把数据绑定到了上面)
区别二:B+树只放在了叶子结点的地方,第二点区别B+树的叶子结点使用了双向链表.
区别三:由于B+在上面绑定了数据,导致,他的树的节点,并非全放到叶子结点(B+树可以更宽,这样他就可以更快速的地位地方,树的高度决定检索的次数,越高,检索次数越多,导致耗费时间越多
B+的双向链表,方便去遍历,往后查即可(范围查询更加优越,双向链表,而不是B树需要回到上面,再来扫描)
B树与B+树相比较:
磁盘读写代价B+树更低
查询效率B+树更加稳定
B+树便于扫库和区间查询,B+树磁盘读写更低,查询效率B+树更加稳定,B+树更适合去扫库和区间查询,磁盘读写B+的代价更低,非叶子结点只是存储指针,叶子存储数据
叶子结点是双向链表,便于扫库和区间查询
聚簇索引,二级索引
聚簇索引:将数据存储和索引放到了一块,索引结构的叶子结点保存了数据,必须有,且只有一个
二级索引:将数据与索引分开存储,索引结构的叶子结点关联的是对应的逐渐,可存在多个
聚集索引:id
二级索引的话,假如给name添加索引,他的叶子结点,下面是主键而不是整行
回表查询:通过二级索引,找到主键,然后通过聚簇索引(主键)去找整行数据,这个过程就是回表
5-8
左边比5小,中间的就是中间位置,右边比8大
覆盖索引:查询使用了索引,并且需要返回的列,在该索引中已经全部能找到
假如命令select id,name from user_name='kit';此时通过上面的查询数据就可以直接全部查到,就叫覆盖索引
那么什么不是覆盖呢
select*from id,name,gender from user='kit'; 此时这块就不会查找到所有,他会查到主键,然后从聚簇索引那么拿一整列,这就不是覆盖索引
超大分页查询,如果执行limit 90000000 ,10,此时需要MYSQL排序前9000010记录,仅仅返回9000000-900000010的记录,其他记录丢弃,查询排序的代价非常大。
MYSQL:通过创建覆盖索引,能够比较好的提高性能,可以通过覆盖索引加子查询形式进行
MYSQL超大分页一般是创建覆盖索引能够比较好的提升性能,可以通过覆盖索引+子查询的形式进行优化(数据量特别大时候,limit分页查询,需要对数据进行排序,效率低)
索引的创建原则
主键索引,唯一索引,业务需要的索引
数据量大,且查询比较频繁的表建立索引(单表数据超10万数据)
针对常查询条件(where),排序(order by),分组(group by)操作的字段建立索引
尽量选择区分度高的作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高。
如果是字符串类型的话,就选择字段特点,建立前缀索引
事务相关 -事务特性(ACID)
原子性:事务时不可分割的最小操作单元,要么全成功,要么全失败
一致性:事务完成时,必须使所有的数据都保持一致
隔离性:数据库系统提供的隔离机制,保证事务不受外部并发影响的独立环境下运行
持久性:事务一旦提交或者回滚,他对数据库里面的数据改变就是永久的。
并发事务的问题,以及隔离界别
并发事务的提交:脏读,不可重复读,幻读
脏读:一个事务读到另外一个事务还没有提交的数据
不可重复读:一个事务先后读取同一条数据,但是两次读取的数据不同,称为不可重复读
幻读:一个事务按照条件查询数据时候,没有对应的数据行,但是在插入数据时候,发现这行数据已经存在,如同幻影。
隔离级别:读未提交,读已经提交,可重复读,串行化
未提交读:一般不用
读已提交
可重复读:可以解决脏读和不可重复读(性能和安全,默认情况,解决的大部分的幻读)
串行化:很安全,可以支付啥的。
多版本并发控制(MVCC)
频繁加锁和释放锁会对性能产生影响,为了提升性能,Innodb与锁配合同时采用另一种食物隔离性的实现机制MVCC,即(Multi-Versioned Concurrency Control),多版本并发控制,用来解决脏读,不可重复读事务等读写问题,MVCC某些场景代替了低效的锁,保证隔离性的基础上,提升了读取效率和并发性(事务的隔离性是通过锁和MVCC共同实现的)
MVCC的实现是基于Undo Log版本链和ReadView来完成的,Undo Log做为回滚的基础,在执行Update或Delete操作时候,会将每次操作详细都记录在Undo Log中,每条Undo中都记录一个叫做roll_pointer的引用信息,通过roll_pointer就可以将某条数据对应Undo Log组织成一个Undo链,在数据行头部通过数据行中的roll_pointer与Undo_Log中第一条日志进行关联,这样就构成一条完整的数据版本链表
ReadView(一致性视图)
ReadView:每条数据链都构造好后,具体查询选择哪个版本,这里就需要使用ReadView结构来实现,所谓ReadView是一个内存结构,顾名思义是一个视图(事务使用select查询数据时候就会产生一个ReadView),里面统计该版本链的一些统计值
假设m_createor_trx_id=201事务
m_ids:当前所有活跃事务的集合(没有结束的事务)
m_low_limit_id:活跃事务集合中最小事务ID(如果Undo版本链中日志对应的事务ID小于该值说明日志对应的事务已经提交)
m_up_limit_id:下一个将被分配的id,也就是版本链头的事务ID+1(还没有创建的事务,对于当前事务来说是不可见的)
m_creator_trx_id:创建当前ReadView的事务ID(当前事务ID)
四步查找规则:
第一步:判断该版本是否为当前事务创建,若m_creator_trx_id等于该版本事务id,意味着读取自己修改的数据,可以直接访问,如果不等则到第二步
第二步:若该版本事务ID<m_up_limit_id(最小事务id),意味着该版本在ReadViews生成之前就已经提交,可以直接访问,如果不是则到第三步
第三步:或该版本事务Id>=m_low_limit_id(最大事务Id),意味着该版本在ReadView生成之后才是关键,所以肯定不能被当前事务访问,无需第四部判断,直接遍历到下一个版本,如果不是则到第四步
第四步:若该版本事务Id在m_up_limit_id(最小事务ID)和m_low_limit_id(最大事务ID)之间,同时该版本不在活跃事务列表中,意味着创建ReadView时该版本已经提交,可以直接访问,如果不是则遍历并且判断下一个版本。
MVCC是否可以解决幻读/不可重复读
首先对于幻读无法通过MVCC单独解决
不可重复读,在事务的第一个查询时,创建一个ReadView,后续查询都是用这个ReadView进行判断,所以每次查询结果都是一样的,从而解决不可重复读问题,在REPEATABLE READ可重复读,隔离级别就采用这种方式(可重复读的隔离级别下,整个事务周期只使用第一个查询所创建的ReadView)
如果事务每次查询都创建一个新的ReadView,这样就不会出现不可重复读问题,在READ COMMIT读已提交级别下就是这种实现方式
了解过分库分表吗
垂直分表:把不常用对字段单独放在一张表
把text,blob表等大字段拆分出来,放到附表中。
冷热数据分离,减少IO过度争抢,两表互相不影响
水平分表也是这个图:将一个表的数据拆分到多个表中
特点:优化单一表数据量过大,而产生的性能问题
避免IO争抢并减少锁表的几率
分库之后的问题: ->使用中间件可以面对下面分库分表会出现的问题 如MyCat
分布式事务一致性问题
跨节点关联查询
跨节点分页,排序函数
主键避重
具体拆分策略:
水平分库:将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
水平分表:解决单表存储和性能问题
垂直分库:根据业务进行拆分,高并发下提高磁盘IO和网络连接数
垂直分表:冷热数据分离,多表互不影响。
结合业务来分库分表
一个设备执行记录表:
一天存在早高峰和晚高峰(早上8点,晚上7点-对应起床和下班时间)
整点和半点存在极大值
数据是insert多,query少
假设一天产生100万数据,早晚高峰各产生20万,15万数据
分库分表策略:按照高峰期时段来分库分表,早晚高峰分贝存储在不同的数据库表中,非高峰时间段存储在不同的数据表中,这样可以避免低效的分库分表,比如按小时分,后半夜基本是没有的
或者按照数据量来分,定好规则,把规则,以数据量较大的时间段单独存储一个表内,数据量比较小的时间可以合并存储另一个数据库表中,方便更好的管理和查询数据。
微服务
Spring Cloud组件有哪些
Ribbon(瑞笨)Feign(f ei) Hystrix(嗨丝 吹克斯), Gateway
sentinel(森ten农)
服务注册和发现:eureka,nacos
服务注册:服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称,ip,端口等
服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用
服务监控:服务提供者每隔30s向eureka发送心跳,报告健康状态,如果eureka服务90s没收到心跳,从eureka中删除。
nacos与eureka的区别
共同点:都支持服务注册和服务拉取,都支持服务提供者心跳方式做健康检测
区别:
1)Nacos支持主动检测提供者状态,临时实例采用心跳模式,非实例采用主动检测模式
2)临时实例心跳不正常会被剔除,非临时实例则不会被剔除
3)Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
4)Nacos集群采用AP方式,当集群存在非临时实例时候,采用CP模式,Eureka采用AP方式
Nacos还支持配置中心。
nacos在心跳检测的时候,会有临时实例与非临时实例,假如是非临时实例:nacos注册中心会主动询问,nacos会主动推送变更消息push(假如服务地址变更).
服务雪崩
服务降级是服务的自我保护方式,或者保护下游服务的一种方式,确保服务不会收到突增影响变得不可用,确保服务不会崩溃。一般开发中与feign接口整合,编写降级逻辑。
Hystrix熔断机制:用于监控微服务调用情况,默认是关闭的,如果监测到10s内请求失败率超过50,出发熔断机制,之后每隔5s重新请求微服务,如果服务可达,则关闭熔断机制
数据结构
数组为什么查询效率是快
因为创建数组是创建的一堆连续空间,并且数组必须存放相同的数据类型,比如我们创建一个长度为10,地址从1000开始,那么假如存整数,一个整数四个字节,那么最后就是10xx,根据这个,我们可以很快的知道每个数组的地址,进行可以查找来修改其中的值
O(n)计数排序, 基数排序, 桶排序
如果数组的长度为N, 桶的个数为M, 假如这些元素是均匀落在每个桶里面的, 那么每个桶里面的元素个数为N/M, 每个桶里面排序为O(N/Mlog(N/M)), M个桶的排序为O(MN/M log(N/M)) = O(Nlog(N/M))
ArrayList源码分析
添加数据的逻辑,以及扩容的方式
构造方法:带初始化容量的构造函数,构建初始化容量,若是为0则创建空的数组。
无参是构造一个空集合
Collection把collection对象转换成数组,然后将数组的地址复制给elementData.
添加和扩容操作(第一次添加数据)
elementData是从无参数的构造来的,目前就是看他俩是否相等(第一次插入就会相等)
DEFAULT_CAPACITY:默认值是10
假如说这个容量大于初始值10了,就会触发扩容,
grow方法(扩容)
拿到当前数组容量,然后看下面部分,oldCapacity>>1相当于/2,扩容原先的1.5倍
假如说<0回第一次初始化数组长度即10,然后进行数组拷贝
假如第二次到第10次的情况,只要不超过10,就无需扩容
假如当前添加第11次数据:所以11往下传递,触发扩容
The maximum size of array to allocate. Some VMs reserve some header words in an array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
但是这里面的MAX_ARRAY_SIZE是什么呢,我一时间很好奇,然后搜百度这段意思,
即有些虚拟机会在数组中保存 header words 头部字。数组对象有些特殊,他需要额外存储数组元素长度在头部,少了8个长度可能跟这个有关,尝试分配大于 MAX_ARRAY_SIZE 长度的数组会导致 OOM (换句话说,超过了该虚拟机的数组长度限制)。
ArrayList底层实现原理
ArrayList底层使用动态数组实现
初始容量为0,当你第一次添加的时候,才会初始化容量为10。
ArrayList在扩容的时候是原来容量的1.5倍,每次扩容都要拷贝数组
添加数据的时候:
确保数组已使用长度(size)+1之后足够存在下一个数据
计算数组容量,如果当前数组已经使用长度+1后大于当前数组长度,则使用grow方法进行扩容,确保新增的数据有地方存储之后,将新元素添加到位于size位置上
返回并且添加成功true.
HashMap实现原理
底层是hash表数据结构,数组+链表+红黑
往HashMap中put元素时候,利用key的hashCode重新计算出当前对象的元素在数组中的下标
1.8之前是拉链法,创建一个数组,每一个格子是一个链表,假如遇到hash冲突,则将冲突的值加道链表中即可。
当java1.8解决哈希冲突有了较大的变化,当链表长度大于阈值8,并且数组长度达到64时候,变成红黑树
当放上元素时候有一个(n-1)&hash的一个算法
为什么HashMap扩容是两倍
因为假如当HashMap容量是16的时候,二进制是10000,那么n-1就是01111,因为他要进行一个与的操作,所以与hash值计算只需看对应哈希值即可,每一个值都不同,不会产生哈希碰撞,但是假如不是2的n次幂,那么n-1的二进制是01001,向里面添加同样的元素,结果大多数会碰撞,比如01001(如果不是2的n次,她-1操作会产生很多奇葩的0,这也会产生非常多的哈希冲突
倘若现在有一个文件20亿整形在磁盘上,如何对文件数据进行排序
他肯定要读到内存里面,20亿读不完,所以读一部分,相当于流式,构造一个最大最小堆,类似于优先级队列,然后把数据加起来,然后堆顶最小,之后如何如何/20亿做成分治
假如不重复的话,O(n)情况
可以考虑桶排序
桶内使用基于比较的排序算法进行排序(时间复杂度为N*log(N))
数组的长度为N, 桶的个数为M, 假如这些元素是均匀落在每个桶里面的, 那么每个桶里面的元素个数为N/M, 每个桶里面排序为O(N/Mlog(N/M)), M个桶的排序为O(MN/M log(N/M)) = O(Nlog(N/M))
HashMap常见面试题
二叉树查询时间复杂度O(logN)假如一条线这种就是O(N)最坏情况,变成了链表
public class TreeNode{
int val;
TreeNode left
TreeNode right
TreeNode(){}
TreeNode(int val,TreeNode left,TreeNode right){
this.val=val;
this.left=left;
this.right=right;
}
红黑树
自平衡的二叉搜索树,
节点要么是红色要么是黑色
根节点是黑色
叶子结点都是黑色的空节点
红黑树中的红色节点的子节点都是黑色
任一节点到叶子结点的所有路径都包含相同数目的黑色节点。
当添加何删除节点时候,如果不符合这五个性质,就会发生旋转来达到性质。
查找O(logn)添加O(logn)删除O(logN)
哈希表中最重要的一个数据结构:散列表,拉链法
散列表:又名哈希表,是根据key直接访问在内存存储位置值(Value)的数据结构,它是由数组演化而来的,利用了数组支持按照下表进行随机访问数据的特性。
假如100个人马拉松,编号1-100,如果要编程实现根据选手的编号迅速找到选手的信息
可以根据选手编号,来到数组中找到对应信息
此时不采用1-100的自然数尽心编号,编号设置一定规则比如2024ZHBJ101,此时不能之间作为下标,那么我们采用将key映射为数组下标的函数叫做散列函数,可以表示为hashValue=hash(key),散列函数计算得到的散列值必须是大于等于0的正整数,因为hashValue需要作为数组下标,如果key1==key2,那么经过hash得到的哈希值也必须相同。如果不等于,则两个也一定不能相等(但是不好实现)
这种情况就叫做散列冲突
指多个key映射到同一个数组下标位置,那么我们如何解决散列冲突呢?
拉链法
查找平均情况基于拉链法解决冲突时是O(1)
散列表可能退化为链表,查询的时间复杂度从O(1)退化为O(N)
将链表发中的链表改造为其他高效的动态数据结构,比如红黑树,查询为O(logN)
HashMap原理:底层使用hash表数据结构,即数组链表+红黑树
当我们往HashMap里面put元素的时候,利用key的hashCode重新hash计算出当前对象元素在数组中的下标
会产生哈希冲突,假如两个key相同,则覆盖原始值,如果key不同,则将当前的key-value放入链表或者红黑(红黑树的转换条件,链表长度大于8且数组长度大于64,因为如果这样遍历的时间会更长)
获取的时候,直接找到hash值对应下标,进一步判断key是否相同,从而找到对应值
jdk1.8之前时拉链法,但是他没有红黑树,就是一直插入链表,注意1.8之后,链表长度大于8且数组长度大于64才会变成红黑,扩容的时候(resize())红黑树拆分的树的节点小于临界值6个,则退化成链表。
HashMap的put的流程
JDK1.7中put原理
put方法会判断是空或者长度是不是0,如果是则inflateTable,对数组初始化
判断key是否为null,为空值遍历table[0]链表寻找是否存在key==null对应的键值对,假如存在则覆盖旧的value,同时返回旧的value值,否则调用addEntry完成插入
key不等于null,则根据key计算数据索引值,循环链表,判断该key是否存在,存在则覆盖旧的value,并且返回旧的value
默认的初始容量时16,默认的加载因子时0.75,扩容阈值==数组容量*扩容因子,假如容量到达16*0.75=12时候,就会出发扩容,
当我们创建HashMap时候,默认构造函数是默认的加载因子0.75
HashMap是懒加载,创建数组的时候并没有初始化数组
public HashMap(){ this.loadFactor=DeFAULT_LOAD_FACTORY
无参数的构造函数中,设置默认的加载因子时0.75
HashMap的扩容机制
扩容的流程:
resize:两个功能,第一个是初始化,后面是扩容,所以会在刚开始就去判断。
扩容之后会去更新容量和阈值,e.next是判断是否有下一个节点,假如没有接i单了,就重新计算新数组的下标,并且放进去,
HashMap的寻址算法,为什么数组长度一定是2的n次幂
HashMap寻址算法:计算对象的hashCode,
再去调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更均匀
最后(capacity-1&hash)得到索引
原因一:hash值哪里来的,调用了一个hash方法,调用一个h==key.hashCode()^(h>>16)
当前运算作用是让hash值更加均匀,减少hash冲突(扰动算法),
按位与运算,97%16和(16-1)&97相同,这个除法运算耗cpu没有按位与性能更好,且数组长度必须是2^n次幂,假如不是2的n次幂,那么你的(n-1)就不是全为1,会出现大量的哈希冲突,并且按照上面的按位与运算才能到达和那个除法一样的效果
原因二:扩容时候重新计算索引(hash)效率更好,hash&oldCap==0的元素留在原来位置,否则新位置=旧位置+oldCap
HashMap在1.7情况下,多线程死循环的问题
在数组进行扩容的时候,链表是头插法,进行数据迁移的过程,有可能导致死循环。
这里面会有一个循环,一个一个获取其中的元素,假如先去迁移A,再去迁移B,得到新数组下标i,e指向是需要迁移的对象,变量next指向的是下一个需要迁移的对象
1.7链表采用头插法,迁移过后
1.7初始状态
这样就会出现循环,当然JDK8扩容算法做出了调整,不再将元素头插,而是选择尾插,这样就会避免jdk7中死循环的问题。
算法
归并排序
核心思想,利用递归,先选择一个中间点mid,排序左边时候,又可以进行一个排序,然后去右边排序,在这一层,再去合并两个数组,这样这一层就有序,归并和快排基本相似,是比较相像的,
class Solution {
public int[] sortArray(int[] nums) {
//传递一个左区间,和一个右边区间
mergeSort(nums,0,nums.length-1);
return nums;
}
public void mergeSort(int[]nums,int left,int right){
if(left>=right){
return ;
}
//根据中间点划分区间
int mid=(right+left)/2;
//[left,mid],[mid+1,right]
mergeSort(nums,left,mid);
mergeSort(nums,mid+1,right);
//先把左右区间都给排序了,我们不要关注是否能排好,我们相信他能排好
//合并两个有序数组,用一个数组去存储对应的排序数据
int[]tmp=new int[right-left+1];
int i=left;
int j=mid+1;
int k=0;
//然后我们合并两个升序数组
while(i<=mid&&j<=right){
if(nums[i]>nums[j]){
tmp[k++]=nums[j++];}
else{
tmp[k++]=nums[i++];
}
}
//合并剩余的数值
while(j<=right)tmp[k++]=nums[j++];
while(i<=mid)tmp[k++]=nums[i++];
把存储的还给原来的数组。
k=0;
//注意这里,我们合并的区间是从left到right而不是只有一次,从0到那个最后的nums.length-1了
因为假如right-left假如是右边那段,因为nums的数量是够的,但是k是不够的(比如1 2 3 4 5)
3,5这个区间,tmp的个数不够,但是nums够,于是会一直加加
for(i=left;i<=right;i++){
nums[i]=tmp[k++];
}
}
}
快速排序
class Solution {
public int[] sortArray(int[] nums) {
//传递一个左区间,和一个右边区间
qSort(nums,0,nums.length-1);
return nums;
}
public void qSort(int[]nums,int left,int right){
if(left>right){
return ;
}
//想想什么是快速排序,三数取中 new Random.nextInt(n):随机返回一个值在[0,num)的int类型的整数,包括0不包括num,+left的原因就是假如区间【3,5】那么right-left+1是一个他的区间长度,此时加上偏移量,就可以到达他的目标位置
int mid=nums[new Random().nextInt(right-left+1)+left];
int i=left;
int l=left-1;
int r=right+1;
//三段区间[left,l],[l+1,r-1],[r right]
//为什么是<r,而不是right,我认为是这样的,我们规定的区间是r的右边必须都是大于mid的,那么r的右边都大于,我还需要管吗,是不是就不用管了,但是i怎么到r呢,是不是我们会自己开始遍历发现比mid大的,我们移动到右边,(可能你在想,万一后面那个也比mid大咋办,因为我们的i是不变的,换句话说,它是统计一个比mid大的数量那种意思,然后还是看这个元素是不是比mid大。这个就是这里的核心
while(i<r){
if(nums[i]<mid)swap(nums,++l,i++);
else if(nums[i]==mid) i++;
else swap(nums,--r,i);
}
[left,l] [l+1,r-1] [r right]
qSort(nums,left,l);
qSort(nums,r,right);
}
public void swap(int[]nums,int left,int right){
int tmp=nums[right];
nums[right]=nums[left];
nums[left]=tmp;
}
}
分布式
CAP和Base是分布式系统的理论
分布式事务方案的指导(分布式系统无法满足这三个指标。)
Consistency(一致性)-用户访问分布式系统的任意节点,得到的数据必须一致(数据同步)
Availability(可用性)-用户访问集群中任意健康节点,必须能够得到响应,而不是超时或者拒绝。
Partition tolerance(分区容错性,分布式系统节点通过网络连接,一定会出现分区问题) ,因为网络故障或者其他原因,导致分布式系统中部分节点与其他节点失去联系,形成独立分区
Tolerance(容错):当集群出现分区时候,整个系统也要持续对外提供服务,访问3时,先等待,等网络好了,在处理(不然不能同步,但是假如同步的话,双方的数据就会不一致)
保持高可用性,持续对外提供服务,就不能保持强一致性->AP
最终一致思想:各个分支事务分别执行并且提交,如果有不一致的地方,再去想办法恢复数据
强一致思想:各个分支事务执行完业务先别去提交,等彼此结果,而后统一提交或者回滚。
如果访问的数据强一致性,就要放弃高可用性->CP
BASE理论
Basically Available(基本可用):分布式系统在出现故障时,允许损失部分可用性,保持核心可用
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是软状态结束之后,最终达到数据一致).
为什么使用MQ
因为正常来说一个人要发送是要确定发送人是谁的
但是这块发送只需要看住MQ即可,你发送和取都是从MQ里面。
场景题
统计用户在线时长
登入时间,退出时间
1.在线主动下线
2.离线(主动退出,红温后台退出)
异常的退出流程挂在后台,一般采用每个xx秒,发一个心跳包,xx秒后没收到心跳包就判定为下线(业务问题)
使用一个redis中,一个判定下线(扫描超过3分钟没心跳的)-集合-时间范围筛选-zset
zset key用户 score:时间戳,ZrangeByScore如果说最近有心跳,可以考虑把他移走,zrem()移除,心跳超时时间,任务执行这个频次,下线需要在最后一次心跳基础上再加上 上一轮心跳的时间。(取t做时间判定,用户可能在下一个心跳来之前有行为产生)
基于redis过期时间的监听
比如key(user,用户标识 expire_time=3min)对该redis的key进行续期,当过期时间超过3min。监听器捕捉事件,并且处理计算(但是监听器并不稳定,不要过度依赖redis的过期监听,过期时间是当redis删除这个之后才行的)
分布式多个线程,可能有多个监听器,有重复消费的问题,业务逻辑不能重复被执行
JVM篇章
JVM跨平台原理:不同操作系统的JVM是不一样的,这才是JVM跨平台的本质,
.java-> .c-> 执行JVM
java代码,字节吗,机器指令 (操作系统执行机器指令,把java代码编译成字节码(编译时候的时间,他是编译时间省去了运行的时间)去翻译成机器指令会比较快)
OOM哪些区域会出现
栈区,堆区,调用方法时候会调用这个栈帧,栈帧压到栈里面(无限调用,无限递归),堆溢出(强引用,什么new 对象,ThreadLocal)
1.7,1.8区别 方法区可能会出现,方法区以前在堆里面,后来移动到本地内存,存储类的源信息,后来随着项目越来越大,慢慢就会把堆撑爆了,不会占用堆的内存了。
JVM三大问题
一、JVM内存区域划分
首先来到第一个问题:什么是JVM呢?
JVM可以说是java的虚拟机,也可以叫做java的一个进程,每一个java进程都是JVM实例
堆,栈,方法区,程序计数器
成员变量->堆
局部变量,(引用类型也包括)->栈
静态变量->方法区
一个进程的运行过程中,就要从操作系统这里申请一些内存资源,JVM也是如此,搞了一大块内存供java代码执行的时候使用,JVM吧这一块内存,又划分出几个区域,作为不同用途。
二、JVM类加载机制
把类从硬盘文件加载到内存中
JAVA程序,最开始是写.java文件,编译成.class文件(字节码),运行java程序,JVM就会读取.class文件,把文件的内容放到内存中,并且构造成.class对象(类对象)
1.加载
找到.class文件,打开文件,读取文件内容,并且尝试解析格式
在JAVA代码中是直接的使用类
2.验证
验证当前.class文件是否符合要求
介绍一下上面的字段意思:
access_flags(相当于是不是public)
attribute注解
minor_version:小版本号
constant-pool:常量池
java文件里写的信息有什么,.class文件有所体现。
3.准备
给类对象分配内存,最终的目标就是构建出完整的类对象,分配内存+初始化。
4.解析
主要是初始化类对象涉及到一些字符串常量,其实字符串常量在.class文件就有了,直接读到内存中就行了。
常量池内符号引用,替换为直接引用的过程。(相对位置,经过偏移换到真实的内存地址)
相对位置是什么意思:我们去看电影,我知道电影的位置吗,不知道,因为我不知道他那个厅是怎么样的,但是我知道我坐在小美的旁边。
5.初始化
对类对象进行更具体的初始化操作,初始化静态成员,初始化静态代码块,加载父类
双亲委派模型(常考)
好处:避免类的重复加载,安全,唯一,方便管理
- 避免类的重复加载,确保一个类的全局唯一性。
- 保护程序安全,防止核心 API 被随意篡改。
- 通过合理使用类加载器,可以更好地组织和管理代码,减少冲突和错误
描述在类加载过程中,如何寻找.class文件。java圈子喜欢高大上的名字,比如自动装箱拆箱(其实不过也只是一个类型转换),
JVM加载.class文件的时候需要用到类加载器模块,JVM带了三个类加载器
Bootstrap ClassLoader
负责加载标准库的类,JAVA有标准文档,描述了都要提供哪些类,
Extension ClassLoader
负责加载JVM扩展的库,除了标准库之外,实现JVM厂商,还会添加一些类
Application ClassLoader
负责加载第三方库,像之前用到的mysql,jdbc,servlet(自己代码中写的类)
此处父子,不是父类子类,继承,而是对象有一个parent引用,指向父类类加载器实例
1. 从Application ClassLoader开始,但是他并不会立即搜索第三方库的目录,而是把加载任务委派给父亲,让父亲先加载
2.到了Extension ClassLoader,也不会立即搜索扩展库目录,也是把加载任务委派给父亲,也让父亲先尝试加载。
3.到了Bootstrap ClassLoader,也不想立即搜索标准库,而是也想把任务给父亲,但是他没有父亲,只能自己动手来搜索了
如果找到了这个类,会进行后续的加载(也就和Application和Extension没关系了)没找到,则把任务还给孩子,给Extension完成
4.任务再次回到Extension ClassLoader手上,他就要搜索扩展库的目录,看没有匹配的,.class文件找到,走,没找到就给他的孩子
5.任务回到了Application ClassLoader,就要搜索第三方库的目录(往往是你的项目目录,以及和jvm一些配置项有关-classpath有关系 找到了,就进行后续的加载,找不到,就要抛异常)
类加载中,更重要,更关键的是针对.class文件的解析校验。
类加载的格式,类卸载
一个类,什么时候会被加载呢?(懒汉模式 当我用到了才会加载)
1.构造类的实例
2.使用了类的静态方法/静态属性
3.子类的加载会触发父类
类加载后,后续就不必加载了
类卸载(把类对象干掉)
属于是特殊情况
一般来说
一般来说类加载过后就不必考虑卸载。一直保存到程序运行结束
但是有的特殊场景可能用到卸载操作
有的服务器需要打,“热补丁”
代码有bug,正常操作是修改代码,重新编译,用新的版本来去代替旧版本,重启服务器,有些特殊情况,服务器不方便重启,可以通过打补丁的方式,通过一些特殊手段,把需要替换的类给卸载掉,直接用加载好的类去替换(新版代码)
有些情况不方便去重启服务器,就可以通过“打补丁”的方式把需要替换的类替换掉,直接用加载好的类卸载掉,直接用加载好的新的类替换(新版代码)
热“并不需要重启,也不需要重新编译”
冷“不需要重新编译,但是需要重启“
JAVA这里用的补丁较少,游戏可能会多一点,比如不停服更新
三、垃圾回收(GC)
于是JAVA引入了垃圾回收机制,自动去判定,某个内存是否会被继续使用(如果不会就把这个内存当成垃圾)
垃圾回收,回收的是对象(以对象为单位,判断对象是不是垃圾)
JVM有好多内存分区,那么GC回收的是哪里的对象呢?
栈首先不需要GC去回收,栈里面包含很多栈帧
栈不需要GC回收吗,栈里包含很多”栈帧“,一个栈帧对应一个方法,该方法执行结束,此时这个栈帧就销毁了,栈帧上的局部变量啥的自然销毁。
程序计数器同理,线程销毁,自然也跟着销毁
方法区,类对象,很少会涉及到对象的卸载.
堆才是GC的主要战场
具体垃圾回收GC步骤
1.判定对象是否为垃圾
判定对象是否是垃圾的方式->看是否有引用指向他
方案1:引用计数(不属于是java的方案,java并没有使用该方案)
给这个对象安排一个计数器,每次有引用指向他它,就把计数器+1,每次引用被销毁,计数器-1,当计数器为0的时候,意味着该对象就是垃圾了。
下列代码是对应的过程。
引用计数的两个明显的缺陷:
1.空间利用率比较低,浪费更多的内存空间
如果给引用计数分配了两个字节,对象本体才四个字节的话,引用计数就浪费了50%的空间,如果代码中都是这些小对象,并且数量众多,此时浪费非常明显了就。
2.可能存在循环引用的问题,导致对象不能被正确识为垃圾
如下图,类似于死锁这种
方案2:可达性分析(JAVA实际采用的方案)
JVM首先会从现有代码中能直接访问到的引用出发,尝试访问遍历所有能访问的对象,只要对象能访问到,都可以标记成可达。完成遍历之后,可达之外的东西,也就是不可达,可就是垃圾咯
更多的是看能不能到达,不能到达,就给你置空
从哪些对象开始出发呢
1.栈上的局部变量
2.常亮池里的引用
3.方法区的静态成员
2.释放对象的内存
1.标记-清除(直接释放)
遍历所有可达对象标记为存活状态,然后遍历堆内存,把没有被标记的对象视为垃圾进行清理。
这种问题,假如不去处理,还是挺严重的,内存碎片随着程序的运行越来越多,越来越碎,内存越来越难申请。
2.复制算法
把内存分成两个区域,复制算法,通过冗余的内存空间,把有效的对象复制到另一部分空间,来避免内存碎片,这样每次都是连续的没有碎片
把一个内存,分成两份,用一份,丢一份,把左侧区域中,有效的对象,复制到右侧,接下来就可以使用右侧区域了,用了一段时间之后,也会有很多对象,也是相同的道理,把有效对象复制到左边,把右侧区域统一释放
缺点:对象重复复制有效率问题,额外占用内存空间
3.标记整理(压缩)
顺序表删除元素:搬运,然后区清理边界外的内存区域,从而消除碎片
实际有用的只有1,3,5
把没用的都迁移到后面,然后后面的元素进行删除。
4.JVM的垃圾回收机制
于是,设计JVM的大佬研究出了一个方法,集百家之长
按照对象的年龄,来制定不同的回收策略
GC是周期性进行扫描,每个对象没经过一轮GC,就称为涨了一岁。
新生代Eden区,两个Survivor区。新生代采用的是一种高效的复制算法,进行垃圾回收它可以快速完成内存回收减少暂停时间。(大量对象短暂存在,可以有效处理大量短暂对象的分配和回收,可以针对不同生命周期的对象采用不同的回收策略。
有一个阈值,如果对象一直没被回收,就会从新生代,调用幸存区/老年代(大对象也是)
新生代的扫描频次是比较高的,老年代的扫描频率就降低了
但是上述情况中,还有一个特殊的情况“如果这个对象的体积特别的大”会直接进入老年代(大的对象不适合复制算法)
Serial 串行化垃圾回收器,比较适用于单核处理器,或者对响应时间要求不高的场景
Parallel 基于多线程并行垃圾回收器,适合高吞吐量的服务器应用或者CPU核心🌲比较多的服务器环境
CMS(Concurrent 并发 Mark Swap 标记清楚器) 只是对老龄代进行垃圾回收, 并行垃圾回收,但是他把垃圾回收分成四个阶段,尽可能的减少STW(stop the world)的时间,比较适用于高交互性的应用,比如Web服务器,以及对停顿时间有严格要求,但是吞吐量相对比较宽松的场景(Serial CMS并发收集失败后备选回收方案)
初始标记,标记GC能直接接触到的对象,遍历整个对象图
G1 划分成多个大小相同的区域,每个区域都可以独立作为年轻代和老年代的一部分,通过并行和并发实现垃圾回收,从而减少停顿时间,另外它还能根据目标停顿时间来动态调整垃圾回收策略,来满足不同应用的需求(标记清除算法)
初始标记STW->重新标记(垃圾回收线程在扫描垃圾,他会产一些丢和漏的问题,重新扫描的修正,最后并发的清除)
Po,Vo是什么意思
PO是持久化对象
PO表示持久化对象,用于数据库的的实体和或表映射,与数据库字段对应,进行持久化操作
Vo值对象
Vo是值对象,用于封装数据,通常是不可变的,用于传递数据,不包括业务数据,VO可以用于在不同层之间传递数据
Bo业务对象
Bo是业务对象,用于封装业务逻辑和操作,包含业务相关的方法和属性,用于实现业务规则和操作,意思就是对象里面有函数,应为包含业务的操作
例如:UserBO在包含了属性id和username的基础上还包含了对username的验证逻辑
// 包含业务逻辑
public class UserBO {
private Long id;
private String username;
public boolean isValid() {
return username != null && !username.isEmpty();
}
}
Redis分布式锁需要注意哪些问题
setnx,我们需要注意,他会自己有一把锁,假如此时redis突然宕机了,那么这个锁不会释放,就会导致一致占用资源,所以我们需要引入时间
直播弹幕设计
消息的及时性强,过期消息的意义不大(电竞比赛,高光时刻)
用户松散,随时来,随时走
可能有瞬时大批量弹幕
流量特点:读多写少
来个纯粹读写DB,发弹幕,入库,看弹幕给时间窗口轮询消费
假如后续过多数据的话,可以考虑redis为主,写redis的zest,时间戳是score,异步刷数据库,读取,给时间窗口,ZRANGEBYSCORE根据分数的范围轮询找数据
缓存回源:缓存中不存在某个数据时,需要从数据库等获取数据,从新缓存的过程
问题:redis的系统为主的问题
1.数据库宕机,会丢失数据
2.redis宕机的话,降级回源会缓存击穿(注意击穿是过期,穿透是数据库和缓存都没有
3.及时性不够(短链接,拉模式)
此前短连接的方式,每次轮询请求后都要重新建立连接,
假如使用长连接轮询,客户端处于打开状态,就不会一直重新建立链接,减少重复资源的消耗,缩短响应时间
缺点:发送者和接受者可能没有连接到同一台服务器
HTTP协议的服务器通常是无状态的,如果使用负载均衡,接收到的弹幕可能并没有与等待接收消息的客户端保持长轮询连接
服务器没有好的方法判断客户端是否断开连接
长连接:WebSocket(HTTP支持全双工+长连接),Session(基于redis,通过用户回话来推)
4.热点直播间(csgo,王者,联盟,永劫,数据倾斜),大量弹幕,挑战Redis并发瓶颈
使用MQ消化瞬时弹幕,削峰;弹幕返回条数根据直播间大小自动调整(热门直播间对时间跨度以及消息条数做更严格的限制)
5.Redis重复请求多,刷刷免费虎粮,小啤酒
对弹幕读请求,使用local cache缓存最近5s的数据在应用服务的内存(过期我再去回源redis)(新问题:直播间变多,本地内存使用量膨胀,本地cache命中率下降,还可能频繁出发GC
解决:只去针对热门直播间使用本地cache
使用一致性hash,控制同一直播间尽可能打到同一台服务器,降低本地cache使用量。
最后还是Redis的zset
发弹幕的话直播间ID,timestamp
拉取弹幕:ZRangeByScore定时轮询(秒级,准实时即可)
问题:弹幕无法持久化
假如支持弹幕回放,直播结束之后,可以使用MYSQL直播结束之后迁移/异步刷入(这个可能更好,监听redis日志刷,发MQ消息)
Write-Behind,这种以Cache为主的模式,可能会丢数据
DB挂了,少写数据,但是只要数据还在redis就能够追回来
redis挂了,服务器感知后降级,请求回源,缓存击穿、
优化:热门直播间
每个直播间指标采样
标准:粉丝数,在线粉丝数,是否有活动
弹幕系统需要与直播间系统隔离
请求时候带上直播间的热门标识
根据服务器机器资源来分配所承载的热门直播间
IO通信(多路复用模型)
IO多路复用(起初,我们是TCP连接一对一,效率比较低,此时我们改进(起初多线程,多进程,一个TCP请求,一个进程10万级还可以,但是不是很好的解决方式,负载也很高),IO回去限制效率,IO多路复用的话相当于单线程),redis单线程处理多个IO请求,不存在什么加锁,同步啥的问题,纯是串型化。IO多路复用三个函数:
Select监听请求,把所有socket请求做一个请求,放到内核态,然后找有没有需要响应socket请求,然后把她拷贝到用户态。再做一次遍历,找到真正的socket.(拷贝两次 ,以及集合的一整个遍历)
poll、
epoll
设计模式
单例模式
单例模式:只能有一个对象,开始要给构造器做私有化,双重检查判断对象是否创建,然后
1.考虑是不是懒加载
2.考虑线程安全
3.反射破坏(是否可以),动态调用一些内存信息(枚举类型无法被破坏,但是不是懒加载);
public class Cat{
private cat(){};
private static Cat cat=null;
public static Cat getCat(){
if(cat==null){
cat=new Cat();
}
实例对象是第一次被调用的时候,才开始加载的,因为有的对象开销比较大,万一没被调用过,就直接加载
return cat;
}
这个线程不保证安全,那么如何使他线程安全,使用synchronized即可
public static Cat getCat(){
if(cat==null){
cat=new Cat();
}
return cat;}
但是这么写会产生新的问题,只有在对象在构建的时候才同步线程,如果上面那么写,就是每次获取对象时候就要进行同步操作,对性能影响大,我们只要在编译构建对象,运行时候调用,就不用考虑线程安全
private static Cat cat=new Cat();
这样不就不是懒加载了吗,所以还是要改,原因就是多了个synchronized
所以采用,
public static Cat getCat(){
if(cat==null){
synchronized(Cat.class); 这样所有的执行第二步都可以直接跳过,解决了低效的问题。
cat=new Cat();
}
return cat;}
但是问题又来了,会有很多线程已经进入if代码块,然后他们在不断等待,这样对象也会创建多次
因此我们使用这样
public static Cat getCat(){
if(cat==null){
synchronized(Cat.class); 这样所有的执行第二步都可以直接跳过,解决了低效的问题。
if(cat==null){ //这里再判断就是,其他线程等待第一个线程完事之后,又去检查发现他是不是空
cat=new Cat();
}
}
return cat;}
以上是开发阶段常用的双检锁,很常见的同步线程手段,但是还有有个happens-before可见性问题
cat=new Cat();这个并非是一个原子性,
在实际被分为三步
memory=allocat//分配内存
ctorinstance(memory)初始化对象
install=memory//对象指向内存地址
因此最后的最后在这块加上volatile
RabbitMQ消息队列
RabbitMQ如何保证消息不丢失
异步发送(验证码,短信,邮件)
MYSQL和Redis,Es之间的数据同步
分布式事务
削峰填谷
绝大多数需要MQ有高可用性
生产者确认机制:RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失,消息发送到MQ以后,会返回一个结果给发送中,表示消息是否处理成功
消息失败后如何处理呢?
回调方法即使重发
记录日志
保存到数据库然后定时重发,发送成功后即可删除表数据
MQ默认时内存存储消息,开启持久化功能可以确保缓存在MQ中的消息不丢失
1.交换机持久化
2.队列持久化
3.消息持久化,SpringAMQP中消息默认是持久化的,可以通过MessageProperties里面的DeliveryMode来指定。
消费者确认:
消费者处理消息后可以向MQ发送ACK回执,MQ收到ACK后会删除该消息
SpringAMQP允许配置三种确认模式
manual:手动ack,需要在业务代码结束后,调用api发送ack
auto:自动ack,由spring检测listener代码是否异常,没异常返回ack,有则抛出异常返回nack
none:关闭ack,MQ假定消费者获取消息会成功处理,因此消息投递后立即被删除
我们可以利用Spring的retry机制,消费者出现异常利用本地重试,设置重试次数,当次数够了,消息依然失败,将消息投递到异常交换机,给人工处理
RabbitMQ消息的重复消费问题如何解决的
网络抖动
消费者挂了
业务唯一表示:支付id,订单id,文章id等(每条消息设置位置的表示id,幂等方案[分布式锁,数据库锁(悲观锁,乐观锁)
RabbitMQ中死信交换机(RabbitMQ延迟队列了解过吗)
延迟队列:进入队列的消息会被延迟消费的队列
场景:超时订单,限时优惠,定时发布
当队列中消息满足下列情况之一,可以成为死信
消费者使用basic.reject或者basic.nack声明消费失败,并且消息的requeue参数设置为false
消息是一个过期消息,超时无人消费
要投递的队列消息堆积满了,最早的消息可能成为死信。
如果该队列配置了dead-letter-exchange属性(指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机DLX)
TTL:存活时间如果队列中消息TTL结束未被消费,则会变成死信,ttl超时分为两种情况
消息所在队列设置了存活时间
消息本身设置了存活时间
延迟队列有了解过吗
延迟队列插件可以取实现延迟队列
或者(死信交换机和TTL)实现的,过了TTL,则放入死信交换机(因为这个消息就是死信了)
RabbitMQ如果有100万的消息堆积,如何解决(消息堆积如何解决)
生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,知道队列存储消息达到上限,之后发送的消息就会变成为死信,可能呗丢弃,这就是消息堆积问题
解决消息堆积三种思路:
增加更多消费者,提高消费速度
在消费者内开启线程池加快消息处理速度
扩大队列容积,提高堆积上限(惰性队列)
引入惰性队列:
接收到消息之后之间存入磁盘而并非内存
消费者要消费消息时才会从磁盘中读取并加载到内存
支持数百万条的消息存储
RabbitMQ高可用机制
生产环境,使用集群保证高可用
普通集群(标准集群):集群每个节点共享部分数据,包括交换机,队列元信息,不包含队列中的消息
镜像集群:本质主从模式(具备以下特征):交换机,队列,队列中消息会在各个mq的镜像节点之间同步备份,一主多从,所有操作都是主节点,然后同步给镜像节点(但是假如主从同步完成之前,主就已经宕机了,会出现数据丢失)
那么丢数据如何解决
仲裁队列(用来代替景象集群):主从模式,主从同步基于Raft协议,强一致性(即选择投票主节点),声明队列时候加一个参数即可