学习题目

 

一.Java虚拟机

1.AQS原理

AQS内部数据和方法,可以简单拆分为:

  • 一个volatile的整数成员表征状态,同时提供了setState和getState方法private volatile int state;

  • 一个先入先出(FIFO)的等待线程队列,以实现多线程间竞争和等待,这是AQS机制的核心之一。

  • 各种基于CAS的基础操作方法,以及各种期望具体同步结构去实现的acquire获取/release释放方法

2.hashMap解决hash冲突,有哪几种方式

https://www.pianshen.com/article/4632297834/

  • 链表法:当hash冲突,对象形成一个链表;jdk1.8之后,如果链表对象大于8个,变为红黑树

  • 再hash:如果hash冲突,再hash

  • 开放寻址法:冲突之后,再重新找一个位置插入(比如:下一个位置,或者平方等)

3.hashMap多线程调用,会有什么问题?为什么?

https://blog.csdn.net/weixin_42373997/article/details/112085344

https://mp.weixin.qq.com/s/yxn47A4UcsrORoDJyREEuQ

  • hashmap是非线程安全

  • jdk 1.8之前,扩容的时候哈希槽里的链表采用头插法,会导致环链,进而导致cpu打满;1.8之后采用尾插法可以避免

  • 但是1.8之后,插入节点采用尾插法,并发时会有数据覆盖的问题,因为不是线程安全的

头插法:

  1. 顾名思义就是每次插入元素都是在头部

  2. put C的时候,比如原来是A->B,那么插入之后:C->A->B

  3. 扩容时也是头插法:原来是A->B->C,先遍历到A先插入,后遍历B和C,所以假如扩容之后ABC还在一个哈希桶,那么就会变成C->B->A

为什么头插法在多线程并发的时候会引起环链

  1. A->B->C线程1扩容,首先插入A,然后A.next是B,然后插入B,所以此时就是B->A

  2. 线程2此时也要扩容,先找到A,A.next是B,然后B.next被线程1改成了A,那么A.next是B,B.next变成A了,环链,死循环 

 

4.synchronized原理,讲一下底层实现,jdk 1.6做了哪些优化

https://www.cnblogs.com/xckxue/p/8685675.html

https://www.cnblogs.com/deveypf/p/11406932.html

https://www.cnblogs.com/wuqinglong/p/9945618.html

synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个referentce类型的参数来指明要锁定的对象和解锁的对象。

在执行monitorenter指令时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

synchronized同步块对同一个线程来说是可重入的,不会出现自己把自己锁死的问题。

Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块,状态转换消耗的时间有可能比用户代码执行的时间还要长。所以synchronized是Java语言中一个重量级的操作。

锁优化:

自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁

  1. 偏向锁:偏向锁的作用就是,当一个线程再次访问这个同步代码或方法时,该线程只需去对象头的Mark Word中去判断一下是否有偏向锁指向它的ID,无需再进入Monitor去竞争对象了。当对象被当做同步锁并有一个线程抢到了锁时,锁标志位还是01,“是否偏向锁”标志位设置为1,并且记录抢到锁的线程ID,表示进入偏向锁状态。一旦出现其它线程竞争锁资源时,偏向锁就会被撤销。偏向锁的撤销需要等待全局安全点,暂停持有该锁的线程,同时检查该线程是否还在执行该方法,如果是,则升级锁,反之则被其它线程抢占。

  2. 轻量级锁:当有另外一个线程竞争获取这个锁时,由于该锁已经是偏向锁,当发现对象头Mark Word中的线程ID不是自己的线程ID,就会进行CAS操作获取锁,如果获取成功,直接替换Mark Word中的线程ID为自己的ID,该锁会保持偏向锁状态;如果获取锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。

5.零拷贝

参考文档:5 04 | 零拷贝:如何高效地传输文件?

传统IO:

零拷贝:

关于pageCache

 

6.concurrenthashmap

java 1.7 Segment继承了ReentrantLock,锁加在Segment上

ConCurrentHashMap与HashMap的区别,HashEntry的value、next是volatile

ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

size计算先采用不加锁的方式,如果连续两次计算结果相同,则返回;如果不同,需要给所有Segment加锁,再计算一次。(所以最多需要计算3次)

1.7与1.8的区别:CAS + synchronized来保证线程完全,抛弃Segment

ConcurrentHashMap putVal方法逻辑

  1. 如果没有初始化数组,初始化数组

  2. 定位到数组中某个位置,哈希桶为空,cas插入对象

  3. 判断是红黑树,还是链表

  4. 插入对象使用Synchronized保证线程安全

7.布隆过滤器怎么实现的

1.保存当要向布隆过滤器中添加一个元素key时,我们通过多个hash函数,算出一个值,然后将这个值所在的方格置为1。

比如,下图hash1(key)=1,那么在第2个格子将0变为1(数组是从0开始计数的),hash2(key)=7,那么将第8个格子置位1,依次类推。

2.判断是否存在

不存在:只需要将这个新的数据通过上面自定义的几个哈希函数,分别算出各个值,然后看其对应的地方是否都是1,如果存在一个不是1的情况,那么我们可以说,该新数据一定不存在于这个布隆过滤器中。

可能存在:如果通过哈希函数算出来的值,对应的地方都是1,那么我们能够肯定的得出:这个数据一定存在于这个布隆过滤器中吗?

   答案是否定的,因为多个不同的数据通过hash函数算出来的结果是会有重复的,所以会存在某个位置是别的数据通过hash函数置为的1。

布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在。

8.ArrayList&LinkedList

Vector是Java早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。

ArrayList是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与Vector近似,ArrayList也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector在扩容时会提高1倍,而ArrayList则是增加50%。

LinkedList顾名思义是Java提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的。

  • Vector和ArrayList作为动态数组,其内部元素以数组形式顺序存储的,所以非常适合随机访问的场合。除了尾部插入和删除元素,往往性能会相对较差,比如我们在中间位置插入一个元素,需要移动后续所有元素。

  • 而LinkedList进行节点插入、删除却要高效得多,但是随机访问性能则要比动态数组慢。

9.JVM内存模型

10.垃圾回收算法&回收策略

垃圾回收算法:标记清除算法;复制算法(from复制到to,然后修改指针,原来的回收);标记整理算法(标记完之后,将存活对象向一端移动,然后清理);分代收集算法。

cms收集器

 CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的。

运作过程:初始标记;并发标记;重新标记;并发清除。其中初始标记、重新标记这两个步骤仍然需要“Stop the world”。

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS优点:并发收集、低停顿。

CMS缺点:

1).CMS收集器对CPU资源非常敏感。其实 ,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量降低。

2).CMS收集器无法处理浮动垃圾,可能出现“concurrent mode failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就会产生新的垃圾,这部分垃圾出现在标记过程之后,CMS无法在档次收集中处理,只好下次清理,这部分垃圾就交浮动垃圾。

3).CMS是一个基于“标记-清除”算法实现的收集器,所以收集结束时会有大量空间碎片产生。空间碎片过多时,就会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不Full GC。

G1收集器(jdk1.7)

 G1收集器特点:

1)并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop the world停顿的时间,部分其他收集器原本需要停顿java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

2)分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已经存货了一段时间、熬过多次GC的旧对象以获得更好的收集效果。

3)空间整合:与CMS的“标记-清理”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器。G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。

4)可预测的停顿:降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样,使用G1收集器时,Java堆的内存布局与其他收集器有很大区别,它将这个java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

 G1跟踪各个Region里面的垃圾堆积的价值大小,在后台维护了一个优先列表,每次根据允许的收集时间,有限回收价值最大的Region。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

G1收集器运作大致有几个步骤:

1)初始标记

2)并发标记

3)最终标记

4)筛选回收

11.GC,Full GC,Minor GC

 

 

 

Minor GC(Young GC)

Eden区空间耗尽,会触发Minor GC

Minor GC过程:Eden区和from Survivor区存活的对象被复制到to Survivor区,然后from 和to交换指针

算法:复制算法

Major GC

老年代中对象持续增长,导致老年代空间不够用,就会执行Major GC

算法:标记清除回收算法或者标记压缩算法

Full GC

  • System.gc()方法调用

  • 老年代空间不足

    新创建的对象都会被分配到Eden区,如果该对象占用内存非常大,则直接分配到老年代区,此时老年代空间不足

    做minor gc操作前,发现要移动的空间(Eden区、From区向To区复制时,To区的内存空间不足)比老年代剩余空间要大,则触发full gc,而不是minor gc

  • 方法区空间不足

  • Minor GC时,晋升到老年生代的平均大小大于旧生代的剩余空间,那么就直接触发Full GC

  • 堆中分配很大的对象,例如很长的数组,此种对象就会直接进入老年代,如果老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC

空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

6.二叉树遍历

前序遍历:根左右

中序遍历:左根右

后序遍历:左右根

7.单例模式

 

8.双亲委派

类加载过程

每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

使用双亲委派模型,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar,无论哪一个类加载器加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。

如果没有双亲委派,由各个类加载器自行去加载,用户自己写一个java.lang.Object,那系统将会出现多个不同的类,应用程序就会混乱。「如果写一个java.lang.Object,会发现可以正常编译,但无法被加载」

 

