面试八股文

1.class类加载:

被编译成jvm看得懂的.class文件后会被类加载器加载,类加载器自上而下分为bootstrapClassLoader,ExtClassLoader,AppclassLoader这三大类,用户还能自己实现类加载器,一般会默认集成appclassloader,然后会触发双亲委派机制,自下而上查找要加载的类是否已经被父类加载,如果已经加载则不加载,如果类加载器不能加载此类,则自上而下的开始委派,知道找到能执行的类加载器。

双亲委派:

在java api官方文档中提到the basic idea of the classloader is a “parent” classloader, 所以这里的双亲的意思并不是实现两个接口的双亲,而是代指父辈

JVM内存分配:

JMM主要分成堆和栈 堆上主要存储类对象的实例和字符串的常量池, 栈上是线程栈或者虚拟机栈,此栈上有局部变量表,方法等信息,此外还有本地方法栈,程序计数器,这些都是线程私有的,元空间即方法区,存储的是类的相关的信息。

2.GC:

2.1 GC算法了解一下?

从回收的维度讲,有三种算法,标记清除算法,标记整理算法,标记复制算法

标记清除算法:对没有root对象指向的先进行标记,然后第二次直接对没有标记的清除,会出现很多的内存碎片

标记整理算法: 同样对没有root对象指向的先进行标记,再第二次不会直接清除,而是将其倒向一边,然后清除,这样会消除小的内存碎片

标记复制算法: 同样对没有root对象指向的标记,然后会将内存区域分成两边,一边用于清除,一边不放对象,将该被回收的对象复制到清除区域,这样的劣势在于损失了一半的工作区域,而且一般对象的存活概率没有一半,所以用一半的工作区是非常浪费的

从判断对象该不该回收的算法而言,主要有计数器法和可达性算法

计数器法是对每个对象进行计数,一根指针计数+1,失去-1,为0时标记为可回收,但是循环依赖的问题无法解决

可达性算法时对JVM的对象,即class或者存活的线程或者说一定存活的对象为root,然后从root出发找与其有引用的对象,没有被指针指向的标记为可回收

2.2 堆上内存了解一下?

堆上分为eden区 surivior0 surivor1 和老年代区 一般是8:1:1

当堆中出现大对象或者大数组时,如果老年代内存足够,则会直接将其放入老年代中,防止其在新生代区一直触发minor gc且一直无法被回收导致一直在0 和1区来回复制浪费资源

当年龄标记>15时也会被直接移动到老年代

如果eden区内存不够时则会触发minor gc 弱引用也会被标记 他们不会被立即清除 而是放入一个消除队列,如果在这段时间内还没有引用指向它,则会被彻底消除

2.3 各种GC收集器了解一下?

jvm系列(三):java GC算法 垃圾收集器 - 纯洁的微笑 - 博客园 (cnblogs.com)

gc收集器:

serial : 单线程 很慢 stoptheworld很久

partnew 多线程版的serial 还是阻塞 很慢

cms 并发模式,只回收老年代,stw很短暂,但是还是会有影响,用的是标记-清除方法,会产生很多小的内存碎片

g1 并发模式 , 全局回收,可以预测暂停时间,回收用的是标记-复制和标记-整理方法,不会产生小的内存碎片,在第一次标记该被清除后,也不会被立即清除,因为此时用户会有操作,在最后会有一个并发标记或者说最终标记的过程,

3.JVM调优:

调整新生代和老年代的比例,尽量让所有的新对象都在新生代触发minor gc时就被回收,而不是进入老年代触发major gc和full gc而stw

检查死锁:JStack

有很多可视化工具让我们调优,比如jconsole Visual vm都可以查看死锁,堆内存等情况

并发:

在一段时间内几个线程可以交替进行

并行:

在一段时间内几个线程同时运行

4.集合相关

4.1 讲一下集合的分类?

java中list和map的底层实现原理 - 雨飞 - 博客园 (cnblogs.com)

集合,Collections,下分为两个大类,map和list,list下有arraylist,linkedlist,vector,list下又有大类set,如TreeSet,HashSet,LinkedHashSet

,map下主要为hashmap,hashtable,concurrenthashmap,TreeMap

4.2 上面的数据结构的区别

Map是键值对类型的数据结构,list是单列值的数据结构

list下比较典型的两个实现,arraylist和linkedlist的主要区别是arraylist底层是基于数组实现的,而linkedlist是基于链表实现的,所以arraylist在查询时会远快于linkedlist,而且分配空间时会直接分配连续内存空间,没有内存碎片,而linkedlist的插入和删除则比arraylist快

从线程安全的层面讲,vector在list中是线程安全的,concurrenthashmap和hashtable是线程安全的

从有序的层面讲,TreeSet和TreeMap都是有序的,通过实现Comparable或者Compartor接口可实现自定义排序

4.3HashMap HashSet ConCurrentHashMap

HashMap: 1.8之前时数组+链表实现,当有元素想put时,会先进行hashcode算法,如果进行hash算法之后发现此数组下标位置没有元素,则直接加入,如果有元素,则进行equals()操作,如果equal结果为false, 则通过尾插法挂在该节点后,如果为true,就覆盖

1.8之后结构变成了数组+链表+红黑树防止因为hashcode导致链表长度过长,延长操作时间,如果hashmap容量<64 则先对数组进行扩容,否则如果链表长度>8,则触发红黑树平衡机制

如果HashMap<> map = new HashMap<>(100000);

则当前map容量是 2的最接近10000的幂即17,为130000 x 负载因子 0.75 < 100000;则插入第100001时会触发扩容

HashSet底层是基于HashMap实现的,去重也是通过equals和hashcode重写实现的

如果hashcode一致,equals不一定一致

equals一致 则hashcode必定一致

所有重写了equals方法就必须重写hashcode

ConcurrentHashMap:

是线程安全版的HashMap

