当前位置:主页 > 查看内容

面试-笔记

发布时间:2021-07-16 00:00| 位朋友查看

简介:class ListNode { int val ; ListNode next ; ListNode ( ) { } ListNode ( int val ) { this . val val ; } ListNode ( int val , ListNode next ) { this . val val ; this . next next ; } } class TreeNode { * int val ; * TreeNode left ; * TreeNode……
 class ListNode {
      int val;
     ListNode next;
      ListNode() {}
      ListNode(int val) { this.val = val; }
      ListNode(int val, ListNode next) { this.val = val; this.next = next; }
  }
class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }

笔记

1.LRU算法java数据结构实现

LRU:(least recently used) 最近最少使用算法。LRU算法的java中数据结构的实现是一个LRU算法在面试中经常被问到的问题,我们可以用LinkedList来表示最近和最少使用。将访问过的数据用LinkedList数据结构进行存储,如果查找最少使用,直接返回LinkedList的尾节点即可。如果添加一个最近访问的数据a,可以将a从链表中的位置删除,移到链表的头部。

但是链表中查询数据DATA的位置一般访问速度慢,所以需要借助其它数据结构来完成查找,我们知道HashMap是一个高效的查询数据结构。如果我们将该数据DATA作为键,DATA对应的节点作为值,存储在HashMap中,那么,我们可以得到查询复杂度为O(1)的操作。

JAVA中已经帮助我们实现了一个这样的数据结构,LinkedHashMap.LinkedHashMap存放的键值对和存放时的位置一致。不会变化,可以设置LinkedHashMap按照插入的顺序排序,也可以按照访问的顺序排序,最先访问的放置在最后面,最近访问的在最前面。

LinkedHashMap自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回false,即不删除数据,我们使用LinkedHashMap实现LRU缓存的方法就是对LinkedHashMap实现简单的扩展,

2.红黑树

在这里插入图片描述

  1. 节点是红色或者是黑色;
  2. 每个叶节点(NIL或空节点)是黑色;
  3. 每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点);
  4. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

优势:红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。但对于插入删除等操作效率提高很多。红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树,在现在很多地方都是底层都是红黑树的天下啦。
红黑树和AVL树都是最常用的平衡二叉搜索树,它们的查找、删除、修改都是O(lgn) time

2.1 AVL树和红黑树有几点比较和区别:

(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。
总结:
(1)AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。
(2)两种实现都缩放为a O(lg N),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。
(3)在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。
(4)两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。

3.HashMap的长度为什么要是2的n次方

HashMap为了存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表长度大致相同,这个实现就在把数据存到哪个链表中的算法;
这个算法实际就是取模,hash%length,计算机中直接求余效率不如位移运算,源码中做了优化hash&(length-1),
hash%length==hash&(length-1)的前提是length是2的n次方;
为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;

4.协程

协程诞生解决的是低速IO和高速的CPU的协调问题,解决这类问题主要有三个有效途径:

?异步非阻塞网络编程(libevent、libev、redis、Nginx、memcached这类)
?协程(golang、gevent)
?“轻量级线程”,相当于是在语言层面做抽象(Erlang)

协程和线程差异

  1. 协程其实可以认为是比线程更小的执行单元。为啥说他是一个执行单元,因为他自带CPU上下文。这样只要在合适的时机,我们可以把一个协程 切换到 另一个协程。只要这个过程中保存或恢复 CPU上下文那么程序还是可以运行的。

  2. 一个线程可以多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。

  3. 线程进程都是同步机制,而协程则是异步

  4. 协程的开销远远小于线程的开销

  5. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的

1.协程(用户级线程)完全由用户自己的程序进行调度(协作式调度),需要协程 自己主动把控制权转让出去之后,其他协程才能被执行到。
2.协程,是在应用层模拟的线程,他避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。协程还是通过共享内存通讯.
目前的协程框架一般都是设计成 1:N 模式。
所谓 1:N 就是一个线程作为一个容器里面放置多个协程。
那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU,
也就是每个协程池里面有一个调度器,这个调度器是被动调度的。
意思就是他不会主动调度。
而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到),
这个时候就可以由这个协程通知调度器,
这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。
切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,
或者它调用主动让出CPU的API之类,触发下一次调度。对的没错就是类似于 领导人模式那么这个实现有没有问题?
其实是有问题的,假设这个线程中有一个协程是CPU密集型的他没有IO操作,也就是自己不会主动触发调度器调度的过程,
那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。