9.乐观锁和悲观锁,可重入锁和Synchronized

 

10.JVM调优思路

 

开始
制定标准
快速止损
保留现场
因果分析
根因分析
调 GC
修代码
上线灰度
结束
正常
异常
指标观测
GC 问题
代码问题
优化手段
复盘(可选)

 

 

11.RenteenLock底层是怎么保证线程安全的?

 

12.反射能获取到父类的私有方法吗?怎么防止反射破坏单例模式

反射不能调用父类的私有方法,只能递归实现;

因为反射调用不管是否是private,所以虽然构造器加了private,但为了防止反射破坏,就需要在构造器里也加判断和加锁

13.volatile关键字的原理?它能保证原子性吗?AtomicInteger底层怎么实现的?

 

14.threadLocal关键字有用过吗?如果没有重写initialValue方法就直接get会怎样?

参考:https://www.jianshu.com/p/3bb70ae81828

实现原理:

  • 每个 Thread 线程内部都有一个 ThreadLocalMap;以线程作为 key,泛型作为 value,可以理解为线程级别的缓存。每一个线程都会获得一个单独的 map。

  • 提供了 set 和 get 等访问方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此 get 方法总是返回由当前执行线程在调用 set 时设置的最新值。

应用场景:

  • JDBC 连接

  • Session 管理

  • Spring 事务管理

  • 调用链,参数传递

  • AOP

ThreadLocal 是一个解决线程并发问题的一个类,用于创建线程的本地变量,我们知道一个对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。但是当我们不想使用同步的时候,我们可以选择 ThreadLocal 变量。例如,由于 JDBC 的连接对象不是线程安全的,因此,当多线程应用程序在没有协同的情况下,使用全局变量时,就不是线程安全的。通过将 JDBC 的连接对象保存到 ThreadLocal 中,每个线程都会拥有属于自己的连接对象副本。

内存泄漏:Threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露。

ThreadLocalMap会在set,get以及resize等方法中对stale slots做自动删除(set以及get不保证所有过期slots会在操作中会被删除,而resize则会删除threadLocalMap中所有的过期slots)。

最好的做法是将调用threadlocal的remove方法:把当前ThreadLocal从当前线程的ThreadLocalMap中移除。(包括key,value)

15.线程池里的线程使用ThreadLocal,会有什么问题?

ThreadLocalMap的生命周期是与线程一样的,但是ThreadLocal却不一定,可能ThreadLocal使用完了就想要被回收,但是此时线程可能不会立即终止,还会继续运行(比如线程池中线程重复利用),如果ThreadLocal对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

如果ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来ThreadLocalMap中使用这个ThreadLocal的key也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现key为null的value值。

在ThreadLocalMap中,调用 set()、get()、remove()方法的时候,会清理掉key为null的记录。在ThreadLocal设置为null之后,ThreadLocalMap中存在key为null的值,那么就可能发生内存泄漏,只有手动调用remove()方法来避免。

所以我们在使用完ThreadLocal变量时,尽量用threadLocal.remove()来清除,避免threadLocal=null的操作。remove方法是彻底地回收该对象,而通过threadLocal=null只是释放掉了ThreadLocal的引用,但是在ThreadLocalMap中却还存在其Entry,后续还需处理。

16.InheritableThreadLocal

InheritableThreadLocal:与ThreadLocal的区别是,实现父线程传递本地变量到子线程

参考:https://blog.csdn.net/hewenbo111/article/details/80487252

17.如果有一个机器突然变慢了,如何排查(CPU,内存和IO)

34 第33讲 | 后台服务出现明显“变慢”,谈谈你的诊断思路?

  • 问题可能来自于Java服务自身,也可能仅仅是受系统里其他服务的影响。初始判断可以先确认是否出现了意外的程序错误,例如检查应用本身的错误日志。
    对于分布式系统,很多公司都会实现更加系统的日志、性能等监控系统。一些Java诊断工具也可以用于这个诊断,例如通过JFR(Java Flight Recordera>),监控应用是否大量出现了某种类型的异常。如果有,那么异常可能就是个突破点。如果没有,可以先检查系统级别的资源等情况,监控CPU、内存等资源是否被其他进程大量占用,并且这种占用是否不符合系统正常运行状况。

  • 监控Java服务自身,例如GC日志里面是否观察到Full GC等恶劣情况出现,或者是否Minor GC在变长等;利用jstat等工具,获取内存使用的统计信息也是个常用手段;利用jstack等工具检查是否出现死锁等。

  • 如果还不能确定具体问题,对应用进行Profiling也是个办法,但因为它会对系统产生侵入性,如果不是非常必要,大多数情况下并不建议在生产系统进行。

  • 定位了程序错误或者JVM配置的问题后,就可以采取相应的补救措施,然后验证是否解决,否则还需要重复上面部分过程。

17.fork join

 

18.并发和并行的区别

 

19.CAS

 

20.Java泛型的原理

https://www.cnblogs.com/robothy/p/13949788.html

泛型本质是将数据类型参数化,它通过擦除的方式来实现。声明了泛型的 .java 源代码,在编译生成 .class 文件之后,泛型相关的信息就消失了。可以认为,源代码中泛型相关的信息,就是提供给编译器用的。泛型信息对 Java 编译器可以见,对 Java 虚拟机不可见。

Java 编译器通过如下方式实现擦除:

  • 用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;

  • 在恰当的位置插入强制转换代码来确保类型安全;

  • 在继承了泛型类或接口的类中插入桥接方法来保留多态性。

21.cpu突然飙高,排查思路

止损:

  1. 单机cpu飙高,禁用节点及时止损

  2. 多台机器:

    1. 正在上线:回滚

    2. 是否有流量突增:限流 + 扩容

定位解决:

  1. 查看机器cpu等各种指标

  2. 分析定位异常线程

    1. top命令查看cpu 进程占用情况

    2. 查找可能有问题的进程

    3. 查找可能有问题的线程

    4. 分析堆栈调用情况

    5. 定位问题代码

  3. 分析GC问题,内存泄漏

    1. 查看对象的数量和大小(jmap、美团火焰图Scalpel)

    2. 分析GC是否正常

    3. jmap生成heap dump

    4. 工具查看对象情况

    5. 定位代码是否有问题

22.HashMap 1.7与1.8的区别

  1. 数组+链表改成了数组+链表或红黑树;(大于8,链表转为红黑树,如果节点数量减少从8减少到6转为链表)

  2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后;

  3. 扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,位置不变或索引+旧容量大小;

  4. 在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容;

23.红黑树

 

24 equals()与hashCode()两个方法

  • 如果两个对象相同(a.equals(b) == true),那么两个对象的hashCode一定相同

  • 如果两个对象的hashcode相同,两个对象不一定相同(equals)

HashMap中要比较两个key是否相同,先计算key的hashCode(),比较是否相同,如果相同再比较equals()是否相同。使用HashMap,如果key是自定义类,就必须重写hashcode()和equals()方法

 

25.HashMap的key

 

26.可达性分析算法

可达性分析算法:通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在java语言中,可作为GC Roots的对象包括以下几种:

①.虚拟机栈(栈帧中的本地变量表)中引用的对象

②.方法区中类静态属性引用的对象

③.方法区中常量引用的对象

④.本地方法栈中引用的对象

27.自动装箱

代码块
 
 
 
 
 
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        System.out.println(c == d);//true
        System.out.println(e == f);//false
        System.out.println(c == (a + b));//true
        System.out.println(c.equals (a + b));//true
        System.out.println(g == (a + b));//true
        System.out.println(g.equals (a + b));//false
    }
 

 

28.ArrayList源码分析

add原理:

  1. 判断是否需要扩容,如果初始化时没有指定列表长度(new ArrayList(99)),就需要扩容

  2. 如果需要扩容,数组扩容1.5倍(扩容方式:elementData = Arrays.copyOf(elementData, newCapacity);)扩容方式相当于把自己copy一份,扩容1.5倍,然后再指向自己

  3. 把新加的元素放到数组末尾

remove原理:

  1. 删除数据中的某个元素,删除元素后边的所有元素向前移动()(删除方式:System.arraycopy(elementData, index+1, elementData, index, numMoved);)删除方式,相当于把自己copy一份,删除元素,后边的元素向前移动,然后把copy之后的数组指向自己;

  2. 最后的元素设置为空

  3. 数组长度不变

  4. --size,当调用list.size(),返回的是size这个字段

  5. 所以list.remove()时,数组长度不减少,但是size这个字段减少了,list.size()返回的是size这个字段,并不是数组长度

29.Java 的队列、栈、列表使用

队列Queue:先进先出

实现类:

  • 不阻塞(LinkedList)

  • 线程安全ArrayBlockingQueue:一个由数组支持的有界队列。

  • LinkedBlockingQueue :一个由链接节点支持的可选有界队列。

  • PriorityBlockingQueue :一个由优先级堆支持的无界优先级队列。

  • DelayQueue :一个由优先级堆支持的、基于时间的调度队列。

  • SynchronousQueue :一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制。