在1.7之前是通过segment+数组+链表实现的,通过对每个segment的加锁,实现了并发编程

在1.8之后改为了Node+数组+链表+红黑树,通过对每个链表的头节点加锁,实现了并发编程

4.4 为什么负载因子选择0.75

如果负载因子为1的话,则会导致数组过于稀疏,利用率不高

如果负载因子趋近为0的话,则会导致链表过长,即使有红黑树做优化,也会很长

0.75是Java开发者们根据事实得到的最优解

4.5 为什么转成红黑树是8,而转化成链表是6
4.6 为什么hashmap每次扩容两倍
4.7 hashmap的并发问题

5. 多线程:

如何实现线程池:?

ThreadPoolExecutor, Executors, 官方手册建议使用ThreadPoolExecutor,可以使用户更了解线程池的本质,ThreadPoolExecutor的构造器有几个核心参数,

CorePoolSize : 核心线程数 一般设置为5

maxPoolSize: 最大工作线程数

keepAliveTime: 不是核心线程数的工作线程的额外存活时间

BlockingQueue: 阻塞队列, 被阻塞的任务挂在此队列

TimeUnit: keepAliveTime的单位

Reject-----: 拒绝策略

当核心线程满时,如果还有任务进入,不会立刻新增线程,而是挂在阻塞队列上,如果阻塞队列满,才会新增线程,且当前线程数不得大于最大线程数,当任务完成后,经过keepalivetime,此工作线程就会被杀死

线程的几大状态:?

新建 new

可运行: runnalbe -> ready and running 不区分这两个状态的原因是在现在高速cpu下,ready状态和running状态的转换非常频繁

等待: 需要等待别的线程的资源才能进行下一步工作,如IO等

超时等待:设置了timewaiting参数,有机会返回

阻塞: 等待锁

死亡: 线程结束

sleep和wait的区别:
sleep不会释放锁,wait会释放锁

!!! 面经题目总览:

1.JAVA基础相关

1.1 i++ , ++ i

java中i=i++问题解析 - 知乎 (zhihu.com)

public void test() {

        int i = 1;
        i = i++;
        i = ++i;
        int j = i++;
        int k = ++j;
        int m = i++ * ++i;
        System.out.println("m: " + m);
}
1.2 值传递相关

F:\桌面\JavaGuide-main\docs\java\basis

就一句话,对于java而言,所有的传参传的都是参数的拷贝,

public class Test {
    public static void main(String[] args) {
        Integer i = 10;
        Integer j = 5;
        int o = 10;
        int k = 5;

        Test test = new Test();
        System.out.println("i:" + i + " j:" + j);
        System.out.println("o:" + o + " k:" + k);
        test.doSwap(i, j);
        System.out.println("i:" + i + " j:" + j);
        test.doSwap2(o, k);
        System.out.println("o:" + o + " k:" + k);
    }
    public void doSwap(Integer a, Integer b) {
        Integer temp = a;
        a = b;
        b = temp;
    }
    public void doSwap2(int a, int b) {
        int temp = a;
        a = b;
        b = temp;
    }
}
1.3 HashMap中的两个参数是怎样影响这个容器的

HashMap 的负载因子 - 简书 (jianshu.com)

首先两个参数分别是initialcapacity 和load factor, 第一个参数指初始化的map的大小,第二个是负载因子,一般默认为0.75,这两个参数一起决定了resize的条件,即threshold,如果负载因子过大的话或者说趋近于1,那么只有当hashmap真正意义的满时才会扩容,会很浪费查询时间,因为很容易出现hash碰撞,如果负载因子过小或者说趋近于0的话,那就会很快的触发扩容,浪费空间。

1.4 String,StringBuilder,StringBuffer的区别

Java 中 String 与 StringBuffer 和 StringBuilder 的区别-阿里云开发者社区 (aliyun.com)

String是java中的常用类,而StringBuilder和StringBuffer比较像是操作String的工具类

StringBuilder是线程不安全的,StringBuffer是线程安全的,如果在单线程且追求速度的情况下,选择StringBuider

这三者都对字符串进行操作时,比如添加字符串,用String操作会在堆上生成新的字符串,很浪费空间,且产生内存碎片,而StringBuilder和StringBuffer不会

1.5 说一下CMS

jvm系列(三):java GC算法 垃圾收集器 - 纯洁的微笑 - 博客园 (cnblogs.com)

CMS是现在java虚拟机比较常用的一种垃圾回收器,它是并发且针对老年代回收的垃圾回收器,注重用户体验,会尽量减少stw的时间

CMS的垃圾回收主要分为四个部分:

  1. 初始标记,会短暂的stw,标记那些不可达对象
  2. 并发标记,与用户程序并发执行,不会stw
  3. 重新标记,标记在2阶段标记时产生的垃圾,会stw
  4. 并发清除,通过标记清除算法清除垃圾,会产生很多的垃圾碎片

与之相类似是G1收集器,是一种逐渐流行的垃圾回收器,是对新生代和老年代都回收的垃圾回收器,与CMS不同的是它建立可预测的停顿模型,即停顿时间会小于等于规定时间,而清除阶段和CMS类似,在初始标记阶段还会触发一次minor gc,而且在回收垃圾时使用的算法时标记整理和标记复制一起使用,会减少内存碎片,节省空间

1.6 数组和链表最基本的区别

最基本的区别在于内存存储,数组的存储会开辟一片连续的内存空间,而链表的内存在非连续的空间中,这也间接导致了数组的查询效率高,添加和删除效率低,而链表的添加和删除效率高,查询效率低

1.7 JVM类加载过程

jvm类加载的过程 - coderLC - 博客园 (cnblogs.com)

JVM类加载过程主要分为三个部分,

加载: 主要作用是将类的信息加载到虚拟机的内存区域。堆上会生成一个class对象作为获取类数据的入口,然后会把类的数据结构转换为方法区能存储的数据结构