5.HashMap为什么不是线程安全?

jdk7中由于头插法,会存在resize后链表中形成环的情况。
线程1阻塞前链表情况:a->b->null
线程2此时执行,会采用头插法将该链表放入新的table,放入后变成b->a->null
线程1此时返回,同样采用头插法插入该线程自己的新的table中,两次循环后变为b->a,但是由于线程2执行时的后果会导致b的next不是null而是a,所以循环多执行一次,e此时变为a会导致a.next = newTablei然后发生循环。

jdk8造成线程不安全分2中情况;
1.并发执行put操作时会出现hashcode冲突从而导致数据覆盖,造成线程不安全;
链接: hsahmap为什么线程不安全 https://blog.csdn.net/weixin_43092168/article/details/89791106
2.resize的时候造成的,jdk8在(++size>threshold)代码片段,如果并发操作,可能导致两次扩容,但最终结果只有一次扩容的效果,从而线程不安全(循环链表)

6.如何处理TCP的黏包和拆包问题:

通常会有以下一些常用的方法:
1.使用带消息头的协议、消息头存储消息开始标识及消息长度信息,服务端获取消息头的时候解析出消息长度,然后向后读取该长度的内容。
2.设置定长消息,服务端每次读取既定长度的内容作为一条完整消息,当消息不够长时,空位补上固定字符。
3.设置消息边界,服务端从网络流中按消息编辑分离出消息内容,一般使用‘\n’。

7.Spring 中使用了哪些设计模式?

工厂模式:
spring中的BeanFactory就是简单工厂模式的体现,根据传入唯一的标识来获得bean对象;
单例模式:
提供了全局的访问点BeanFactory;
代理模式:
AOP功能的原理就使用代理模式(1、JDK动态代理。2、CGLib字节码生成技术代理。)
装饰器模式:
依赖注入就需要使用BeanWrapper;
观察者模式:
spring中Observer模式常用的地方是listener的实现。如ApplicationListener。
策略模式:
Bean的实例化的时候决定采用何种方式初始化bean实例(反射或者CGLIB动态字节码生成)。
链接: 设计模式https://blog.csdn.net/qq_35190492/article/details/105992063.

8.springMVC流程:

(1):用户请求发送给DispatcherServlet,DispatcherServlet调用HandlerMapping处理器映射器;

(2):HandlerMapping根据xml或注解找到对应的处理器,生成处理器对象返回给DispatcherServlet;

(3):DispatcherServlet会调用相应的HandlerAdapter;

(4):HandlerAdapter经过适配调用具体的处理器去处理请求,生成ModelAndView返回给DispatcherServlet

(5):DispatcherServlet将ModelAndView传给ViewReslover解析生成View返回给DispatcherServlet;

(6):DispatcherServlet根据View进行渲染视图;

->DispatcherServlet->HandlerMapping->Handler
->DispatcherServlet->HandlerAdapter处理handler->ModelAndView
->DispatcherServlet->ModelAndView->ViewReslover->View
->DispatcherServlet->返回给客户

9.skiplist与平衡树、哈希表的比较

1.skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
2.在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
3.平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
4.从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
5.查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
6.从算法实现难度上来比较,skiplist比平衡树要简单得多。

10.线程间通信的几种实现方式

方式一:使用 volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
方式二:使用Object类的wait() 和 notify() 方法
众所周知,Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。
注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁

11.JVM内存模型

JVM内存空间分为五部分,分别是:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器
1.方法区主要用来存放类信息、类的静态变量、常量、运行时常量池等,方法区的大小是可以动态扩展的,
2.堆主要存放的是数组、类的实例对象、字符串常量池等。
3.Java虚拟机栈是描述JAVA方法运行过程的内存模型,Java虚拟机栈会为每一个即将执行的方法创建一个叫做“栈帧”的区域,该区域用来存储该方法运行时需要的一些信息,包括:局部变量表、操作数栈、动态链接、方法返回地址等。比如我们方法执行过程中需要创建变量时,就会将局部变量插入到局部变量表中,局部变量的运算、传递等在操作数栈中进行,当方法执行结束后,这个方法对应的栈帧将出栈,并释放内存空间。栈中会发生的两种异常,StackOverFlowError和OutOfMemoryError,StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。 而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

4.本地方法栈结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法的内存模型。运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法返回地址等,在本地方法执行结束后栈帧也会出栈并释放内存资源,也会发生OutOfMemoryError。

5.最后是程序计数器,程序计数器是一个比较小的内存空间,用来记录当前线程正在执行的那一条字节码指令的地址。如果当前线程正在执行的是本地方法,那么此时程序计数器为空。程序计数器有两个作用,1、字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。2、在多线程的情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。

12.谈谈synchronized与ReentrantLock的区别?

  • ① 底层实现上来说,synchronized 是JVM层面的锁,是Java关键字,通过monitor对象来完成(monitorenter与monitorexit),对象只有在同步块或同步方法中才能调用wait/notify方法,ReentrantLock 是从jdk1.5以来(java.util.concurrent.locks.Lock)提供的API层面的锁。
    synchronized 的实现涉及到锁的升级,具体为无锁、偏向锁、自旋锁、向OS申请重量级锁,ReentrantLock实现则是通过利用CAS(CompareAndSwap)自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。
  • ② 是否可手动释放:
    synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用; ReentrantLock则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。
  • ③ 是否可中断
    synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
  • ④ 是否公平锁
    synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁。
  • ⑤ 锁是否可绑定条件Condition
    synchronized不能绑定; ReentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒,而不是像synchronized通过Object类的wait()/notify()/notifyAll()方法要么随机唤醒一个线程要么唤醒全部线程。
  • ⑥ 锁的对象
    synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

13.ArrayList的大小是如何自动增加的?

1、添加元素时,首先进行判断是否大于默认容量10

2、如果,小于默认容量,直接在原来基础上+1,元素添加完毕

3、如果,大于默认容量,则需要进行扩容,扩容核心是grow()方法

  • 3.1 扩容之前,首先创建一个新的数组,且旧数组被复制到新的数组中
    这样就得到了一个全新的副本,我们在操作时就不会影响原来数组了
  • 3.2 然后通过位运算符将新的容量更新为旧容量的1.5陪(原来长度的一半再加上原长度也就是每次扩容是原来的1.5倍)
  • 3.3 如果新的容量-旧的容量<=0,就拿新的容量-最大容量长度如果<=0的,那么最终容量就是扩容后的容量
4.添加(add)方法的实现,添加时首先检查数组的容量是否满足
  • 1、添加元素时,首先进行判断是否大于默认容量10
  • 2、如果,小于默认容量,直接在原来基础上+1,元素添加完毕
  • 3、如果,大于默认容量,则需要进行扩容,扩容核心是grow()方法
    • 3.1 扩容之前,首先创建一个新的数组,且旧数组被复制到新的数组中
      这样就得到了一个全新的副本,我们在操作时就不会影响原来数组了
  • 3.2 然后通过位运算符将新的容量更新为旧容量的1.5陪
  • 3.3 如果新的容量-旧的容量<=0,就拿新的容量-最大容量长度如果<=0的,那么最终容量就是扩容后的容量

14.索引的数据结构