添加元素:offer(), add()

返回第一个元素并删除元素:poll(), remove()

返回第一个元素:element(), peek()

LinkedList

增加元素到末尾:add(),addLast()

增加元素到表头:addFirst()

获取最先插入的元素:getFirst();

获取最后一个元素:getLast();

代码块
 
 
 
 
 
 
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        LinkedList<String> lList = new LinkedList<String>();
        lList.add("100");
        lList.add("200");
        lList.add("300");
        lList.add("400");
        lList.add("500");
        System.out.println("链表的第一个元素是:" + lList.getFirst());
        System.out.println("链表的最后一个元素是:" + lList.getLast());
    }
}
链表的第一个元素是:100
链表的最后一个元素是:500
import java.util.LinkedList;

public class Main {
    public static void main(String[] args) {
        LinkedList<String> lList = new LinkedList<String>();
        lList.add("1");
        lList.add("2");
        lList.add("3");
        lList.add("4");
        lList.add("5");
        System.out.println(lList);
        lList.addFirst("0");
        System.out.println(lList);
        lList.addLast("6");
        System.out.println(lList);
    }
}
1, 2, 3, 4, 5
0, 1, 2, 3, 4, 5
0, 1, 2, 3, 4, 5, 6
import java.util.LinkedList;
import java.util.Queue;