连接: 连接分为三个步骤,验证,准备,解析,这三个步骤主要就是保证语义,词法,文法的合理性,然后还会为类上的静态变量分配内存,但是不会初始化

第三步才会初始化为类上的静态变量赋值的操作都是在这一步完成的,加载此类时还会检查父类是否被初始化,如果没有被初始化就会先初始化父类

1.8 双亲委派机制有可能被打破吗

首先我们先介绍一下什么时双亲委派,这是类加载的一种机制,有三个重要的类加载器,bootstrapclassloader,extclassloader,applicationclassloader,还有用户自定义的类加载我们就叫它UserDesignClassLoader,当有一个类要被加载时,会先自底向上的像它的父类们查询此类是否已经被加载了,如果已被加载就不加载,如果没有被加载,就会自顶向下的开始尝试加载子类秒如果本类无法加载,就委派给子类,如果一直没有找到,就抛出claasnotdoundexception

双亲委派机制是可以打破的,核心就是重写loadclass方法,loadclass默认会向上查找向下委派,重写此方法就会直接的打破,具体类似的实现有tomcat,似乎是打破了双亲委派机制

1.9 volatile关键字

volatile是多线程中的常用关键字,保证了多线程并发安全中的可见性,还禁止了指令的重排序,jvm提供了一种jmm内存模型,这是保证并发安全的基础,当给一个变量加了volatile关键字后,每当这个变量发生改变时,此变量会被立刻刷到内存中,然后强制使用了这个变量的其他线程强行重新读取此变量到各自的工作副本中,使用volatile还满足了happens-before原则

1.10 synchronize关键字和lock

synchronize是关键字,是虚拟机层面的锁,而lock是个类,对api层次的锁

synchronize不支持请求中断,而lock提供了允许中断的相关操作

synchronize运行完会自动释放资源,而lock需要手动释放,所以lock操作块的常用结构是try-catch-finally,在finally中保证释放资源

synchronize和reentrantlock都是可重入锁

synchronize可以锁对象,方法,代码块,而lock只能锁对象

synchronize的操作是定死的,没有办法自己修改,而lock提供了丰富的api给用户操作,所以当需要拓展操作的时候,可以考虑lock

2. 操作系统相关

2.1 CPU的调度方式

宏观上从可不可以抢占上分为可抢占调度和不可抢占调度

微观上细分可以分为时间片轮转调度,短作业优先调度(可抢占,不可抢占),最高响应比优先调度(可抢占,不可抢占),先到先得

2.2 操作系统为什么有物理地址和逻辑地址

有物理地址和逻辑地址的主要作用是让用户不能直接操作实际的物理地址,可以防止用户恶意破环操作系统,维护了安全性,有了逻辑地址,与用户直接交互的就是逻辑地址,再由逻辑地址经过映射去寻找具体的物理内存地址

2.3 为什么分页管理内存

一文让你看懂内存与CPU之间的关系 - 云+社区 - 腾讯云 (tencent.com)

我们可以先考虑一下没有分页如何管理内存,还是会有虚拟内存和物理内存地址的映射关系,但是如果我们此时切换进程,那么我们需要调换的内存是很大的,这显然是很浪费系统资源的,但是如果我们引入分页管理内存,当我们切换进程的时候,会触发缺页中断,这时就只需要将缺少的内存页调换进来,不用大动干戈的切换所有内存,提升了上下文环境切换效率。其次分页管理还可以解决物理内存不足和地址空间隔离的问题

2.4 进程和线程的区别

进程是最早的调度单位和内存资源分配单元,进程是程序的一次的动态的执行过程,而经过计算机的飞速发展,人们发现进程之间的切换非常的损耗资源,故而提出了线程的概念,线程是现在操作系统最小的调度单位,所属于每个进程下的线程公用一个内存资源,而且一个进程下的几个线程对别的进程下的线程是不可见的,线程比进程轻的多,线程之间的切换不需要切换内存资源,所以切换速度和效率比进程快的多

2.5 进程间通信的方式

进程主要有几种通信方式:

无名管道,无名管道是半双工通信,只能在有亲缘关系的进程之前通信

有名管道: 有名管道对比无名管道的区别仅在于允许了非亲缘关系的进程通信

消息队列通信: 指的是把要通信的消息放到一个队列数据结构中,克服了传输量小的问题

信号量通信: 信号量是一种对共享资源访问的控制机制,通常与PV操作一起

信号: 信号通信的底层非常复杂,大概就是类似于java多线程的notify和wait的操作

共享内存: 共享内存指的是允许别的进程也可以访问这一块内存,实现信息交互

套接字通信: 套接字通信有点像tcp,他会现在两个进程之间建立连接,然后通过read和write操作通信

2.6 BIO NIO ASYNC-IO

深入底层,Linux五种IO模型全解析 - 知乎 (zhihu.com)

2.7 操作系统中的锁
2.8 死锁的必要条件和死锁的避免

3. 网络相关

3.1 TCP/IP协议

TCP和UDP的区别(转) - bizhu - 博客园 (cnblogs.com)

tcp/ip协议不仅仅指这二种协议,而是一整个协议族

tcp/ip协议族涵盖了应用程序层,传输层,网络层

应用程序层的协议由http协议,telnet协议,ftp协议,DNS等等

传输层包含tcp协议和udp协议

网络层主要有,ip协议等

网络接口层有arp,rarp等

此协议族中最重要的就是tcp协议了

3.2 三次握手

「图文详解」TCP为啥要3次握手和4次挥手?3次挥手不行吗? - 云+社区 - 腾讯云 (tencent.com)

客户端向服务端发送SYN=1的字段申请同步,服务器收到后发送ACK=1字段代表收到请求和SYN=1告知同步序列号,然后客户端再发送ACK=1

三次握手的过程其实是为了确认连接可靠,连接可靠意味着客服端收发正常,服务端收发正常,再精确一点的讲,客户端和服务端还要知道对方是收发正常的,这个三次握手的过程就是为了确认这个的