为什么数据库要用b+树而不用b树,平衡二叉树

1.平衡二叉树

平衡二叉树是采用二分法思维,平衡二叉查找树除了具备二叉树的特点,最主要的特征是树的左右两个子树的层级最多相差1。在插入删除数据时通过左旋/右旋操作保持二叉树的平衡,不会出现左子树很高、右子树很矮的情况。
使用平衡二叉查找树查询的性能接近于二分查找法,时间复杂度是 O(log2n)。查询id=6,只需要两次IO。
就这个特点来看,可能各位会觉得这就很好,可以达到二叉树的理想的情况了。然而依然存在一些问题:
1.时间复杂度和树高相关。树有多高就需要检索多少次,每个节点的读取,都对应一次磁盘 IO 操作。树的高度就等于每次查询数据时磁盘 IO 操作的次数。磁盘每次寻道时间为10ms,在表数据量大时,查询性能就会很差。(1百万的数据量,log2n约等于20次磁盘IO,时间20*10=0.2s)

3.平衡二叉树不支持范围查询快速查找,范围查询时需要从根节点多次遍历,查询效率不高。

2.B树:改造二叉树

MySQL的数据是存储在磁盘文件中的,查询处理数据时,需要先把磁盘中的数据加载到内存中,磁盘IO 操作非常耗时,所以我们优化的重点就是尽量减少磁盘 IO 操作。访问二叉树的每个节点就会发生一次IO,如果想要减少磁盘IO操作,就需要尽量降低树的高度。那如何降低树的高度呢?

  • 这种数据结构我们称为B树,B树是一种多叉平衡查找树,如下图主要特点:
    • 1.B树的节点中存储着多个元素,每个内节点有多个分叉。
    • 2.节点中的元素包含键值和数据,节点中的键值从大到小排列。也就是说,在所有的节点都储存数据。
    • 3.父节点当中的元素不会出现在子节点中。
    • 4.所有的叶子结点都位于同一层,叶节点具有相同的深度,叶节点之间没有指针连接。
  • 缺点:
    • 1.B树不支持范围查询的快速查找,你想想这么一个情况如果我们想要查找10和35之间的数据,查找到15之后,需要回到根节点重新遍历查找,需要从根节点进行多次遍历,查询效率有待提高。

    • 2.如果data存储的是行记录,行的大小随着列数的增多,所占空间会变大。这时,一个页中可存储的数据量就会变少,树相应就会变高,磁盘IO次数就会变大。

15.Nginx

工作流程:

1、用户通过域名发出访问Web服务器的请求,该域名被DNS服务器解析为反向代理服务器的IP地址;
2、反向代理服务器接受用户的请求;
3、反向代理服务器在本地缓存中查找请求的内容,找到后直接把内容发送给用户;
4、如果本地缓存里没有用户所请求的信息内容,反向代理服务器会代替用户向源服务器请求同样的信息内容,并把信息内容发给用户,如果信息内容是非缓存的还会把它保存到缓存中。


Nginx模块:

Nginx有五大优点:模块化、事件驱动、异步、非阻塞、多进程单线程。由内核和模块组成的,其中内核完成的工作比较简单,仅仅通过查找配置文件将客户端请求映射到一个location block,然后又将这个location block中所配置的每个指令将会启动不同的模块去完成相应的工作。