public class Main {
    public static void main(String[] args) {
        //add()和remove()方法在失败的时候会抛出异常(不推荐)
        Queue<String> queue = new LinkedList<String>();
        //添加元素
        queue.offer("a");
        queue.offer("b");
        queue.offer("c");
        queue.offer("d");
        queue.offer("e");
        for(String q : queue){
            System.out.println(q);
        }
        System.out.println("===");
        System.out.println("poll="+queue.poll()); //返回第一个元素,并在队列中删除
        for(String q : queue){
            System.out.println(q);
        }
        System.out.println("===");
        System.out.println("element="+queue.element()); //返回第一个元素 
        for(String q : queue){
            System.out.println(q);
        }
        System.out.println("===");
        System.out.println("peek="+queue.peek()); //返回第一个元素 
        for(String q : queue){
            System.out.println(q);
        }
    }
}
a
b
c
d
e
===
poll=a
b
c
d
e
===
element=b
b
c
d
e
===
peek=b
b
c
d
e
 
  1. 单端阻塞队列:其实现有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue和DelayQueue。内部一般会持有一个队列,这个队列可以是数组(其实现是ArrayBlockingQueue)也可以是链表(其实现是LinkedBlockingQueue);甚至还可以不持有队列(其实现是SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。而LinkedTransferQueue融合LinkedBlockingQueue和SynchronousQueue的功能,性能比LinkedBlockingQueue更好;PriorityBlockingQueue支持按照优先级出队;DelayQueue支持延时出队。

  2. 双端阻塞队列:其实现是LinkedBlockingDeque。

  3. 单端非阻塞队列:其实现是ConcurrentLinkedQueue。

  4. 双端非阻塞队列:其实现是ConcurrentLinkedDeque。

30.内存溢出和解决方式

https://www.cnblogs.com/xiaoxi/p/7406903.html

除了程序计数器,其他区域都有可能会因为可能的空间不足发生OutOfMemoryError,简单总结如下:

  • 堆内存不足是最常见的OOM原因之一,抛出的错误信息是“java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在内存泄漏问题;也很有可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显式指定JVM堆大小或者指定数值偏小;或者出现JVM处理引用不及时,导致堆积起来,内存无法释放等。

  • 而对于Java虚拟机栈和本地方法栈,这里要稍微复杂一点。如果我们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM实际会抛出StackOverFlowError;当然,如果JVM试图去扩展栈空间的的时候失败,则会抛出OutOfMemoryError。

  • 对于老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似Intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。

  • 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。

  • 直接内存不足,也会导致OOM,这个已经专栏第11讲介绍过。

31.Java关键字final、static

 

32.如何排查内存泄漏

  1.jmap查看对象数量和大小是否正常;

  2.分析GC是否正常

  3.jmap生成heapdump或dump文件

  4.工具查看对象情况

  5.定位问题代码 

33.虚拟机性能监控与故障处理工具

jps:JVM Process Status Tool,显示制定系统内所有HotSpot虚拟机进程

jstat:JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据

jinfo:Configuration Info for Java,显示虚拟机配置信息

jmap:Memory Map for Java,生成虚拟机的内存转储快照(heapdemp文件)

jhat:JVM Heap Dump Browser,用户分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器查看分析结果

jstack:Stack Trace for Java,显示虚拟机的线程快照

二.MySQL

1.mysql隔离级别

概念:

隔离级别

含义

读数据一致性

脏读

不可重复读

幻读

读未提交(Read Uncommitted)

事务中的修改,即使没有提交,对其他事务都是可见的

最低级别,只能保证不读取物理上损坏的数据

读已提交(Read Committed)

事务从开始到提交之前,所做的修改对其他事务都不可见

语句级

可重复读(Repeatable read)InnoDB默认的隔离级别

同一事务中多次读取同样的记录结果是一致的

事务级

可序列化(Serializable)

在读取的每一行数据上加锁,强制事务串行执行

最高级别,事务级

  1. 脏读:一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系,这种现象被称为“脏读”

  2. 幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象被称为“幻读”

2.分布式锁

redis实现的分布式锁

  1. 加锁:setNx数据

  2. 解锁:del

  3. 异常情况下不释放锁:添加过期时间(setNx时一起设置过期时间,否则有可能set成功了,但是设置过期时间失败,就会导致锁一直不释放)

  4. 解锁分析:如何解决误释放锁?A客户端释放了B客户端的锁。解决方式:加锁时,value设置一个唯一的值,在解锁时判断是否相同,如果相同释放锁。先判断然后再del这是两步操作,不是原子操作,如何解决呢?答:使用lua脚本

可重入锁分析:

  1. 可重入锁:在外层使用锁之后,在内层仍然可以使用。a.xx1()方法内调用了 a.xx2()方法
  2. 使用ThreadLocal实现,加锁时value等于一个唯一标识,并且把这个唯一标识保存到ThreadLocal,所以加锁时,先判断ThreadLocal如果已经加锁,并且是本线程,return true无需再setNx加锁。

可靠性分析:

  • 单节点有可能挂掉,导致锁不可用,所以集群部署,一主多从

  • 一主多从会有什么问题:假设主节点加锁成功,还没有同步从节点,会怎么样?

    答:挂掉之后,哨兵机制,会选取新的主节点,选取完成后,因为挂掉的主节点并没有把锁的信息同步完成,所以会造成多个线程同时获取到锁

RedLock算法:

  • 部署5个master节点,加锁,向5个redis 添加数据,多数以上返回值,则加锁成功;

RedLock注意事项:

  1. 设置多个主节点(注意不是一主多从,而是多个主节点),依次setNx值(相同的key、value)

  2. 当大部分节点返回成功,说明加锁成功。加锁时设置过期时间,防止Redis挂掉,还在等Redis响应结果。

  3. 如果没有获取到锁,需要在所有Redis实例解锁。

  4. 假如A、B、C、D、E5个节点,线程一ABC三个主节点加锁成功,DE加锁失败;然后C节点挂了还没来得及持久化,然后C节点就会重启,重启之后并没有加锁记录,线程二来加锁CDE返回加锁成功(AB加锁不成功),线程二也会获取锁,这种情况就会造成两个线程都获取到锁。解决办法是挂机之后延迟重启。

Zookeeper实现的分布式锁

  1. 加锁:创建临时节点,如果节点是序号最小的则加锁成功;否则添加watch监听序号最大的节点,进入等待

  2. 解锁:删除节点

  3. 异常情况下不释放锁:当客户端异常,与zk断开连接,临时节点自动删除

可靠性分析:

  • 客户端加锁,从节点接到命令,通知主节点

  • 主节点接收命令,发起投票

  • 多数从节点ask,认为通过

  • Leader将结果汇总后如果需要写入,则开始写入同时把写入操作通知给Leader,然后commit;

  • Follwer把请求结果返回给Client

分析:

  • client给Follwer写数据,可是Follwer却宕机了,会出现数据不一致问题么?不可能,这种时候,client建立节点失败,根本获取不到锁。

  • client给Follwer写数据,Follwer将请求转发给Leader,Leader宕机了,会出现不一致的问题么?不可能,这种时候,Zookeeper会选取新的leader,继续上面的提到的写流程。

3.mysql的id为什么是递增的

如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 总的来说就是可以提高查询和插入的性能。

如果不是自增的:碰到不规则数据插入时,为了保持B+树的平衡,会造成频繁的页分裂和页旋转,插入速度比较慢。所以聚簇索引的主键值应尽量是连续增长的值,而不是随机值(不要用随机字符串或UUID)。

4.mysql日志

InnoDB引擎 redo log日志

mysql层 binlog日志

回滚日志 undo log日志

undo log怎么回滚事务呢?

  • inset,记录insert进来的数据id,roll back时,根据id删除

  • delete,记录刚才删除的数据,回滚时,将删除的数据insert

  • update,undo log中记录了修改前的数据,回滚时反向update

5.分库分表为什么分了32张表

为了扩容,扩容 1 -> 2 -> 4->8->16->32

6.mysql 索引结构B树和B+树的区别

B树的每个节点存储了key和data,key是一条数据记录的键值,是唯一的,data存储的是数据记录除key以外的数据。而B+树只在叶子节点存储data数据,这样非叶子节点就能存储更多的key。

B+树的每个叶子节点的指针指向相邻的叶子节点,构成一个有序链表(双向链表),可以按照关键码排序的次序遍历全部记录。由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树叶子节点指针为null,则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好

7.mysql索引优化没有走对是什么原因

  • 扫描行数的估计值依然不准确

  • 优化器会优先选择唯一索引

解决方式:

1.force index强制走某个索引

2.修改sql,例如加order byXX

8.常见的索引结构

  • 哈希表:范围查询非常慢

  • 有序数组:查询性能好,插入、修改很慢

  • B+树

9.InnoDB与MyISAM的区别

  1. MyISAM不支持事务,而InnoDB支持。InnoDB的AUTOCOMMIT默认是打开的,即每条SQL语句会默认被封装成一个事务,自动提交,这样会影响速度,所以最好是把多条SQL语句显示放在begin和commit之间,组成一个事务去提交。

  2. InnoDB支持数据行锁定,MyISAM不支持行锁定,只支持锁定整个表。即MyISAM同一个表上的读锁和写锁是互斥的,MyISAM并发读写时如果等待队列中既有读请求又有写请求,默认写请求的优先级高,即使读请求先到,所以MyISAM不适合于有大量查询和修改并存的情况,那样查询进程会长时间阻塞。因为MyISAM是锁表,所以某项读操作比较耗时会使其他写进程饿死。

  3. InnoDB支持外键,MyISAM不支持。

  4. InnoDB的主键范围更大,最大是MyISAM的2倍。

  5. InnoDB不支持全文索引,而MyISAM支持。全文索引是指对char、varchar和text中的每个词(停用词除外)建立倒排序索引。MyISAM的全文索引其实没啥用,因为它不支持中文分词,必须由使用者分词后加入空格再写到数据表里,而且少于4个汉字的词会和停用词一样被忽略掉。

  6. MyISAM支持GIS数据,InnoDB不支持。即MyISAM支持以下空间数据对象:Point,Line,Polygon,Surface等。

  7. 没有where的count(*)使用MyISAM要比InnoDB快得多。因为MyISAM内置了一个计数器,count(*)时它直接从计数器中读,而InnoDB必须扫描全表。所以在InnoDB上执行count(*)时一般要伴随where,且where中要包含主键以外的索引列。为什么这里特别强调“主键以外”?因为InnoDB中primary index是和raw data存放在一起的,而secondary index则是单独存放,然后有个指针指向primary key。所以只是count(*)的话使用secondary index扫描更快,而primary key则主要在扫描索引同时要返回raw data时的作用较大。

10.mysql的组件

 

 

11.聚集索引说一下

就索引会存全部数据,按顺序存储之类的。另外聚集一定有唯一性约束。所以不能走出change buffer

 

12.慢sql优化

  1. explain一下,分析可能的优化点

  2. 判断索引,查看是否走索引,索引是否合理等

  3. 是否查的数据量太大(可以采用多次查询)

  4. update、delete语句,是否有锁等待

  5. 分析语句是否查了不必要的字段

  6. 表数据量太大,判断是否需要分库分表

  7. 是否使用缓存,减少查询次数

13.mysql加锁机制

 

14.Mysql索引最左匹配原则

 

15.现在一个表有三列a b c,组合索引(a,b,c)查询的时候where a like ? and b=? and c=?能用到这个组合索引吗?为什么?

 

16.mysql 大字段为什么不能键索引

数据库的数据存储以页为单位,一页存的数据越多,一次获取IO操作获取的数据越多,索引查询效率越高。

18.mysql深度分页问题

https://blog.csdn.net/Carson_Chu/article/details/108445426

 

20.mysql 双1配置

双1配置,就是binlog sync_binlog设置为1,每次提交事务都会持久化到磁盘;redo log innodb_flush_log_at_trx_commit设置为1,每次提交事务,redo log也持久化到磁盘

每个线程有自己binlog cache,但是共用同一份binlog文件。

  • 图中的write,指的就是指把日志写入到文件系统的page cache,并没有把数据持久化到磁盘,所以速度比较快。

  • 图中的fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为fsync才占磁盘的IOPS。

write 和fsync的时机,是由参数sync_binlog控制的:

  1. sync_binlog=0的时候,表示每次提交事务都只write,不fsync;

  2. sync_binlog=1的时候,表示每次提交事务都会执行fsync;

  3. sync_binlog=N(N>1)的时候,表示每次提交事务都write,但累积N个事务后才fsync。

为了控制redo log的写入策略,InnoDB提供了innodb_flush_log_at_trx_commit参数,它有三种可能取值:

  1. 设置为0的时候,表示每次事务提交时都只是把redo log留在redo log buffer中;

  2. 设置为1的时候,表示每次事务提交时都将redo log直接持久化到磁盘;

  3. 设置为2的时候,表示每次事务提交时都只是把redo log写到page cache。

21.分库分表之后,查询与路由规则不相同的字段

映射

22.百万级别的数据如何删除

关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的 IO,会降低增/改/删的执行效率。所以,在我们删除数据库百万级别数据的时候,查询 MySQL 官方手册得知删除数据的速度和创建的索引数量是成正比的。

  •  所以我们想要删除百万数据的时候可以先删除索引(此时大概耗时三分多钟)

  • 然后删除其中无用数据(此过程需要不到两分钟)

  • 删除完成后重新创建索引(此时数据较少了)创建索引也非常快,约十分钟左右。

  • 与之前的直接删除绝对是要快速很多,更别说万一删除中断,一切删除会回滚。那更是坑了。

23.mysql binlog3种格式

  • 当binlog_format=statement时,binlog里面记录是变更数据库的执行语句和事务,binlog日志中记录的是变更数据库的执行语句和事务。问题:当delete 带limit时,如果主库和从库索引选择不一致,会引起主从不一致

  • Row-based: 在这个格式下,binlog日志中记录的是发生变更的数据行。

  • Mixed-logging: 这个格式是前两种格式的混合版,主库会根据执行的语句来决定binlog日志中记录的内容,可以是具体的行,也可以是执行的语句。

代码块
 
 
 
 
 
为什么binlog_format=statement时,会有问题?
mysql> delete from t /*comment*/  where a>=4 and t_modified<='2018-11-10' limit 1;
分析sql,删除1行数据,当binlog_format=statement,binlog记录的是执行语句,假如主库选择的索引是a,从库选择的索引是t_modified,得到的删除结果有可能是不一样的
 

24.MVCC(多版本并发控制)

MVCC 多版本并发控制,读写互相不阻塞,提高并发处理能力。

每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

  • DB_TRX_ID:事务id

  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本

  • DB_ROW_ID:如果表没有主键,InnoDB会自动生成一个隐藏主键,因此会出现这个列

流程:

  1. 首先获取事务自己的版本号,也就是事务ID;

  2. 获取Read View;

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

  4. 如果不符合ReadView规则,就需要从Undo Log中获取历史快照;

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

 

  • Read View保存了当前事务开启时所有活跃(还没有提交)的事务列表

  • RR隔离级别:事务开始时,将当前所有活跃的事务拷贝到一个列表中 read view

  • RC隔离级别:在事务每个语句开始时,把当前所有活跃的事务拷贝到一个列表中 read view

  • 事务执行时,会生成一个读视图(read view),生成一个递增的事务id

附录:

undo log主要分为两种:

  • insert undo log

代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃

  • update undo log

事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

25.mysql主从复制

主库将变更写入binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到本地,写入一个relay中继日志中,接着从库中有一个sql线程从中继日志中读取binlog,然后执行binlog。

26.mysql主从延迟

  1. 主库的从库太多

  2. 从库硬件配置比主库低

  3. 慢sql引起

  4. 主从之间网络延迟

  5. 主库读写压力大

27.mysql索引处理null值

参考:https://www.jb51.net/article/164582.htm

结论:建表时,不允许值为null

原因:很多文章说,null用不到索引;2.对MySQL来说,null是一个特殊的值,比如:不能使用=,<,>这样的运算符,对null做算术运算的结果都是null,count时不会包括null行等,null比空字符串需要更多的存储空间等。

28.mysql 索引B+树层数与存储

参考:https://blog.csdn.net/qq_35590091/article/details/107361172

每页16KB估算,3层可以存两千万级别的数据;4层可存80亿的数据

29.mysql 大事务有什么问题

 

30.事务中加入RPC接口有什么问题

 

三.Redis

1..分布式事务

 

2.如何解决Redis与mysql数据不一致

不一致原因:

问题1:修改时,删除缓存,但是还没有更新mysql,此时查请求,发现没有命中缓存,查mysql之后保存缓存。(这样mysql就与redis数据不一致了)

问题2:查请求,没有命中缓存,查数据库,然后需要更新缓存;假如在查完之后更新缓存之前,有一个update请求,更新mysql,删除缓存,此时读请求做更新缓存。

https://blog.csdn.net/wd_boy/article/details/88289763

  • 查,命中从redis取出;没命中,查mysql,之后保存缓存

  • 修改或保存,先删除缓存,再更新mysql

  • 修改保存,先更新mysql,再删除缓存

  • 查,命中从redis取出;没命中,查mysql,之后保存缓存

  • 修改保存,先更新mysql,再删除缓存

  • 延时双删:1.del redis;2.update mysql;3.sleep;4.del redis

  • databus:insert、update、delete操作都是先更新mysql;根据binlog发送mq,更新redis

3.redis 热点key问题处理

热点Key探测与解决方案

  • 提前可预知的,比如:秒杀,独立部署redis集群

  • 不可预知的,需要埋点探测,探测到热点key,放到缓存或者其他独立的redis集群

探测方案:

方案

优点

缺点

客户端

实现简单

内存泄漏风险,维护成本高,只能统计单个客户端

代理

代理是客户端与服务端桥梁

增加代理端开发部署成本,不适合squirrel架构

服务端

实现简单

monitor本身使用成本和危害,只能短时间使用,只能统计单个节点

机器

客户端和服务端无侵入

需要专业团队开发,增加了机器的部署成本。

修改server源码

对于server性能无损耗

需要修改redis server源码

解决方案:

  1. 如果本地缓存开启,使用本地缓存

  2. 如果本地缓存没有开启,迁移热点key单独分片

4.Redis的数据淘汰机制

Redis在每个服务客户端执行一个命令的时候,都会先检测使用的内存是否超额。

在Redis中,我们可以设置Redis的最大使用内存大小(server.maxmemory)。当Redis内存数据集大小上升到一定程度的时候,就会施行数据淘汰机制。Redis提供了一下6种数据淘汰机制:

volatile-lru :从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中随机挑选数据淘汰。

allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。

allkeys-random:从数据集(server.db[i].dict)中随机挑选数据淘汰。

no-envivtion(驱逐):禁止驱逐数据。

LRU机制: Redis保存了lru计数器server.lrulock,会定时的去更新(redis定时程序severCorn()),每个Redis对象都会设置相应的lru值,每次访问对象的时候,Redis都会更新redisObject.lru。

LRU淘汰机制: 在数据集中随机挑选几个键值对,取出其中lru最大的键值对淘汰。所以,Redis并不能保证淘汰的数据都是最近最少使用的,而是随机挑选的键值对中的。

TTL机制: Redis数据集结构中保存了键值对过期时间表,即 redisDb.expires。

TTL淘汰机制: 在数据集中随机挑选几个键值对,取出其中最接近过期时间的键值对淘汰。所以,Redis并不能保证淘汰的数据都是最接近过期时间的,而是随机挑选的键值对中的。

5.怎么查看所有的key?redis怎么切换库?怎么清数据?

 

6.怎么保证Redie的高可用

 

7.redis高性能IO模型

4 03 | 高性能IO模型:为什么单线程Redis能那么快?

8.Redis RDB日志会阻塞么?

Redis提供了两个命令来生成RDB文件,分别是save和bgsave。

  • save:在主线程中执行,会导致阻塞;

  • bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配置。

为了快照而暂停写操作,肯定是不能接受的。所以这个时候,Redis就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。

简单来说,bgsave子进程是由主线程fork生成的,可以共享主线程的所有内存数据。bgsave子进程运行后,开始读取主线程的内存数据,并把它们写入RDB文件。

此时,如果主线程对这些数据也都是读操作(例如图中的键值对A),那么,主线程和bgsave子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave子进程会把这个副本数据写入RDB文件,而在这个过程中,主线程仍然可以直接修改原来的数据。

9.redis zset怎么生成

 

10.缓存穿透、缓存击穿和缓存雪崩

缓存穿透

原因:查询不存在的key

解决办法:①.布隆过滤器;②.为不存在的值,添加默认值;③.以缓存为主,没有就是没有,有专门的异步线程去加载缓存(异步线程加载缓存,定期或者根据条件触发去加载)

缓存击穿

原因:某个key失效时,正好有大量请求

解决办法:①.更新缓存时,添加分布式锁,只能有一个线程去查mysql,然后更新缓存;②.以缓存为主,没有就是没有,有专门的异步线程去加载缓存(异步线程加载缓存,定期或者根据条件触发去加载)

缓存雪崩

原因1:大量缓存同一时间失效

解决办法:避免同一时间失效,设置过期时间时,加一个随机值,尽量避免同一时间大量过期;限流,服务降级,返回默认值

原因2:redis宕机了

解决办法:使用的热数据尽量分散在不同的机器;多个副本,实现高可用;限流,服务降级,返回默认值

11.假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

12.利用redis实现积分排序,查看前1000个

 

13.redis value特别大怎么优化?

 

 

四.多线程

1.线程池参数有哪些,具体解释

参数:

  • corePoolSize:核心线程大小

  • maximumPoolSize:最大线程大小

  • keepAliveTime:线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)成为核心线程的有效时间

  • unit:keepAliveTime的时间单位

  • workQueue:阻塞任务队列

  • threadFactory:线程工厂

  • handler当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理