第一次握手,服务端可以确认自己收正常,对方发正常

第二次握手,客户端确认自己发正常,收正常,对方收正常,对方发正常,

第三次握手,服务端就能确认自己发正常,对方收正常

这样就建立了可靠的连接

3.3 四次挥手

「图文详解」TCP为啥要3次握手和4次挥手?3次挥手不行吗? - 云+社区 - 腾讯云 (tencent.com)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kAZPILnJ-1649230547621)(C:\Users\86180\AppData\Roaming\Typora\typora-user-images\image-20220305161924600.png)]

客户端向服务端发送FIN=1的消息,告知我断开客户端到服务器的连接

服务端回发ACK=1的消息,告知客户端我知道你想断开了,但是服务端不会立即断开,因为此时服务端可能还有消息没有传输完

等到服务端消息传送完后,服务端才会发送FIN=1消息告知客户端我关闭服务器到客户端的连接了,最后客户端发送ACK=1字段告知我确认收到 了

最后连接就完全断开了

举例说明: A和B打电话,说了一大通之后A说我说完了,B呢就说我知道了,但是B可能还有要说的话,然后就巴拉巴拉说,说完之后B就说我也说完了,然后A知道了,这样通话才算完全结束

3.3.1 time_wait

这最主要是因为两个理由:

1、为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。

2、他还可以防止已失效的报文段。客户端在发送最后一个ACK之后,再经过经过2MSL,就可以使本链接持续时间内所产生的所有报文段都从网络中消失。从保证在关闭连接后不会有还在网络中滞留的报文段去骚扰服务器。

注意:在服务器发送了FIN-ACK之后,会立即启动超时重传计时器。客户端在发送最后一个ACK之后会立即启动时间等待计时器。

3.4 TCP是如何保证可靠传输的

计算机网络——TCP的拥塞控制(超详细) - 特务依昂 - 博客园 (cnblogs.com)

通过三次握手和四次挥手保证了连接和断开连接的可靠

通过拥塞控制,流量控制,校验和,滑动窗口,超时重传,最大消息长度,确认应答等方法保证了数据可靠

校验和: 指经过一些运算如且运算判断发送的消息是否正确 是一种数据检验机制

超时重传: 客户端向服务端发送数据时,如果过了一定时间还没有收到ack,则会重传数据

最大消息长度: 客户端和服务端会通过通信来约定一个最大消息长度,从而方便传输

确认应答: 客户端发送数据后服务器收到会发送ack,如果没有收到则会重发

滑动窗口: 滑动窗口是因为超时重传效率过低而衍生出来的方法,例如客户端向服务端先发送1-1000序号的数据,成功后服务端会标记自己的数据接受应该接受1001号数据,此时客户端如果发送1001-2000号数据失败或者没有发送这段数据,直接发送2001-3000号数据,这时服务端因为知道自己要接受的时1001号数据,所以会要求客户端发1001号数据,如果经过几次应答服务端还是要求1001号数据,那么客户端就会认为这段数据丢失了,会重新发送

拥塞控制: 拥塞控制是怕突然有大量的连接请求或者数据的收发导致服务拥塞,拥塞控制主要用拥塞窗口实现:

首先的问题是如何判断网络拥塞的问题,分为两种,一种是端到端判断,指由主机之前判断是否拥塞,比如超时未收到或者多次冗余确认主机就会判断此时网络拥塞了,还有一种是通过网络的中间服务层判断,比如路由器,如果网络中数据太多使得路由器的收发压力很大,路由器就会告知网络拥塞了,请求减少压力。

拥塞控制是tcp可靠传输最重要的方法,如果没有拥塞控制,比如很多请求阻塞在路由器,然而网络的发送效率和路由器的处理效率差不多,就会陷入恶性循环,甚至会丢失数据

拥塞窗口是维护网络吞吐量的一个动态窗口,如果网络没有拥塞,就会增大此窗口,反之减小

拥塞窗口的大小主要由两种算法实现,慢开始和拥塞避免,

慢开始指的是网络会在传输开始阶段先试探一下,每次传输成功一次会将cwnd增加一个mss的长度,知道增加到慢开始的阈值,此时会人文已经接近网络拥塞的阈值了,就会转入拥塞避免算法,在此算法下cwnd的大小还是在缓慢的先行增长的,如果遇到网络超时或者触发快重传,就会消减cwnd大小然后重新开始

3.5 一次URL请求背后的具体过程

在浏览器发送一次url请求,会先经过DNS协议映射到相应的ip地址,此过程会优先查找缓存

然后浏览器向找到的ip地址发送http请求,并且会携带cookie

然后服务器会处理请求和参数,如果还需要向后端发送请求获得数据,此时如果是ajax异步请求就会一边生成页面一边向后端发送请求

然后将html页面响应给浏览器

最后浏览器开始渲染页面

此过程会使用tcp协议向服务端申请建立连接,

然后会使用ip协议在网络层层传递数据,

然后用arp协议找到mac地址

最后通过http协议发送请求

3.6 HTTP 1.0/1.1/2.0的不同

面试官:说说 HTTP1.0/1.1/2.0 的区别? | web前端面试 - 面试官系列 (vue3js.cn)

3.7 https的加密

彻底搞懂HTTPS的加密原理 - 知乎 (zhihu.com)

4. 框架相关

(192条消息) Spring常见面试题总结(超详细回答)_张维鹏的博客-CSDN博客_spring面试题

4.1 spring事务中的7种传播行为

传播行为指的是对于事务的操作,即遇到一个事务时,是新建一个事务还是并入当前事务执行还是直接以无事务方式运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v9mTvqpU-1649230547622)(C:\Users\86180\AppData\Roaming\Typora\typora-user-images\image-20220302111400490.png)]

REQUIRED: 如果当前没有事务,则新建一个事务,如果有事务,则加入到当前事务中,即如果回滚,则一起回滚