作用:

  • 保护了真实的web服务器,保证了web服务器的资源安全
  • 节约了有限的IP地址资源
  • 减少WEB服务器压力,提高响应速度
    • 反向代理就是通常所说的web服务器加速,它是一种通过在繁忙的web服务器和外部网络之间增加一个高速的web缓冲服务器来降低实际的web服务器的负载的一种技术。反向代理是针对web服务器提高加速功能,作为代理缓存,它并不是针对浏览器用户,而针对一台或多台特定的web服务器,它可以代理外部网络对内部网络的访问请求。
    • 反向代理服务器会强制将外部网络对要代理的服务器的访问经过它,这样反向代理服务器负责接收客户端的请求,然后到源服务器上获取内容,把内容返回给用户,并把内容保存到本地,以便日后再收到同样的信息请求时,它会把本地缓存里的内容直接发给用户,以减少后端web服务器的压力,提高响应速度。因此Nginx还具有缓存功能。
  • 请求的统一控制,包括设置权限、过滤规则等;
  • 区分动态和静态可缓存内容;
  • 实现负载均衡,内部可以采用多台服务器来组成服务器集群,外部还是可以采用一个地址访问;
  • 解决Ajax跨域问题;
  • 作为真实服务器的缓冲,解决瞬间负载量大的问题;

16.理解restful(Representational State Transfer)

(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。

RESTful 架构的核心规范与约束:统一接口

分为四个子约束:
1.每个资源都拥有一个资源标识,每个资源的资源标识可以用来唯一地标明该资源
2.消息的自描述性
3.资源的自描述性。
4.HATEOAS Hypermedia As The Engine Of Application State(超媒体作为应用状态引擎)
即客户只可以通过服务端所返回各结果中所包含的信息来得到下一步操作所需要的信息,如到底是向哪个URL发送请求等。也就是说,一个典型的REST服务不需要额外的文档标示通过哪些URL访问特定类型的资源,而是通过服务端返回的响应来标示到底能在该资源上执行什么样的操作

17.为啥redis zset使用跳跃链表而不用红黑树实现

  • skiplist的复杂度和红黑树一样,而且实现起来更简单。
  • 在并发环境下红黑树在插入和删除时需要rebalance,性能不如跳表。

18.线程状态

  1. 新建(NEW):新创建了一个线程对象。
  2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
  3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
  4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
(二). 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态

  1. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

19.jvm内存模型

链接: JVM内存结构和Java内存模型别再傻傻分不清了.

20.Spring的IOC和AOP

1.IOC本质

控制反转IoC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现IoC的一种方法 也有人认为DI只是IoC的另一种说法。没有IoC的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了。

采用XML方式配置Bean的时候,Bean的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的。
控制反转是一种通过描述(XML或注解)并通过第三方去生产或获取特定对象的方式。在Spring中实现控制反转的是IoC容器,其实现方法是依赖注入(Dependency Injection,DI)。

2.AOP

  • 什么是AOP

AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

  • AOP在Spring中的作用
    提供声明式事务;允许用户自定义切面
  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志,安全,缓存,事务等等…
  • 切面(ASPECT):横切关注点被模块化的特殊对象。即,它是一个类。
  • 通知(Advice):切面必须要完成的工作。即,它是类中的一个方法。
  • 目标(Target):被通知对象。
  • 代理(Proxy):向目标对象应用通知之后创建的对象。
  • 切入点(PointCut):切面通知执行的“地点”的定义。
  • 连接点(JointPoint):与切入点匹配的执行点。

21.synchronized和lock区别以及底层原理

区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

底层原理

synchronized:
原子性:保证语句块内是原子的
可见性:通过在unlock前需要将变量同步回主存,其他线程需要重新获取
有序性:一个变量在同一时刻只允许一条线程对其操作
方法级的同步是通过方法调用和返回中实现的,方法常量池的ACC_SYNCHRONIZED标志是否为同步方法,如果方法是同步方法,则执行线程需要先持有monitor然后执行方法,返回后释放。
代码块的同步是通过monitorenter和monitorexit实现的。遇到monitorenter试图获取monitor对象,如果未加锁或者已经被自己持有,则锁计数器+1,执行,遇到monitorexit则锁计数-1.计数器为0代表锁释放。如果获取monitor对象失败会进入阻塞。且是可重入的。
1.6前慢的原因:对象内部的监视器锁是通过底层OS的Mutex实现的,存在用户态到内核态的切换,成本极高
1.6后的优化:四种锁状态 无锁——偏向锁——轻量级锁——重量级锁。(不可降级)

偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。只有一次CAS
锁对象第一次被获取时,jvm将对象头锁标志位设为01偏向模式,然后通过CAS将线程id记录到对象的markword中。如果成功,该线程在以后每次进入该同步块时,jvm不进行任何操作。如果不是第一次获取锁,则判断偏向线程id是否为当前线程,是的话就进入同步块。否则则根据当前偏向的线程是否存活,未存活则取消锁到无锁状态,存活则升级为轻量级锁。

轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。申请和释放需要CAS
轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,而是在当前线程栈帧中开辟空间Lock Record用来记录当前对象markword的拷贝。然后将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为00轻量级锁;否则,说明已经有线程获得了轻量级锁,如果指向的是当前线程的栈帧,则重入代码块,否则出现了竞争,会尝试几次CAS,如果不行,升级为重量级锁,标志位11,markword中指针指向重量级锁。

重量级锁:有实际竞争,且锁竞争时间长。monitor实现。

Lock: 有三个实现类,ReentrantLock, ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
底层实现为AQS。
AQS:CLH锁队列(双向链表)+state状态变量,线程通过CAS去改变状态,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程)
lock获取锁的过程:本质上是通过 CAS 来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。
lock释放锁的过程:修改状态值,调整等待链表。
lock()-acquire()-tryAcquire()-未成功获取锁-addwaiter()-acquireQueued()
acquireQueued的主要作用是把已经追加到队列的线程节点进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回。