核心的两个队列:

  • 线程等待池,即线程队列BlockingQueue

  • 任务处理池PoolWorker,即正在工作的Thread列表

2.线程池的流程

corePoolSize:核心线程数;maximunPoolSize:最大线程数

每当有新的任务到线程池时,

第一步: 先判断线程池中当前线程数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理,若当前线程数量已达到corePoolSize,则进入下一步;

第二步: 判断工作队列(workQueue)是否已满,未满则将新的任务提交到工作队列中,满了则进入下一步;

第三步: 判断线程池中的线程数量是否达到了maxumunPoolSize,如果未达到,则新建一个工作线程来执行这个任务,如果达到了则使用拒绝策略来处理这个任务。注意: 在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止。直到线程池中线程的数量不大于corePoolSize为止。

(由第三步可知,在一般情况下,Java线程池中会长期保持corePoolSize个线程。)

拒绝策略:1.什么都不做丢弃;2.抛出异常;3.交给调用线程去处理;4.丢弃最前边的任务,尝试重新执行

3.多线程取多个数据拼装返回给前端

//todo

 

4.线程池的实现原理

 

5.线程的状态,线程池的状态

线程的状态

关于线程生命周期的不同状态,在Java 5以后,线程状态被明确定义在其公共内部枚举类型java.lang.Thread.State中,分别是:

  • 新建(NEW),表示线程被创建出来还没真正启动的状态,可以认为它是个Java内部状态。

  • 就绪(RUNNABLE),表示该线程已经在JVM中执行,当然由于执行需要计算资源,它可能是正在运行,也可能还在等待系统分配给它CPU片段,在就绪队列里面排队。

  • 在其他一些分析中,会额外区分一种状态RUNNING,但是从Java API的角度,并不能表示出来。

  • 阻塞(BLOCKED),这个状态和我们前面两讲介绍的同步非常相关,阻塞表示线程在等待Monitor lock。比如,线程试图通过synchronized去获取某个锁,但是其他线程已经独占了,那么当前线程就会处于阻塞状态。

  • 等待(WAITING),表示正在等待其他线程采取某些操作。一个常见的场景是类似生产者消费者模式,发现任务条件尚未满足,就让当前消费者线程等待(wait),另外的生产者线程去准备任务数据,然后通过类似notify等动作,通知消费线程可以继续工作了。Thread.join()也会令线程进入等待状态。

  • 计时等待(TIMED_WAIT),其进入条件和等待状态类似,但是调用的是存在超时条件的方法,比如wait或join等方法的指定超时版本,如下面示例:

public final native void wait(long timeout) throws InterruptedException;

  • 终止(TERMINATED),不管是意外退出还是正常执行结束,线程已经完成使命,终止运行,也有人把这个状态叫作死亡。

线程池的状态

  • RUNNING: Accept new tasks and process queued tasks 接收新任务,并且处理排队的任务

  • SHUTDOWN: Don't accept new tasks, but process queued tasks 不接收新任务,但是处理排队的任务

  • STOP: Don't accept new tasks, don't process queued tasks,and interrupt in-progress tasks* 不接收新任务,不处理排队的任务,并且中断正在执行的任务

  • TIDYING: All tasks have terminated, workerCount is zero,the thread transitioning to state TIDYING will run the terminated() hook method:所有的任务都已终止,正在执行的任务数量为0,线程池状态转换为tidying,将执行terminated()方法,到TERMINATED状态

  • TERMINATED: terminated() has completed terminated()执行完成

 

6.线程池中线程的回收,回收什么状态的线程

https://blog.csdn.net/u013256816/article/details/109213183

7. 4种线程池类型

Java 通过 Executors(jdk1.5 并发包)提供四种线程池,分别为:

  1. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

  4. newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

8.线程和线程池的状态,以及转换

线程状态:

线程池状态:running,shut down(不接收新任务);stop(不接收新,中断正在进行的);tidying(shutdown状态后,调方法,所有任务终止);terminated(线程池终止)

阻塞分为3种:

  1. 等待阻塞:wait()方法,JVM会把线程放入等待队列 waitting queue

  2. 同步阻塞:在获取锁时,锁被其他线程占用,JVM把该线程放入锁池中 lock pool

  3. 其他阻塞:sleep、join、IO请求等,JVM把该线程设置为阻塞状态,等sleep结束、xx结束,重新进入runable状态

sleep和wait的区别

  • sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时会自动恢复。调用sleep不会释放对象锁。

  • wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法后本线程才进入对象锁定池准备获得对象所进入运行状态。

9.线程结束有哪几种方式

  1. 正常线程执行完run方法,线程结束

  2. stop方法,已经被弃用,调用stop方法线程会立即停止

  3. 使用 interrupt() 中断线程,调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。

为什么stop方法被弃用?

  1. 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。

  2. 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。

interrupt如何中断线程呢?

  • 目标线程调用isInterrupted方法判断是否需要中断,如果中断,跳出方法,走到线程结束;

代码块
 
 
 
 
 
public void run() {
    super.run();
    for(int i = 0; i <= 200000; i++) {
        //判断是否被中断
        if(Thread.currentThread().isInterrupted()){
            //处理中断逻辑
            break;
        }
        System.out.println("i=" + i);
    }
}
 

10.stop()和interrupt()方法的主要区别是什么呢?

stop()方法会真的杀死线程,不给线程喘息的机会,如果线程持有ReentrantLock锁,被stop()的线程并不会自动调用ReentrantLock的unlock()去释放锁,那其他线程就再也没机会获得ReentrantLock锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有suspend() 和 resume()方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。

而interrupt()方法就温柔多了,interrupt()方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被interrupt的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。

当线程A处于WAITING、TIMED_WAITING状态时,如果其他线程调用线程A的interrupt()方法,会使线程A返回到RUNNABLE状态,同时线程A的代码会触发InterruptedException异常。上面我们提到转换到WAITING、TIMED_WAITING状态的触发条件,都是调用了类似wait()、join()、sleep()这样的方法,我们看这些方法的签名,发现都会throws InterruptedException这个异常。这个异常的触发条件就是:其他线程调用了该线程的interrupt()方法。

当线程A处于RUNNABLE状态时,并且阻塞在java.nio.channels.InterruptibleChannel上时,如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptException这个异常;而阻塞在java.nio.channels.Selector上时,如果其他线程调用线程A的interrupt()方法,线程A的java.nio.channels.Selector会立即返回。

上面这两种情况属于被中断的线程通过异常的方式获得了通知。还有一种是主动检测,如果线程处于RUNNABLE状态,并且没有阻塞在某个I/O操作上,例如中断计算圆周率的线程A,这时就得依赖线程A主动检测中断状态了。如果其他线程调用线程A的interrupt()方法,那么线程A可以通过isInterrupted()方法,检测是不是自己被中断了。

10.线程和进程的区别

 

11.可重入锁

举例来说明锁的可重入性

代码块
 
 
 
 
 
public class UnReentrant{
  Lock lock = new Lock();

  public void outer(){
    lock.lock();
    inner();
    lock.unlock();
  }
  public void inner(){
    lock.lock();//do somethinglock.unlock();
  }
}
 

outer 中调用了 inner,outer 先锁住了 lock,这样 inner 就不能再获取 lock。其实调用outer 的线程已经获取了 lock 锁,但是不能在 inner 中重复利用已经获取的锁资源,这种锁即称之为不可重入可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码
块。

synchronized、ReentrantLock 都是可重入的锁,可重入锁相对来说简化了并发编程的开发。

12.给用户发消息,任务超出队列,使用哪种拒绝策略?

ThreadPoolExecutor.CallerRunsPolicy

  • 无界队列(LinkedBlockingQuene),继续添加任务到阻塞队列中等待执行。

  • 用消息队列存任务数据,线程池慢慢处理。

13.线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1)FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

14.线程池多少个线程才合适

参考:12 10 | Java线程(中):创建多少线程才是合适的?

最佳线程数=CPU核数 * [ 1 +(I/O耗时 / CPU耗时)]

单核的话如果cpu 100%运行,就可以如下图所示,IO操作的时候,CPU空闲,如果吞吐量提高到两个线程,就可以在CPU空闲时(IO),另一个线程去计算。

线程池配置

核心线程数 = 每秒请求数 × 平均请求处理时间 (这是理想的情况,也就是说线程一直在忙着干活,没有被阻塞在I/O等待上。实际上任务在执行中,线程不可避免会发生阻塞)

核心线程数= CPU核数*(1+ 线程等待时间 / 线程时间运行时间 ) 【线程等待时间:大部分被当成IO时间】

队列大小 = 核心线程 * 容忍的最大等待时长 / 平均请求处理时间

最大线程数 = (请求量峰值 - 队列大小)/ ( 1s / 平均请求处理时间)

 

五.Kafka

1.Kafka的高性能

  1. 使用批量处理的方式来提升系统吞吐能力。

    1. producer批量发送消息

    2. 服务端批量保存消息

    3. 消费者批量拉取消息(批量拉取消息之后,再一条一条交给用户代码处理)

  2. 基于磁盘文件高性能顺序读写的特性来设计的存储结构。

    1. 顺序读写:保存log,append到一个日志段,一个日志段满了之后开一个新的日志段继续append;

    2. 消费者也是从log的某个位置开始,顺序读消息

  3. 利用操作系统的PageCache来缓存数据,减少IO并提升读性能。

    1. PageCache是现代操作系统都具有的一项基本特性。通俗地说,PageCache就是操作系统在内存中给磁盘上的文件建立的缓存。

  4. 使用零拷贝技术加速消费流程。

    1. 从文件复制数据到PageCache中,如果命中PageCache,这一步可以省掉;

    2. 从PageCache复制到应用程序的内存空间中,也就是我们可以操作的对象所在的内存;

    3. 从应用程序的内存空间复制到Socket的缓冲区,这个过程就是我们调用网络应用框架的API发送数据的过程。

2.kafka可靠性保证

 

3.kafka顺序性保证

 

4.rabbitmq

 

5.ack机制,offset何时位移,broker复制原理

Kafka的ack机制:指的是producer的消息发送确认机制 通过request.required.acks参数来设置

这直接影响到Kafka集群的吞吐量和消息可靠性。而吞吐量和可靠性就像硬币的两面,两者不可兼得,只能平衡。

1(默认):producer在ISR中的leader已成功收到数据并得到确认。(leader写完即代表接收成功)

最低的延迟,但是最弱的持久性。

如果leader刚刚接收到消息,follwer还没来得及同步过去,结果leader所在的broker宕机了,此时也会导致这条消息丢失。

0:producer无需等待来自broker的确认而继续发送下一批消息。(只管发 不管leader是否接收到)

这种情况下数据传输效率最高,但是数据可靠性确是最低的。

可能你发送出去的消息还在半路,leader所在broker就直接挂了,但是你的客户端还是认为消息发送成功,此时就会导致这条消息丢失。

-1(all):producer需要等待ISR中的所有follower都确认接收到数据后才算一次发送完成

持久性最好,延时性最差,可靠性最高。

但是当ISR中只有leader时 这样就变成了acks=1的情况。也会丢失数据

6.Kafka同步/异步发送消息

 

7.美团mq mafka死信处理逻辑

Kafka本身不支持延迟发送和死信处理,延迟发送和死信是mafka支持的

延迟发送的逻辑:

  1. 延迟发送,先将消息存到redis

  2. 然后redis根据时间,到期之后发送消息

死信的处理逻辑

  1. mq如果几次都没有消费成功,就会进入死信队列,消费者先不处理当前消息,当前消息sendDelayMsg(),即上边的延迟发送(其实就是把死信的mq放入了redis)

  2. 然后过一段时间,再去处理消息

8.kafka日志处理逻辑

 

9.Kafka消息幂等&事务

参考:15 14 | 幂等生产者和事务生产者是一回事吗?

所谓的消息交付可靠性保障,是指Kafka对Producer和Consumer要处理的消息提供什么样的承诺。常见的承诺有以下三种:

  • 最多一次(at most once):消息可能会丢失,但绝不会被重复发送。

  • 至少一次(at least once):消息不会丢失,但有可能被重复发送。

  • 精确一次(exactly once):消息不会丢失,也不会被重复发送。

幂等型Producer

指定Producer幂等性的方法很简单,仅需要设置一个参数即可,即props.put(“enable.idempotence”, ture),或props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true)

底层具体的原理很简单,就是经典的用空间去换时间的优化思路,即在Broker端多保存一些字段。当Producer发送了具有相同字段值的消息后,Broker能够自动知晓这些消息已经重复了,于是可以在后台默默地把它们“丢弃”掉。

  • 首先,它只能保证单分区上的幂等性,即一个幂等性Producer能够保证某个主题的一个分区上不出现重复消息,它无法实现多个分区的幂等性。

  • 其次,它只能实现单会话上的幂等性,不能实现跨会话的幂等性。这里的会话,你可以理解为Producer进程的一次运行。当你重启了Producer进程之后,这种幂等性保证就丧失了。