SUPPORTS: 支持当前事务,如果当前无事务,就以无事务状态运行

MANDATORY: 使用当前的事务,如果没有事务,则强制抛出异常

REQUIRES_NEW: 无论如何都会新建事务,如果当前有事务,则挂起当前事务

NOT_SUPPORTED: 不支持事务,如果当前存在事务,则挂起当前事务

NEVER: 不支持事务,如果当前存在事务,则抛出异常

NESTED: 如果当前存在事务,则会嵌套使用,即看做当前事务的子事务,如果没有事务,则和REQUIRED一致, NESTED和REQUIRED的区别在于回滚,NESTED的子事务回滚父事务可以不回滚

4.2 spring中bean的四种作用域

singleton: 单例模式,全局之会创建一个实例

prototype: 原型模式,每次需要都会创建一个实例

request: 每次有一次http请求则创建一个实例,请求结束就销毁

session: 每次会话创建一个实例,会话结束就销毁

4.3 单例bean存在线程安全问题吗?

在大部分情况下是不存在的,导致单例bean存在线程安全的原因是对其成员变量的操作,如果想要避免可以加锁或者用ThreadLocal,但是大部分情况下,单例bean不存在成员变量,如service,controller,所以并不存在线程安全的问题

4.4 说一下spring的aop机制

Spring AOP——Spring 中面向切面编程 - SharpCJ - 博客园 (cnblogs.com)

aop,即aspect oriented program, 面向切面编程,这种编程思想常用于日志等增强型行为,他是一个横向的抽象,举个例子,继承解决了很多代码重复问题,但是对个性化的方法还是要自己写,比如一个run方法和eat方法,我们想知道这两个方法的执行时间,则会在方法开头用System.currentTimeMills和结尾计算,两个方法的代码是一样的,但是无法抽象出来,这是我们就会用到aop来横向增强

aop分为静态代理和动态代理,静态代理指的是在编译期就对源代码进行修改或增强,会直接对字节码进行改变,而且不够灵活

动态代理是在运行时实现的,如果被代理对象实现了某个接口,则用jdk代理,如果没有实现,则通过继承类使用cglib实现

jdk代理的缺点在于需要被切入的类需要实现接口,而cglib的缺点在于如果被切入的类有final关键字则无法进行代理织入

4.5 mybatis一级二级缓存

一级缓存指的是每发送一次select请求,mybatis都会通过sqlsessionfactory创建一个sqlsession会话,在这次会话中,所有的select语句和其结果都会被放进map中,以sql语句做key,结果做value, 在本次会话中,其他的select语句都可以使用此缓存中的数据,但是增删改操作会触发clearcache操作即清空缓存防止数据不一致,二级缓存是一级缓存的升级版,是全局缓存,在整个namespace相同的mapper中都可以共享此缓存,sql查询的顺序是先查询二级缓存,如果二级缓存没有命中再查询一级缓存,一级缓存也没有命中的话才回去查询数据库。在mybatis中,一级缓存是默认开启的,而二级缓存需要手动开启,而且缓存只是mybatis的附带功能,只是顺带的加速查询,不同于redis主要是为了提升速度而会一定程度的牺牲一致性

4.6 AOP

再讨论aop的原理之前,我们先讨论一下什么是aop,aop指的是aspect oriented programming,面向切面编程,是在oop思想即面向对象编程的思想的一种进阶,主要解决的是日志管理等相关问题,举个例子,在oop思想下,一个父类生物类,有几种共同方法,子类有人类和狗之类的,然后呢通过继承我们确实可以节省写公共方法的代码量,但是假如我们再子类中想要输出点什么,或者说要计算方法执行的时间,这种横向的抽象,就是由aop实现的.aop是通过代理实现的,代理有两大类,静态代理和动态代理,静态代理指的是在编译器就实现代理,这种方法非常不灵活,而动态代理又分为JDK代理和CGLIB代理,jdk代理是指被代理对象实现了某个接口,然后代理对象也实现这个接口然后代理被代理的对象然后增强方法,这种代理模式在被代理对象没有实现接口时无法使用,cglib代理是指代理对象会直接继承被代理对象,然后做拓展,这种方法的缺陷在于如果类标记了final关键字就无法继承

4.7 Spring VS SpringBoot VS SpringMVC

https://www.javatpoint.com/spring-vs-spring-boot-vs-spring-mvc

Spring: 意为春天,不得不说,每一款语言的流行,一款优秀的操作框架是必须的,而Spring就是Java中最重要的框架,我对Spring的理解是,它是框架中的框架,可以视作是小框架的大框架,它集成了很多框架比如说Hibernate JPA等, DI和IOC是Spring提供的特性,Spring的主要目的是为了建立一个松耦合的项目

SpringBoot: 意为Spring引导程序,我觉得他是用于操作Spring框架的工具框架,它为Spring框架带来了很多便利,最重要的三点便是独立程序和简化装配和自动装配,stand-alone的原因是SpringBoot内置了Tomcat服务器,运行一个程序时直接运行本项目就行,而不需要再先启动Tomcat服务器再运行项目,简化装配则是相对于Spring,SpringBoot省略了很多不必要代码,比如XML配置文件,自动装配则是通过SpringBoot新增的注解@EnableAutoConfiguration实现的,通过这个注解,会直接自动装配所需配置,SpringBoot出现的目的就是加速开发Spring项目的速度,SpringBoot其实也只是Spring框架中的一个框架

SpringMVC: SpringMVC就是刚才所说的框架里的框架,是Spring集成的对HTTP请求处理的框架,通过操作Model-View-Controller三层操作返回的视图和数据,SpringMVC框架集成了Servlet, 处理请求的流程是通过DispatcherServlet -> HandlerMapping -> Handler -> HandlerAdapter操作具体的controller -> 返回数据

-> 为View渲染数据 ->交给ViewResolver返回视图

4.8 bean的生命周期