22.Redis持久化

  • RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
  • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。

两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备,AOF更适合做热备,比如我杭州的某电商公司有这两个数据,我备份一份到我杭州的节点,再备份一个到上海的,就算发生无法避免的自然灾害,也不会两个地方都一起挂吧,这灾备也就是异地容灾,地球毁灭他没办法。

RDB优缺点

优点:

他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。
RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。

缺点:

RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。
还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。

AOF优缺点

优点:

上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。
AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。

AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。

缺点:

一样的数据,AOF文件比RDB还要大。
AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志嘛fsync,当然即使这样性能还是很高,我记得ElasticSearch也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的,大家可以思考下为啥哟。

23.Redis和Memcache区别,优缺点对比

redis和memecache的不同在于:

  • 1、存储方式:
    memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小
    redis有部份存在硬盘上,这样能保证数据的持久性,支持数据的持久化(笔者注:有快照和AOF日志两种持久化方式,在实际应用的时候,要特别注意配置文件快照参数,要不就很有可能服务器频繁满载做dump)。
  • 2、数据支持类型:
    redis在数据支持上要比memecache多的多。 例如 list、set、sorted set、hash 等。
  • 3、使用底层模型不同:
    新版本的redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
  • 4、Memcache .处理请求时使用多线程异步 IO 的方式,可以合理利用 CPU 多核的优势,提高性能,与 Memcache不同的是,Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。
  • 5、Redis 提供持久化,主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。

个人总结一下,有持久化需求或者对数据结构和处理有高级要求的应用,选择redis,其他简单的key/value存储,选择memcache。

24.单线程的redis为什么这么快

(一)纯内存操作
(二)单线程操作,避免了频繁的上下文切换
(三)采用了非阻塞I/O多路复用机制

25.Redis 为什么是单线程的

官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问
1)绝大部分请求是纯粹的内存操作(非常快速)2)采用单线程,避免了不必要的上下文切换和竞争条件
3)非阻塞IO优点:
1.速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
2. 支持丰富数据类型,支持string,list,set,sorted set,hash
3.支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
4. 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发竞争key问题

同时有多个子系统去set一个key。这个时候要注意什么呢? 不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。
(1)如果对这个key操作,不要求顺序: 准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可
(2)如果对这个key操作,要求顺序: 分布式锁+时间戳。 假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。
(3) 利用队列,将set方法变成串行访问也可以redis遇到高并发,如果保证读写key的一致性
对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内部已经帮你处理好并发的问题了。