事务型Producer

设置事务型producer:

  • 和幂等性Producer一样,开启enable.idempotence = true。

  • 设置Producer端参数transactional. id。最好为其设置一个有意义的名字。

producer.initTransactions();

try {

producer.beginTransaction();

producer.send(record1);

producer.send(record2);

producer.commitTransaction();

} catch (KafkaException e) {

producer.abortTransaction();

}

和普通Producer代码相比,事务型Producer的显著特点是调用了一些事务API,如initTransaction、beginTransaction、commitTransaction和abortTransaction,它们分别对应事务的初始化、事务开始、事务提交以及事务终止。

这段代码能够保证Record1和Record2被当作一个事务统一提交到Kafka,要么它们全部提交成功,要么全部写入失败。实际上即使写入失败,Kafka也会把它们写入到底层的日志中,也就是说Consumer还是会看到这些消息。因此在Consumer端,读取事务型Producer发送的消息也是需要一些变更的。修改起来也很简单,设置isolation.level参数的值即可。当前这个参数有两个取值:

  1. read_uncommitted:这是默认值,表明Consumer能够读取到Kafka写入的任何消息,不论事务型Producer提交事务还是终止事务,其写入的消息都可以读取。很显然,如果你用了事务型Producer,那么对应的Consumer就不要使用这个值。

  2. read_committed:表明Consumer只会读取事务型Producer成功提交事务写入的消息。当然了,它也能看到非事务型Producer写入的所有消息。

六.spring

1.spring里边的设计模式有哪些?

工厂模式:构建bean对象

单例模式:构建bean对象

策略模式:set注入;还是构造器注入

代理模式:jdk;cglib

观察者模式:Listener

适配器模式:

包装器模式:

模板方法:

2.SpringBean的生命周期。,你常用哪一种注入方式?BeanFactory和ApplicationContext有什么区别?你们项目里用的哪个?

生命周期

  • 实例化Bean对象。

  • 设置Bean属性。

  • 如果我们通过各种Aware接口声明了依赖关系,则会注入Bean对容器基础设施层面的依赖。具体包括BeanNameAware、BeanFactoryAware和ApplicationContextAware,分别会注入Bean ID、Bean Factory或者ApplicationContext。

  • 调用BeanPostProcessor的前置初始化方法postProcessBeforeInitialization。

  • 如果实现了InitializingBean接口,则会调用afterPropertiesSet方法。

  • 调用Bean自身定义的init方法。

  • 调用BeanPostProcessor的后置初始化方法postProcessAfterInitialization。

  • 创建过程完毕。

  • Spring Bean的销毁过程会依次调用DisposableBean的destroy方法和Bean自身定制的destroy方法。

3.spring bean的作用域

  • Singleton,这是Spring的默认作用域,也就是为每个IOC容器创建唯一的一个Bean实例。

  • Prototype,针对每个getBean请求,容器都会单独创建一个Bean实例。

从Bean的特点来看,Prototype适合有状态的Bean,而Singleton则更适合无状态的情况。另外,使用Prototype作用域需要经过仔细思考,毕竟频繁创建和销毁Bean是有明显开销的。

如果是Web容器,则支持另外三种作用域:

  • Request,为每个HTTP请求创建单独的Bean实例。

  • Session,很显然Bean实例的作用域是Session范围。

  • GlobalSession,用于Portlet容器,因为每个Portlet有单独的Session,GlobalSession提供一个全局性的HTTP Session。

4.spring mvc一次请求

5.tomcat架构

 

6.Spring IOC、AOP,Spring 事务传播

IOC:

依赖注入:

你两种依赖方式都可以使用,构造器注入和Setter方法注入。最好的解决方案是用构造器参数实现强制依赖,setter方法实现可选依赖。

(其实还有一种注入方式:接口注入,接口注入即controller加入依赖service,service需要是一个接口,不是xximpl实现类,那么问题来了如果一个接口有多个实现类,怎么办呢?如果用@Autowired,先根据类型匹配,再根据名称匹配,如果匹配到多个报错,除非用@Qualifier标识;如果用@Resource,根据名称匹配)

AOP

事务传播机制:spring事务传播

事务传播行为类型

说明

PROPAGATION_REQUIRED

如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择(spring默认的事务传播行为)

PROPAGATION_SUPPORTS

支持当前事务,如果当前没有事务,就以非事务方式执行

PROPAGATION_MANDATORY

使用当前事务,如果当前没有事务,就抛出异常

PROPAGATION_REQUIRES_NEW

新建事务,如果当前存在事务,把当前事务挂起

PROPAGATION_NOT_SUPPORTED

以非事务方式执行操作,如果当前存在事务,就把当前事务挂起

PROPAGATION_NEVER

以非事务方式执行,如果当前存在事务,则跑出异常

PROPAGATION_NESTED

如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。(底层的数据源必须基于3.0)

8.BeanFactory与FactoryBean的区别

https://blog.csdn.net/qiesheng/article/details/72875315

BeanFacotry是spring中比较原始的Factory。如XMLBeanFactory就是一种典型的BeanFactory。原始的BeanFactory无法支持spring的许多插件,如AOP功能、Web应用等。 

ApplicationContext接口,它由BeanFactory接口派生而来,因而提供BeanFactory所有的功能。ApplicationContext以一种更向面向框架的方式工作以及对上下文进行分层和实现继承,

ApplicationContext包还提供了以下的功能:
• MessageSource, 提供国际化的消息访问
• 资源访问,如URL和文件
• 事件传播
• 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层

9.spring 一次http请求,filter在哪儿处理

总结下我对SpringWebMvc流程:

  Step1.请求被前端控制器DispatcherServlet接收;

  Step2.前端控制器DispatcherServlet根据自身的handlerMappings集合查找可用的handler,本质也就是Controller,当然现在最常见的就是@Controller注解的类;

  Step3.匹配的HandlerMapping查找到合适的handler对象,并封装成HandlerExecutionChain对象(含有一个handler对象,以及若干个HandlerInterceptor拦截器);

  Step4.前端控制器DispatcherServlet遍历自身的handlerAdapters集合,调用HandlerAdapter接口的support方法,查找匹配的handlerAdapter对象;

  Step5.返回匹配的HandlerAdapter给前端控制器;

  Step6. 首先执行的是HandlerExecutionChain中的HandlerInterceptor,顺序执行其preHandle方法;有一个返回false,后面流程不再执行;

  Step7.匹配的handlerAdapter调用其handle方法,执行业务逻辑,并且返回DispatcherServlet  ModelAndView对象(当然ModelAndView只是最基本的对象,还有多种返回可以接受);

  Step8.后序遍历HandlerExecutionChain中的HandlerInterceptor,执行其postHandle方法;

  Step9. 前端控制器DispatcherServlet根据自身的viewResolvers寻找合适的视图解析视图,生成View对象,常见的JSP技术解析JSP文件路径等等,相应的其他技术替换视图解析器即可;

  Sep10.视图解析器返回View对象给前端控制器,前端控制器根据View对象进行渲染,(比如将属性设置到request中);

  Step11.后序遍历HandlerExecutionChain中的HandlerInterceptor,执行其afterCompletion方法

10.spring AOP底层原理

Spring AOP底层是采用动态代理机制实现的,AOP就是由代理创建出一个和impl实现类平级的对象,但是这个对象不是一个真正的对象,只是一个代理对象,但它可以实现和impl相同的功能,这个就是AOP的横向机制原理,这样就不需要修改源代码。

  • 如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK proxy,去创建一个代理对象;

  • 如果没有实现的接口的对象,就无法使用JDK proxy去进行代理了,这时候会使用 Cglib生成一个被代理的子类作为代理。

11.spring 事务失效的原因

事务失效的几个原因:

  1. Service没有被spring管理

  2. 方法不是public

  3. 异常被吞了

  4. 通过this调用自身方法

附:为什么调用自身方法事务会失效?

因为事务是通过spring aop实现的,而spring aop是通过动态代理实现的,这里的this也就是真实对象,并不是代理对象,所以事务会失效。

12.过滤器、拦截器、AOP区别

参考:https://blog.csdn.net/xiaorui51/article/details/108753111

过滤器:过滤不符合要求的请求,根据url过滤。eg:权限判断

方法:init、destroy、doFilter

拦截器:是动态拦截 action 调用的对象,然后提供了可以在 action 执行前后增加一些操作,也可以在 action 执行前停止操作,登录认证、日志、通用处理

方法:preHandle,postHandle,afterCompletion

AOP:面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,更灵活,比如日志,事务,权限等待

方法:

三者功能类似,但各有优势,从过滤器--》拦截器--》切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。

七.其他

1.es查询过程

 

2.限流和限流策略

 

3.熔断怎么熔断的

 

 

4.mybatis缓存

查询过程:二级缓存-->一级缓存-->查询数据库

一级缓存:默认开启,一次请求,查询到的结果保存在SQLSession中,如果相同的sql,会先从一级缓存中获取