当容器创建时,bean的生命周期开始,首先一个bean会被实例化,然后进行依赖注入,传播属性,然后走init()初始化方法,然后经过一系列的功能方法,比如什么什么postprocessor,afterbalala等等,最后经过destroy()被销毁

4.9 循环依赖

循环依赖简单来说就是两个被Spring代理的类,出现了A依赖B,B又依赖A的问题,如果不提供解决方案,那么这两个类就会一直处于increation状态从而报错

对于单例bean的setter注入Spring提供了三级缓存来解决循环依赖,大概来讲第一级缓存是完全被初始化的bean,此级缓存与用户交互,直接为用户提供服务,第二级缓存存的是半初始化bean,即被实例化了但没有进行属性注入,原则上来讲这两级缓存就解决了setter的循环依赖的问题,第二级缓存可以提前暴露无法初始化的bean从而解决循环依赖,但是如果bean需要aop,那么就出现了问题,此时第三级缓存就有作用了,第三级缓存存放的value是objectfactory,用于生成代理对象,他可以把提前生成的代理对象放入二级缓存中然后删除自己缓存的内容,这样第二级缓存存的就是被aop过的代理对象

对于单例bean的构造器注入其实也是有解的,大致方法是在构造器或者直接简单粗暴在类上加@Lazy注解,这样子Spring容器在启动时就不会完全的初始化被注解的类,而是生成代理对象,相当于A类依赖于这个被代理对象,B依赖于A,这样循环依赖就被打破了,当然,这种情况下A类并没有完全的初始化,有点像三级缓存中二级缓存的状态

4.10 @Autowired VS @Resource

@autowired 主要根据类型注入

@resource 可以根据名称注入

@autowired配合@qualifed注解可以按照名称注入,所以可以理解为@autowire的能力包含了@resource的能力

4.11 SpringBoot的自动配置原理

SpringBoot自动配置的核心在于@EnableAutoConfiguation注解,它是@SpringBootApplication注解的子注解之一,他会根据一些像@Conditional注解去加载META-INF下的spring.factories下的配置文件的信息

4.12 Spring加载机制
4.13 afterPropertiesSet 和init-method

postConstruct > afterPropertiesSet > init-method

4.14 factoryBean和Beanfactory

前者是bean的一种实现,后者是个工厂,用于创建bean实例

4.15

5. 中间件相关

5.1 ES
5.1.1 ES的索引方法,倒排索引和关系型数据库的索引有什么区别?
5.2

6. 数据库相关

6.1 Mysql
6.1.1 一个数据库语句查询太慢,你会从哪些方面去进行优化

MySQL查询性能优化 - 小家电维修 - 博客园 (cnblogs.com)

提到数据库语句优化,自然而然的优先会想到索引,如果语句没有索引,则设置区分度大的索引,如果已经存在索引,则通过explain语句检查索引是否生效,如果没有生效,则再检查是否符合最左匹配等原则使得索引失效

抛开索引的优化,还可以对子查询有优化,因为子查询的速度大部分时候远远不如联表查询

还可以考虑是不是因为服务器同时对mysql产生了海量的操作而导致此条语句要查询的表堵塞,再等待资源而导致查询太慢,从这个角度看需要优化的地方就很多

然后再考虑表的设计的问题,看看是不是因为表的设计问题导致冗余过多等而使查询变慢的

最后再考虑硬件方面的问题,检查是不是CPU太垃圾,硬盘空间太小之类的问题导致的

6.1.2 说一下mysql数据库的索引类型

从聚集不聚集的角度讲,大体上可以分成非聚集索引和聚集索引,聚集索引为innodb引擎所特有的,只有对主键加索引的时候才是聚集索引,其他所有的索引都是非聚集索引,聚集索引和非聚集索引的区别在于聚集索引上存的是整行的数据,所以不用回表查询,而非聚集索引存的是主键的数据,需要根据此数据进行回表查询

对索引在细分的话,可以分为主键索引,唯一索引,非空索引,全表索引,覆盖索引,单列索引,多列等等,覆盖索引的话就是对表中所有的字段加索引

6.1.3 联合索引可以用来查询一个列吗?

可以的,因为建立一个联合索引相当于建了几组索引,比如说我们对身份证和姓名建了联合索引,就相当于建了身份证索引,身份证和姓名索引,但是注意,不能对姓名进行单列查询,因为最左前缀原则

6.1.4 B+树

B+树是mysql在建立索引是常用到的一种数据结构,B+树可以看成是B树的进阶版,B+树与B树的不同点在于B+树的非叶子节点上不会挂载数据,而且在叶子节点上还建立了链表,方便查询,而B树则每个节点上都带有数据,查询到一个数据后查询第二个数据需要从新从根节点开始,而B+树只需要遍历链表即可。

如果大量单个key,不适合用B+树,而更适合B树,而范围查询非常适合B+树

6.1.5 select * from table limit xx,50 1000w级别 执行流程,存在的问题,如何有哈

执行流程: 如果没有建立覆盖索引,则不会走索引,它会进行全表扫描,

存在的问题: mysql的limit语句会查询所有数据,然后根据limit再丢弃掉不要的数据,而且如果这个pagenum很大的话,它会扫描出很多无用数据,浪费时间

优化: 优化先考虑走索引,我们可以用子查询的方式优化,如先select id from table order by id desc limit 所要求的 如50000,1,然后父查询select * from table > where id > 子查询的结果 limiit 0 , 1

在这种情况下子查询走主键索引速度会快很多

还有一种优化可以考虑联表查询

即select * from table inner join select id from table limit 50000,10

6.1.6 事务的隔离级别

彻底搞懂 MySQL 事务的隔离级别-阿里云开发者社区 (aliyun.com)

全网最全的一篇数据库MVCC详解,不全我负责-mysql教程-PHP中文网

隔离级别有未提交读,read-uncommited, 已提交读read-commited, 可重复读repeatable-read和序列化serializable