26.MySQL中索引失效的常见场景与规避方法

  1. where语句中包含or时,可能会导致索引失效
  2. where语句中索引列使用了负向查询,可能会导致索引失效
  3. 索引字段可以为null,使用is null或is not null时,可能会导致索引失效
  4. 在索引列上使用内置函数,一定会导致索引失效
    链接: MySQL中索引失效的常见场景与规避方法.

27.InnoDB事务日志(redo log 和 undo log)详解

1.redo log 和undo log

为了满足事务的持久性,防止buffer pool数据丢失,innodb引入了redo log。为了满足事务的原子性,innodb引入了undo log。

2.undo log

Undo log 是为了实现事务的原子性。还用Undo Log来实现多版本并发控制(简称:MVCC)。

delete/update操作的内部机制
当事务提交的时候,innodb不会立即删除undo log,因为后续还可能会用到undo log,如隔离级别为repeatable read时,事务读取的都是开启事务时的最新提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。
但是在事务提交的时候,会将该事务对应的undo log放入到删除列表中,未来通过purge来删除。并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。

通过undo log记录delete和update操作的结果发现:(insert操作无需分析,就是插入行而已)
delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
update分为两种情况:update的列是否是主键列。
如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
如果是主键列,update分两部执行:先删除该行,再插入一行目标行。

  • ①事务的原子性
  • 事务的所有操作,要么全部完成,要不都不做,不能只做一半。如果在执行的过程中发生了错误,要回到事务开始时的状态,所有的操作都要回滚。
  • 原理
      Undo Log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到一个地方(这个存储数据备份的地方称为Undo Log)。然后进行数据的修改。如果出现了错误或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。

3.Redo Log

redo log就是保存执行的SQL语句到一个指定的Log文件,当mysql执行数据恢复时,重新执行redo log记录的SQL操作即可。引入buffer pool会导致更新的数据不会实时持久化到磁盘,当系统崩溃时,虽然buffer pool中的数据丢失,数据没有持久化,但是系统可以根据Redo Log的内容,将所有数据恢复到最新的状态。redo log在磁盘上作为一个独立的文件存在。默认情况下会有两个文件,名称分别为 ib_logfile0和ib_logfile1。

①原理

和Undo Log相反,Redo Log记录的是新数据的备份。在事务提交前,只要将Redo Log持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是Redo Log已经持久化。系统可以根据Redo Log的内容,将所有数据恢复到最新的状态。

②Undo + Redo事务的简化过程

redo & undo log的作用

  • 数据持久化
  • buffer pool中维护一个按脏页修改先后顺序排列的链表,叫flush_list。根据flush_list中页的顺序刷数据到持久存储。按页面最早一次被修改的顺序排列。正常情况下,dirty page什么时候flush到磁盘上呢?

1.当redo空间占满时,将会将部分dirty page flush到disk上,然后释放部分redo log。
2.当需要在Buffer pool分配一个page,但是已经满了,这时候必须 flush dirty pages to disk。一般地,可以通过启动参数 innodb_max_dirty_pages_pct控制这种情况,当buffer pool中的dirty page到达这个比例的时候,把dirty page flush到disk中。
3.检测到系统空闲的时候,会flush。

  • 数据恢复

随着时间的积累,Redo Log会变的很大。如果每次都从第一条记录开始恢复,恢复的过程就会很慢,从而无法被容忍。为了减少恢复的时间,就引入了Checkpoint机制。假设在某个时间点,所有的脏页都被刷新到了磁盘上。这个时间点之前的所有Redo Log就不需要重做了。系统记录下这个时间点时redo log的结尾位置作为checkpoint。在进行恢复时,从这个checkpoint的位置开始即可。Checkpoint点之前的日志也就不再需要了,可以被删除掉。

链接: redo log 和 undo log的详解.

;原文链接:https://blog.csdn.net/weixin_44938368/article/details/115738636
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