二级缓存:默认关闭,保存在CachingExecutor

  • 映射语句文件中的所有 select 语句的结果将会被缓存。

  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。

  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。

  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。

  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。

  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。

  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。

  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

5.除了zk你还用过其他注册中心吗?之间有什么区别

zookeeper(dubbo):一致性;分区容错性;牺牲可用性(leader节点负责读写;leader挂了,就重新选择,有一段时间不可用)

Eureka(spring cloud):可用性和分区容错性(集群中不分leader,如果某个节点挂了,数据还没有同步到其他节点)

6.一致性哈希算法说一下,具体扩容过程。

 

7.cglib和jdk代理的原理

 

8.Http和Https的区别以及Https加密的方式?混合加密

https://www.cnblogs.com/wqhwe/p/5407468.html

超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。

  为了解决HTTP协议的这一缺陷,需要使用另一种协议:安全套接字层超文本传输协议HTTPS,为了数据传输的安全,HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。

  HTTPS和HTTP的区别主要如下:

  1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

  2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

  3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

  4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

9.一次http请求的过程

  1. 域名解析(这个说了一下域名解析的过程) ,解析出对应IP地址

  2. 解析成功之后,发起TCP三次握手建立连接

  3. 建立连接后发起HTTPS请求

  4. 服务器响应https请求,浏览器得到html代码

  5. 浏览器解析html代码,并请求静态资源(html/css/js等)

  6. 然后浏览器渲染,展示给用户

10.计算机网络、TCP、UDP、http

7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层 ;其中高层(即7、6、5、4层)定义了应用程序的功能,下面3层(即3、2、1层)主要面向通过网络的端到端,点到点的数据流

TCP的优点:TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。 TCP的缺点: 慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。

UDP的优点: 快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击…… UDP的缺点: 不可靠,不稳定 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。

什么时候应该使用TCP: 当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。 在日常生活中,常见使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输 ………… 什么时候应该使用UDP: 当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。 比如,日常生活中,常见使用UDP协议的应用如下: QQ语音 QQ视频 TFTP ……

TCP三次握手的过程如下:

  1. 客户端发送SYN(SEQ=x)报文给服务器端,进入SYN_SEND状态。

  2. 服务器端收到SYN报文,回应一个SYN (SEQ=y)ACK(ACK=x+1)报文,进入SYN_RECV状态。

  3. 客户端收到服务器端的SYN报文,回应一个ACK(ACK=y+1)报文,进入Established状态。

TCP四次挥手过程:

//todo

小结TCP与UDP的区别:

1.基于连接与无连接;
2.对系统资源的要求(TCP较多,UDP少);
3.UDP程序结构较简单;
4.流模式与数据报模式 ;

5.TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。

tcp协议和udp协议的差别

 

TCP

UDP

是否连接

面向连接

面向非连接

传输可靠性

可靠

不可靠

应用场合

少量数据

传输大量数据

速度

 

 

 

 

 

 

 

 

 

 

TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCPIP两个协议,而是指一个由FTPSMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议

11.DNS

使用DNS域名解析,利用域名解析进行负载均衡,然后再请求实际的机器

12.三次握手和四次握手

https://blog.csdn.net/qq_38950316/article/details/81087809

 

因为 TCP 是全双工,每个方向都必须进行单独关闭。

  1. 客户端关闭连接时,发送给服务器FIN指令

  2. 当 Server 端收到 FIN报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client端,”你发的 FIN 报文我收到了”。

  3. 只有等到 Server 端所有的报文都发送完了,我才能发送 FIN 报文,因此不能一起发送。

  4. 客户端收到,回复ask,进入TIME-WAIT状态

  5. 服务端收到客户端ask,接入close状态

13.讲讲负载均衡的原理

 

14.如何实现高并发环境下的削峰、限流?

 

 

15.高并发设计思路

 

16.加密,对称加密和非对称加密

加密算法:

  • 单向散列加密:MD5 SHA

  • 对称机密:DES、RC

  • 非对称加密:公钥加密,私钥解密,RSA算法(https传输中浏览器使用的数字证书实际上就是非对称机密)

17.Dubbo有哪些模块,底层通信的原理?Dubbo 集群的负载均衡有哪些策略?

 

18.常用的负载均衡,该怎么用,你能说下吗?

 

19.一致性hash算法

 

20.快速排序

快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。

(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值。

(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

 

一趟快速排序的算法是:

1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;

2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];

3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]的值交换;

4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换;

5)重复第3、4步,直到i==j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

代码块
public static int[] qsort(int arr[],int start,int end) {        
    int pivot = arr[start];        
    int i = start;        
    int j = end;        
    while (i<j) {            
        while ((i<j)&&(arr[j]>pivot)) {                
            j--;            
        }            
        while ((i<j)&&(arr[i]<pivot)) {                
            i++;            
        }            
        if ((arr[i]==arr[j])&&(i<j)) {                
            i++;            
        } else {                
            int temp = arr[i];                
            arr[i] = arr[j];                
            arr[j] = temp;            
        }        
    }        
    if (i-1>start) arr=qsort(arr,start,i-1);        
    if (j+1<end) arr=qsort(arr,j+1,end);        
    return (arr);    
}    
View Code

21.mybatis #和$的区别

   #{}: 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,一个 #{ } 被解析为一个参数占位符 ,可以防止sql注入

   ${}: 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。

22.mybatis 映射

 

23.定时任务原理

  1. Portal:统一的UI界面

  2. Scheduler:调度模块,采用原生Java提供的延迟队列,对于每个任务,预先生成下一次调度实例放入延迟队列,当调度实例被消费后,随即生成下一次实例重新放入队列,如此循环,驱动任务周期性调度。

  3. Zookeeper:注册中心

  4. Manager:管理调度器模块,Manager 通过Zookeeper进行选主,主节点对调度进行租约发放。

  5. Monitor:Monitor启动后会注册自己节点,并且定时更新时间戳。所有存活的Monitor都会对客户端进行心跳检测,N个Monitor理论上每个Monitor会负责的1/N的客户端。当某个Monitor宕机时,其他Monitor检测到时间戳1分钟未更新,会自动删除注册节点,从而将其所负责的客户端接管过来。

24.定时任务:上一次没有执行完,处理策略

上一次没执行完,阻塞,直到上一次执行完,下一个定时任务才开始

25.LRU算法实现

参考:https://blog.csdn.net/mysteryhaohao/article/details/51072657

原理:LRU(Least Recently Used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

实现:

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

1. 新数据插入到链表头部;

2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

3. 当链表满的时候,将链表尾部的数据丢弃。

分析

【命中率】

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。

【复杂度】

实现简单。

【代价】

命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

26.LRU-K算法

原理

LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

实现

相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下:

1. 数据第一次被访问,加入到访问历史列表;

2. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

3. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

4. 缓存数据队列中被再次访问后,重新排序;

5. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。

分析

【命中率】

LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。

【复杂度】

LRU-K队列是一个优先级队列,算法复杂度和代价比较高。

【代价】

由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多;当数据量很大的时候,内存消耗会比较可观。

LRU-K需要基于时间进行排序(可以需要淘汰时再排序,也可以即时排序),CPU消耗比LRU要高。

27.LRU-2算法

原理

Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。

实现

当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。详细实现如下:

1. 新访问的数据插入到FIFO队列;

2. 如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;

3. 如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;

4. 如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;

5. LRU队列淘汰末尾的数据。

 

注:上图中FIFO队列比LRU队列短,但并不代表这是算法要求,实际应用中两者比例没有硬性规定。

分析

【命中率】

2Q算法的命中率要高于LRU。

【复杂度】

需要两个队列,但两个队列本身都比较简单。

【代价】

FIFO和LRU的代价之和。

2Q算法和LRU-2算法命中率类似,内存消耗也比较接近,但对于最后缓存的数据来说,2Q会减少一次从原始存储读取数据或者计算数据的操作。

28 DDD领域驱动设计

核心域:决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。举例:出库、入库、加工。

通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。举例:抽象出通用的功能,eg扫码、sso、权限

支撑域:还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。举例:日志打印、数据仓库等

贫血模型:UserBo是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。业务逻辑集中在UserService中。我们通过UserService来操作UserBo。换句话说,Service层的数据和业务逻辑,被分割为BO和Service两个类中。像UserBo这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)

充血模型:在基于贫血模型的传统开发模式中,Service层包含Service类和BO类两部分,BO是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在Service类中。在基于充血模型的DDD开发模式中,Service层包含Service类和Domain类两部分。Domain就相当于贫血模型中的BO。不过,Domain与BO的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而Service类变得非常单薄。总结一下的话就是,基于贫血模型的传统的开发模式,重Service轻BO;基于充血模型的DDD开发模式,轻Service重Domain。

 

八.准备

1.开发过程中遇到的最大难题,技术难题

 

 

2.库存系统的高并发

 

3.几亿条数据中找到重复的数据

 

4.微博设计

 

5.短网址映射

 

九.算法

文档:数据结构与算法之美

 

posted @ 2022-04-08 14:17  刘尊礼  阅读(153)  评论(0)    收藏  举报