未提交读会存在脏读,不可重复读和幻读问题,脏读指读到另一个事务还未提交的数据

已提交读,解决了脏读问题,其他事务只能读到其他事务已提交的数据,但还是存在不可重复读和幻读

可重复读,解决了不可重复读问题,不可重复读指的是在某一时刻读的值和下一时刻读的值不一样

序列化,串行模式,解决了幻读问题,幻读指的是一个事务在某一时刻只读到了5条数据,结果后一时刻读取到了8条数据,

6.1.7 讲一下MVCC

MVCC,multiple version concurrency control, 多版本并发控制,是用于提高事务的并发效率的解决方案之一,也是事务的RC和RR的具体实现方案,MVCC的实现具体依赖于几个id,首先是db_trx_ids, 记录的是最近操作该行的事务id,还有db_rollback_pointer,回滚指针,配合undolog可以实现读以前版本的数据,还有一个不怎么重要的db_row_id,指的是表没有主键且没有唯一非空索引时,数据库会隐式的创建一个id作为聚集索引,然后readview也是能使得mvcc实现的重要一环,此类中具体有三个字段:up_limit_id,我的理解是最小可见id,low_limit_id,最大可见id,因为在这个区间内的事务如果失活则可见,还有ids是记录活跃事务id的,在一次事务并发中,如果trx_ids小于最小可见id,则此行的更新是可见的,如果大于,则不可见,如果介于两者之前且失活,则可见,最后根据undolog和回滚指针向下索引,重复上述循环,直到找到满足的版本

值得一提的是,RC和RR的隔离级别对MVCC的实现并不一样,RC在每次请求快照读的时候都会产生一个ReadView,而RR只会在第一次请求快照读的时候产生ReadView,这也导致了RC可以读到其他事务已提交的数据而RR只会读到一样的数据,准确一点说,RR情况下,ReadView在第一次请求后就不会更新了,活动的事务和最大可见id和最小可见id几乎可以理解为已经固定了

RR情况下其实已经解决了部分幻读的问题,所有快照读都不会产生幻读,只有当前读会,这时我们可以利用间隙锁来解决,间隙锁指的是锁住当前行和一定范围内的记录,防止别的事务在查询范围内插入数据,这样就解决了幻读的问题。

6.1.8 事务的ACID及其实现

A: atomacy , 原子性,即事务的操作不可再分,是最小单位,要么一起成功,要么一起失败

C:consistency, 一致性,一致性是一种结果,是由其他三种特性而保证的结果,

I: isolation,隔离性,innodb引擎为事务提供了四种隔离级别,分别是未提交读,已提交读,可重复读和序列化,隔离性主要使用MVCC实现的

D: duration, 数据的持久化,即数据一旦存在或者更新,不会因为电脑宕机或数据库宕机而丢失数据

原子性是由undolog实现的,数据库会将操作记录在undolog中,如果成功则提交,如果失败则rollback,触发undolog

隔离性的RC和RR都是由MVCC实现的,具体实现见6.1.7 , 序列化则是完全牺牲事务并发的效率,一个事务必须等待另一个事务提交才可继续操作,当然,读读操作是不阻塞的

一致性的实现基于以上三种特性

6.1.9 索引失效

索引的使用需要符合最左前缀原则,比如建立了c1c2c3三个联合索引,一个索引语句如select * from table where c1 = 2000 and c3 > 2000,不会使用c3索引

需要从左往右使用索引,本质上建立了c1,c1.c2,c1,c2,c3这三组索引

对索引的使用还要避免隐式转换,比如某一个索引列使用的是varchar类型,然后select的where语句中有 colum > 200,mysql会隐式的为其加单引号,导致索引失效

使用模糊查询也容易导致索引失效,不能使用前百分号会导致索引失效

6.1.10 数据库如何实现读写分离
6.1.11 表的行转列

mysql 行转列 列转行 - 平凡希 - 博客园 (cnblogs.com)

6.1.12 分库分表的一致性问题

https://www.cnblogs.com/aigongsi/archive/2013/01/25/2875731.html

6.1.13 left join right join inner join

left join是以左边表为基准的,right join是以右边表为基准的,inner join只返回两表都有的结果

基准的意思是,比如说left join左边表的所以值都会被select出来,但是右边表没有的值会标为null

6.1.14
6.2 Redis
6.2.1 Redis数据结构

主要数据结构有string字符串类型,list双端链表,hash, key-value结构,类似java的hashmap,set,去重集合,类似java的hashset,可以进行集合的交并补操作,还有zset,即实现了排序的set

6.2.2 zset的底层设计

在元素较少的时候,具体值不记得了,好像是64时,会先用ziplist存储,ziplist底层本质上就是一个双向链表,

但当元素数量较多时,就会使用skiplist存储,即跳表,跳表是基于有序链表实现的,会根据一定的规则固定或随机的添加索引,其实就是升级高度,在使用跳表时,查找效率基于链表会从o(n)的复杂度提升到o(logn),举个例子吧,比如底层有个有序链表3,5,8,9,10,然后要变成跳表,在某些时候会进行一个抛硬币的过程,即决定此节点是否升高,如果升高,比如3这里,就会多出一层,然后头节点指向生成的3,此节点有一个down指针和一个right指针,down指针指向它下层相同的节点,right指针指向它同层的下一个节点,在经过一套这个逻辑后,我们假设顶层是3,只有一个节点5,第二层有3,5,9三个节点,如果我们要查询节点8,那么要执行的逻辑就是,先暂存一个head节点,然后从此节点出发,因为它的下一个节点5<查询节点但是他的下一个节点是null,所以它会走down节点,即找到第二层的5,然后他的下个节点9 >8 ,所以可以知道所查询节点在此区间内,他会执行down节点,然后继续此逻辑直到找到节点。

跳表和红黑树的最大的区别在于区间查询,因为跳表是有序的而红黑树是无序的,所以在区间查询是跳表的表现大于红黑树,而且跳表的底层实现比红黑树简单,当然如果执行插入或者删除的话两者的效率可能差不多,甚至跳表表现更差,因为跳表对插入要进行的修改是修改节点指针,如果索引高度较高的话修改会很麻烦,红黑树的操作是变色和左旋右旋

6.2.3 redis哈希表的rehash的过程

redis的hashmap底层是基于字典实现的,每个字典会持有两个表,ht[0]和ht[1],一个表用于存储键值对,一个表用于下次扩容rehash,在触发扩容时,会先改变ht[1]表的大小,然后将ht[0]中的键值对rehash到ht[1]中,但是这个rehash的过程不是一次性完成的,因为redis的出现的初衷是为了解决与数据库操作的速度不匹配而应运而生的,所以它的最主要的目标就是高可用,如果一次性rehash的话,会大大影响程序的执行效率,所以redIs使用的是渐进式rehash,渐进式rehash指的是每次对redis进行增删改查操作时,会顺便将ht[0]的键值对rehash到ht[1]中,每进行一次这个操作,idx++,如果完成rehash,会将idx重置为-1,代表rehash完成

6.2.5 redis单线程,为什么用单线程

单线程和多线程对比的优势是单线程操作不会涉及上下文环境的切换从而浪费资源,单线程也不需要考虑锁和并发安全等降低性能的元素,而且redis单线程使用了多路复用IO模型,通过这种selector-channel,比阻塞IO的速度也快的多

6.2.6 redis与数据库的一致性

一致性问题有四种情况

先更新redis,再更新数据库

先更新数据库,再更新redis

先删除数据库,再删除redis

先删除redis,再删除数据库

对于每种操作,如果完成了第一步操作而第二步失败例如宕机了之类的问题,都会存在缓存不一致问题

对于并发操作,第一种情况而言,

第二种情况而言,更新数据库时,有对redis的读操作,会发现redis与数据库不一致

6.2.7 redis zset为什么不用红黑树
6.2.8 redis 集群
6.2.9 redis持久化
6.2.10 redis分布式锁
6.2.11 bitmap
6.2.12 redis持久化策略
6.2.13 RDB怎么保证主进程写入的同时子进程持久化
6.2.14 redis过期key处理策略
6.2.15 redis为什么这么快

redis是缓存数据库,对redis的操作是与内存的操作,自然远远快于IO硬盘操作

redis是单线程的,没有切换上下文环境的浪费资源和锁等并发安全操作

redis的数据结构相对于mysql非常简单,没有锁之类的概念,对redis的操作简单且快速

redis使用的是IO多路复用模型,快于阻塞IO

7. 多线程相关

7.1 两个线程交替打印1-100

(189条消息) 两个线程交替打印1-100的多种方式_sinat_38705282的博客-CSDN博客_两个线程打印1到100

7.2 无锁,偏向锁,轻量级锁,重量级锁

是1.6之后引入的一种机制,为了不每次直接加重量级锁影响效率,而是根据锁竞争的激烈程度逐渐升级来保证并发安全

无锁: 无锁就是不加锁,对于共享资源的安全实现仅用CAS实现,CAS就是compareAndSet,有三个参数,第一个参数是内存地址,第二个参数是原值,第三个参数是将要set的值,当要更新值的时候,CAS方法会先比较此内存地址里的值是不是原值,如果是,则set新值,如果不是,则不改变

偏向锁: 偏向锁由无锁的状况升级而来,偏向锁指的是对已经获得锁的线程友好,即已经获得了锁的线程是可重入的,不需要重新获得锁

轻量级锁: 轻量级锁是偏向锁的升级版本,是标准的锁,可以理解为自旋锁,当其他线程获取锁失败后,它们不会立即的阻塞挂起切换用户态和内核态浪费资源,而是进入一种循环反复尝试获得锁,如果到达一个阈值还没有获得,则升级为重量级锁

重量级锁: 重量级锁就是再引入锁升级之前默认的锁级别,获取锁失败的线程会直接阻塞挂起

7.3 四种产生线程的方式

实现Runnable接口,实现Callable接口,继承Thread类,线程池提供

dis是单线程的,没有切换上下文环境的浪费资源和锁等并发安全操作

redis的数据结构相对于mysql非常简单,没有锁之类的概念,对redis的操作简单且快速

redis使用的是IO多路复用模型,快于阻塞IO

7. 多线程相关

7.1 两个线程交替打印1-100

(189条消息) 两个线程交替打印1-100的多种方式_sinat_38705282的博客-CSDN博客_两个线程打印1到100

7.2 无锁,偏向锁,轻量级锁,重量级锁

是1.6之后引入的一种机制,为了不每次直接加重量级锁影响效率,而是根据锁竞争的激烈程度逐渐升级来保证并发安全

无锁: 无锁就是不加锁,对于共享资源的安全实现仅用CAS实现,CAS就是compareAndSet,有三个参数,第一个参数是内存地址,第二个参数是原值,第三个参数是将要set的值,当要更新值的时候,CAS方法会先比较此内存地址里的值是不是原值,如果是,则set新值,如果不是,则不改变

偏向锁: 偏向锁由无锁的状况升级而来,偏向锁指的是对已经获得锁的线程友好,即已经获得了锁的线程是可重入的,不需要重新获得锁

轻量级锁: 轻量级锁是偏向锁的升级版本,是标准的锁,可以理解为自旋锁,当其他线程获取锁失败后,它们不会立即的阻塞挂起切换用户态和内核态浪费资源,而是进入一种循环反复尝试获得锁,如果到达一个阈值还没有获得,则升级为重量级锁

重量级锁: 重量级锁就是再引入锁升级之前默认的锁级别,获取锁失败的线程会直接阻塞挂起

7.3 四种产生线程的方式

实现Runnable接口,实现Callable接口,继承Thread类,线程池提供

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值