Java 常见面试题总结(仅用于自己复习使用)

类加载的过程

类加载的过程包括:加载、验证、准备、解析、初始化,其中验证、准备、解析统称为连接。

加载:通过一个类的全限定名来获取定义此类的二进制字节流,在内存中生成一个代表这个类的java.lang.Class对象。

验证:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:为静态变量分配内存并设置静态变量初始值,这里所说的初始值“通常情况”下是数据类型的零值。

解析:将常量池内的符号引用替换为直接引用。

初始化:到了初始化阶段,才真正开始执行类中定义的 Java 初始化程序代码。主要是静态变量赋值动作和静态语句块(static{})中的语句。


Map集合的遍历方式 

在这里插入图片描述

 //4.map集合的迭代方式一
        /**方式一:
         * 遍历map中的数据,但是map本身没有迭代器,所以需要先转换成set集合
         * Set<Key>:把map中的所有key值存入到set集合当中--keySet()*/
        //4.1将map集合中的key值取出存入set集合中,集合的泛型就是key的类型Integer
        Set<Integer> keySet = map.keySet();
        //4.2想要遍历集合就需要获取集合的迭代器
        Iterator<Integer> it = keySet.iterator();
        //4.3循环迭代集合中的所有元素
        while(it.hasNext()){//判断是否有下一个元素可以迭代
            Integer key = it.next();//拿到本轮循环中获取到的map的key
            String value = map.get(key);
            System.out.println("{"+key+","+value+"}");
        }

        /**方式二:
         * 遍历map集合,需要把map集合先转成set集合
         * 是把map中的一对键值对key&value作为一个Entry<K,V>整体放入set
         * 一对K,V就是一个Entry*/
        Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
        //获取迭代器
        Iterator<Map.Entry<Integer, String>> it2 = entrySet.iterator();
        while(it2.hasNext()){//判断是否有下一个元素可迭代
            //本轮遍历到的一个Entry对象
            Map.Entry<Integer, String> entry = it2.next();
            Integer key = entry.getKey();//获取Entry中的key
            String value = entry.getValue();//获取Entry中的value
            System.out.println("{"+key+","+value+"}");
        }

方法一:普通的foreach循环,使用keySet()方法,遍历key

for(Integer key:map.keySet()){
       System.out.println("key:"+key+" "+"Value:"+map.get(key));
        }

方法二:把所有的键值对装入迭代器中,然后遍历迭代器

 Iterator<Map.Entry<Integer,String>> it=map.entrySet().iterator();
      while(it.hasNext()){
          Map.Entry<Integer,String> entry=it.next();
          System.out.println("key:"+entry.getKey()+" "
                  +"Value:"+entry.getValue());
      }

方法三:分别得到key和value

for(Integer obj:map.keySet()){
            System.out.println("key:"+obj);
        }

        for(String obj:map.values()){
            System.out.println("value:"+obj);
        }

方法四,entrySet()方法

Set<Map.Entry<Integer,String>> entries=map.entrySet();
        for (Map.Entry entry:entries){
            System.out.println("key:"+entry.getKey()+" "
                    +"value:"+entry.getValue());
        }


Java数据类型


 Switch支持的数据类型


 continue, break, return之间的区别


explain关键字查询索引使用情况,type字段属性

all,index,range,ref,eq_ref,const,用于mysql引擎优化的sql语句分析,从左到右,性能依次增强,all代表全表扫描,index索引表查询……



缓存雪崩、缓存穿透、缓存击穿

缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
  • 缓存预热
  • 互斥锁

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案:

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同—条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:

  • 设置热点数据永远不过期。
  • 加互斥锁

说一下ArrayList和LinkedList区别

1.底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的
2.由于底层数据结构不同,他们所适用的场景也不同,

ArrayList更适合随机查找,(底层是数组,get(1)查询速度快)

ArrayList.add(b)速度相对快,
但是ArrayList.add(1,b)速度分情况,在添加元素的时候,ArrayList需要找到对应位置,这个过程相对来说较快,但是在指定位置添加元素时,相对较慢,应为要对数组扩容,然后将对应位置的后面元素后移

Linkedlist更适合删除,首位操作

Linkedlist查询速度相对较慢,(底层是双向链表,需要遍历链表对半查找才基于get(1)下标查询)
但首尾查询速度快,因为Linkedlist中有first,last属性分别记录

Linkedlist.add(b)时,相对来说速度较快,只需要在尾部直接添加即可
Linkedlist.add(1,b)时,这个需要看情况,如果下标值大,Linkedlist遍历时间长;如果下标值小,Linkedlist遍历时间短,速度也相对快


3.ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做双端队列来使用
 


JDK,JRE,JVM之间区别


 JVM虚拟机

 JVM调优工具Arthas:http://arthas.gitee.io/

类装载子系统: 用于在程序运行前,将所有的字节码数据加载到方法区中,之后程序运行。
程序执行引擎: 用于执行代码(按行执行),能够获取目前程序执行到的位置,且会将线程执行到的行号记录到程序计数器中。
元空间:从JKD1.8开始的新叫法,其前身为方法区,和方法区的区别为:元空间上的数据是直接保存在磁盘的物理内存上的。保存的数据:所有的类(.class文件)信息
程序计数器:会为每个线程分配一块空间,用于记录该线程执行到的代码行号。行号是由程序执行引擎记录进来的。
本地方法栈:服务于本地方法的,若调用的方法为native方法,则该方法中产生的局部变量均会保存到本地方法栈中,除此之外,其用法和虚拟机栈的用法是相同的。
虚拟机栈:每创建一个线程,在虚拟机栈中会为该线程开辟一块区域,该线程中的局部变量会保存在该区域中。在线程的区域中,以栈帧的方式来保存对应方法中的局部变量的。即每调用一个方法,会在栈中为该方法开辟一块栈帧区域,该方法中产生的局部变量会保存在这个栈帧区域中。
 


1.递归必须有出口,否则会栈内存溢出。(StackoverflowError-- SoF)
2.递归是有深度的,使用时深度不能太深,否则可能会造成栈内存溢出
soF:栈内存溢出
OOM:堆内存溢出outofMemoryError
创建的对象太大或内存泄漏的一直累积会造成堆内存溢出。
内存泄漏:分配出去的内存回收不回来,无法重复利用,这种现象叫做内存泄漏内存溢出:剩余的内存不足以分配给请求的资源,会造成内存溢出。
内存溢出的原因:2个
1.创建的对象太大,超过堆内存的可用空间,直接溢出2.内存泄漏的一直累积,最终会造成内存溢出。


你们项目如何排查JVM问题

对于还在正常运行的系统:
1.可以使用jmap查看JVM中各个区域的使用情况
2.可以通过jstack查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
3.可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
4.通过各个命令的结果,或者jvisualvm/Arthas等工具来进行分析
5.就像我们之前的项目中频繁发生fullgc,但是又一直没有出现内存溢出,说明fullgc实际上是回收了很多对象了,这些对象最好能在年轻代就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,改完之后,fullgc减少,则证明修改有效
6.可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发生了OOM的系统;
1.一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件(-X-+HeapDumpOnOutOfMemoryEror -XHeapDumpPath=/usr/local/base)
2.我们可以利用jsisualvm等工具来分析dump文件
3.根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
4.然后再进行详细的分析和调试
总之,调优不是一蹴而就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题


Jdk1.7到Jdk1,8 java虚拟机发生了什么变化?

1.7中存在永久代,1.8中没有永久代,替换它的是元空间,元空间所古的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是方法区的具体实现,之所以元空间所占的内存改成本地内存,官方的说法是为了和JRocki统一,不过额外还有一些原因,比如方法区所存储的类信息通常是比较难确定的,所以对于方法区的大小是比较难指定的,太小了容易出现方法区溢出,太大了又会占用了太多虚拟机的内存空间,而转移到本地内存后则不会影响虚拟机所占用的内存


Java死锁如何避免?

造成死锁的几个原因:
1.—个资源每次只能被一个线程使用
2.一个线程在阻塞等待某个资源时,不释放已占有资源
3.—个线程已经获得的资源,在未使用完之前,不能被强行剥夺
4.若干线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁
⒉.要注意加锁时限,可以针对所设置一个超时时间
3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决


如何查看线程死锁

1.可以通过jstack命令来进行查看,jstack命令中会显示发生了死锁的线程
⒉.或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况

1、查询是否锁表
show OPEN TABLES where In_use > 0;
2、查询进程
show processlist;
3、查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA. INNODB_LOCKS;
4、查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; 


说一下HashMap的Put方法先说HashMap的Put方法的大体流程:

1.根据hash(key)%n(n数组下标)得出数组下标
⒉如果数组下标位置元素为空,则将KV封装为(JDK1.7中是Entry对象,JDK1.8中是Node对象)并放入该位置
3.如果数组下标位置元素不为空,则要分情况讨论:
1).如果是JDK1.7,遍历该链表元素,判断K是否存在并对链表长度计数,若存在对应K则覆盖对应的V,
若不存在对应K,判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
2).如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node
a.如果是红黑树Node,需要遍历红黑树节点元素,判断K是否存在,若存在对应K则覆盖对应的V ,若不存在对应K,则将K和V封装为红黑树节点,添加到红黑树中去.
b.如果此位置上的Node对象是链表节点,需要遍历链表,判断K是否存在并对链表长度计数,若存在对应K则覆盖对应的V ; 若不存在对应K,则将K和V封装为链表节点,通过尾插法插入到链表的最后位置去,再判断是否需要进行扩容,若链表的节点个数超过了8,则会将该链表转成红黑树


谈谈ConcurrentHashMap的扩容机制

1.7版本
1.1.7版本的ConcurrentHashMap是基于Segment分段实现的
2.每个Segment相对于一个小型的HashMap
3.每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
4.先生成新的数组,然后转移元素到新数组中
5.扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
1.1.8版本的ConcurrentHashMap不再基于segment实现
2.当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
3.如果某个线程put时,发现没有正在进行扩容,则将key -value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
4.ConcurrentHashMap是支持多个线程同时扩容的
5.扩容之前也先生成一个新的数组
6.在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作


泛型中extends和super的区别

1.<? extends T>表示包括T在内的任何T的子类
2.<? super T>表示包括T在内的任何T的父类


并发编程三要素?

1.原子性:不可分割的操作,多个步骤要保证同时成功或同时失败
2.有序性:程序执行的顺序和代码的顺序保持一致
3.可用性:一个线程对共享变量的修改,另一个线程能立马看到




简述CAP理论

CAP理论是分布式领域非常重要的一个理论,很多分布式中间件在实现时都需要遵守这个理论,其中:1.C表示一致性:指的的是分布式系统中的数据的一致性
2.A表示可用性:表示分布式系统是否正常可用
3.P表示分区容器性:表示分布式系统出现网络问题时的容错性
CAP理论是指,在分布式系统中不能同时保证C和A,也就是说在分布式系统中要么保证c,要么保证AP,也就是一致性和可用性只能取其一,如果想要数据的一致性,那么就需要损失系统的可用性,如果需要系统高可用,那么就要损失系统的数据一致性,特指强一致性。
CAP理论太过严格,在实际生产环境中更多的是使用BASE理论,BASE理论是指分布式系统不需要保证数据的强一致,只要做到最终一致,也不需要保证一直可用,保证基本可用即可。|


图的深度遍历和广度遍历

1.图的深度优先遍历是指,从一个节点出发,一直沿着边向下深入去找节点,如果找不到了则返回上一层找其他节点
⒉图的广度优先遍历只是,从一个阶段出发,向下先把第一层的节点遍历完,再去遍历第二层的阶段,知道遍历到最后一层


TCP的三次握手和四次挥手

TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输.
在建立TCP连接时,需要通过三次握手来建立,过程是:
1.客户端向服务端发送一个SYN
⒉.服务端接收到SYN后,给客户端发送一个SYN_ACK
3.客户端接收到SYN_ACK后,再给服务端发送一个ACK
在断开TCP连接时,需要通过四次挥手来端口,过程是:
1.客户端向服务端发送FIN
⒉服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
3.服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接
4.客户端收到服务端的FiN,向服务端发送ACK,表示客户端也会断开连接了


Jdk1.7到Jdk1.8 HashMap发生了什么变化(底层)?

1)1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体效率
2)1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和nvalue时需要判断链表元素个数,所以需要遍历铠表统计链表元素个数,所以正好就直接使用尾插法
3.1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为复杂的哈希算法的目的就是提高款列性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源


HashMap简述

HashMap的结构是 数组+链表 或者 数组+红黑树 的形式
HashMap 底层的Entry[ ]数组初始容量为16,加载因子是0.75f,扩容按约为2倍扩容
当存放数据时,会根据hash(key)%n算法来计算数据的存放位置,n就是数组的长度,其实也就是集合的容量
当计算到的位置之前没有存过数据的时候,会直接存放数据
当计算的位置,有数据时,会发生hash冲突/hash碰撞
解决的办法就是采用链表的结构,在数组中指定位置处以后元素之后插入新的元素
也就是说数组中的元素都是最早加入的节点
如果 链表的长度>8且数组长度>64时,链表会转为红黑树,当链表的长度<6时,红黑树会重新恢复成链表


介绍一下Spring,读过源码介绍一下Spring启动的大致流程

1.Spring框架是一个开放源代码的J2EE应用程序框架,是管理Bean生命周期的轻量级容器,其核心技术 IOC_DI和AOP
2.在创建Spring容器,也就是启动spring时:
a.首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
b.然后筛选出非懒加载的单例lBeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefintion去创建
c.利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
3.单例Bean创建完了之后,Spring会发布一个容器启动事件
4.Spring启动结束
5.在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoyPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
6.在spring启动过程中还会去处理@Import等注解


说一下Spring的事务机制

1. Spring事务底层是基于数据库事务和AOP机制的
2.首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean
3.当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
4.如果加了,那么则利用事务管理器创建一个数据库连接
5.并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交
6.然后执行当前方法,方法中会执行sql
7.执行完当前方法后,如果没有出现异常就直接提交事务
8.如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务

//9.Spring事务的隔离级别对应的就是数据库的隔离级别
10. Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
11. pring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制于置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sqlI


什么时候@Transactional失效rollback

1.因为Spring事务是基于代理来实现的,所以某个加了@Tansactional的方法只有是被代理对象调用时,那么这个注解才会生效
2.如果某个方法是private的,那么@Tansactona也会失效,因为底层Cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效
3.如果出现的异常类型不属于rollback指定的异常的话,也会导致@Transactianal失效


如何理解Spring Boot中的Starter

starter就是定义一个starter的jar包,写一个@Configuration配置类、将这些bean定义在里面,然后在starter包的META-INF/spring.factories中写入该配置类,springboot会按照约定来加载该配置类
开发人员只需要将相应的starter包依赖进应用,进行相应的属性配置(使用默认配置时,不需要配置),就可以直接进行代码开发,使用对应的功能了


说说你了解的分布式锁实现

分布式锁所要解决的问题的本质是:能够对分布在多台机器中的线程对共享资源的互斥访问。在这个原理上可以有很多的实现方式:
1.基于Mysql,分布式环境中的线程连接同一个数据库,利用数据库中的行锁来达到互斥访问,但是Mysql的加锁和释放锁的性能会比较低,不适合真正的实际生产环境
⒉.基于Zookeper, Zookeeper中的数据是存在内存的,所以相对于Mysq)性能上是适合实际环境的,并且基于zookeeper的顺序节点、临时节点、Watch机制能非常好的来实现的分布式锁
3.基于Redis,Redis中的数据也是在内存,基于Redis的消费订阅功能、数据超时时间,lua脚本等功能,也能很好的实现的分布式锁


Redis的数据结构及使用场景

Redis的数据结构有:
1.字符串;可以用来做最简单的数据缓存,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、Session共享、分布式ID
2.哈希表:可以用来存储一些key-value对,更适合用来存储对象
3.列表:Redis的列表通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据
4集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集操作,从而可以实现类似,我和某人共同关注的人、朋友圈点赞等功能
5.有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能


Redis集群策略

Redis提供了三种集群策略:
1.主从模式:这种模式比较简单,主库可以读写,并且会和从库进行数据同步,这种模式下,客户端直接连主库或某个从库,但是但主库或从库宕机后,客户端需要手动修改IP,另外,这种模式也比较难进行扩容,整个集群所能存储的数据受到某台机器的内存容量,所以不可能支持特大数据量
⒉.哨兵模式:这种模式在主从的基础上新增了哨兵节点,但主库节点宕机后,哨兵会发现主库节点宕机,然后在从库中选择一个库作为进的主库,另外哨兵也可以做集群,从而可以保证但某一个哨兵节点宕机后,还有其他哨兵节点可以继续工作,这种模式可以比较好的保证Redis集群的高可用,但是仍然不能很好的解决Redis的容量上限问题。
3.Cluter模式: Cluster模式是用得比较多的模式,它支持多主多从,这种模式会按照ikey进行槽位的分配,可以使得不同的key分散到不同的主节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果该主节点宕机,会从它的从节点中选举一个新的主节点。
对于这三种模式,如果Redis要存的数据量不大,可以选择哨兵模式,如果Redis要存的数据量大,并且需要持续的扩容,那么选择C(uster模式.


lnnodb是如何实现事务的

Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以一个update语句为例:1.Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中2.执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
3.针对update语句生成一个RedoLog对象,并存入LogBuffer中
4.针对update语句生成undolog日志,用于事务回滚
5.如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中6.如果事务回滚,则利用undolog日志进行回滚


消息队列如何保证消息可靠传输

消息可靠传输代表了两层意思,既不能多也不能少。
1.为了保证消息不能重复,也就是生产者不能重复生产消息,或者消费者不能重复消费消息
a,首先要确保消息不多发,这个不常出现,也比较难控制,因为如果出现了多发,很大的原因是生产者自己的原因,如果要避免出现问题就需要在消费端做控制
b.要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决生产者重复发送消息的问题

⒉.消息不能丢失,生产者发送的消息,消费者一定要能消费到
a.生产者发送消息时,要确认broker确实收到并持久化了这条消息比如RabbitMQ的confim机制,Kafka的ack机制都可以保证生产者能正确的将消息发送给brokerb. broker要等待消费者真正确认消费到了消息时才删除掉消息,这里通常就是消费端ack机制,消费者接收到—条消息后,如果确认没问题了,就可以给broker发送一个ack, broker接收到ack后才会删除消息


二叉搜索树 和 平衡二叉树 有什么关系?

平衡二叉树也叫做平衡二叉搜索树,是二叉搜索树的升级版,二叉搜索树是指节点左边的所有节点都比该节点小,节点右边的节点都比该节点大,而平衡二叉搜索树是在二叉搜索的基础上还规定了节点左右两边的子树高度差的绝对值不能超过1


强平衡二叉树和弱平衡二叉树有什么区别

强平衡二叉树AVL树,弱平衡二叉树就是我们说的红黑树。
1.AVL树比红黑树对于平衡的程度更加严格,在相同节点的情况下,AVL树的高度低于红黑树
2.红黑树中增加了一个节点颜色的概念
3.AVL树的旋转操作比红黑树的旋转操作更耗时


B树和B+树的区别,为什么Mysql使用B+树

B树的特点:
1.节点排序
2.—个节点了可以存多个元素,多个元素也排序了
B+树的特点:
1.拥有B树的特点
2.叶子节点之间有指针
3.叶子节点中存储了所有的元素,并且排好顺序
Mysql索引使用的是B+树,因为索引是用来加快查询的,而B+树通过对数据进行排序所以是可以提高查询速度的,然后通过一个节点中可以存储多个元素,从而可以使得B+树的高度不会太高,在Mysql中一个nnodb页就是一个B+树节点,一个mnodb页默认16kb,所以一般情况下一颗两层的B+树可以存200万行左右的数据,然后通过利用B+树叶子节点存储了所有数据并且进行了排序,并且叶子节点之间有指针,可以很好的支持全表扫描,范围查找等sQL语句。


#{}和${}的区别是什么?

#{}是预编译处理、是占位符,${}是字符串替换、 是拼接符。
Mybatis在处理#{时,会将sql中的#{}替换为?号,调用PreparedStatement来赋值;
Mybatis在处理${}时,就是把${}替换成变量的值, 调用Statement来赋值;
#{}的变量替换是在DBMS中、量替换后, #{} 对应的变量自动加上单引号
${}的变量替换是在DBMS外、变量替换后, ${} 对应的变量不会加上单引号
使用#{可以有效的防止SQL注入,提高系统安全性。


描述一下Spring Bean的生命周期?

1、解析类得到BeanDefinition
2、如果有多个构造方法,则要推断构造方法3、确定好构造方法后,进行实例化得到一个对象
4、对对象中的加了@Autowired注解的属性进行属性填充
5、回调Aware方法,比如BeanNameAware,BeanFactoryAware
6、调用BeanPostProcessor的初始化前的方法
7、调用初始化方法
8、调用BeanPostProcessor的初始化后的方法,在这里会进行AOP9、如果当前创建的bean是单例的则会把bean放入单例池
10、使用bean
11、Spring容器关闭时调用DisposableBean中destory()方法


线程的生命周期

1.线程通常有五种状态,创建,就绪,运行、阻塞和死亡状态。
⒉.阻塞的情况又分为三种:
(1)、等待阻塞:调用Object.wait ()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁。只有调用了notify ()或notifyAll()后等待池的线程才会开始去竞争锁,notify ()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
(2)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则VM会把该线程放入"锁池"中。
(3)、其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep状态超时、join等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。sleep是Thread类的方法

1.新建状态(New) :新创建了一个线程对象。
⒉就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4.阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
5.死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。


sleep().wait().join().yield()的区别

1、sleep是Thread类的静态本地方法,wait则是Object类的本地方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。
5、sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信。
6、sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。
yield ()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
join ()执行后线程进入阻塞状态,例如I在线程B中调用线程A的join (),那线程B会进入到阻塞队列,直到线程A结束或中断线程


对线程安全的理解

不是线程安全、应该是内存安全,堆是共享内存,可以被所有线程访问
当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的
是进程和线程共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是进程分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。
是每个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。操作系统在切换线程的时候会自动切换栈。栈空间不需要在高级语言里面显式的分配和释放。
目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。


 说一下ThreadLocal

1.Threadlocal是线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是Threadlocal对象)中都存在一个ThreadlocalMap,Map的K为Threadocdl对象,Map的V为需要缓存的值

 3.如果在线程池中使用Threadlocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadlocalMap,ThreadLoclMap也是通过强引用指向Enty对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象
4.ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)


线程之间如何进行通讯的

1.线程之间可以通过共享内存或基于网络来进行通信
2.如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
3.像Java中的wait()、notify()就是阻塞和唤醒
4.通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式


简述线程池原理,FixedThreadPool用的阻塞队列是什么

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
1.如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
⒉.如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
3.如果此时线程池中的数量大于等于corePooSize,缓冲队列wcorkCueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
4.如果此时线程池中的数量大于corePoolSize,缓冲队列lworkOueue满,并且线程池中的数量等于maimumPolSize,那么通过handler所指定的策略来处理此任务。
5.当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数
FixedThreadPool代表定长线程池,底层用的LinkedBlockingQueue,表示无界的阻塞队列。


线程池中阻塞队列的作用?
为什么是先添加列队而不是先创建最大线程?

1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于—直占用cpu资源
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。
就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task >core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。


sychronized和ReentrantLock的区别

1. sychronized是一个关键字,ReentrantLock是一个类
2.sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
3. sychronized的底层是JVM层面的锁,ReentrantLock是APl层面的锁
4. sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock所的是线程,通过代码中int类型的state标识来标识锁的状态
6. sychronized底层有一个锁升级的过程


重定向和请求转发的区别

①请求转发是请求request的功能,全程1次请求1次响应,地址栏不变
     request.getRequestDispather("目标访问的方式").forward(请求,响应)
②重定向是response的功能,全程2次请求2次响应,地址栏改变
      response.sendRedirect("目标访问的方式")

    /**
     * 要求客户端重定向到指定路径
     * @param uri
     */
    public void sendRedirect(String uri){
        /*
            重定向的响应中,状态代码为302
            并且应当包含一个响应头Location,用来指定浏览器需要重定向的路径
         */
        //修改状态代码
        statusCode = 302;
        statusReason = "Moved Temporarily";

        //响应头
        addHeader("Location",uri);
    }

​​​​​​​


浅拷贝/深拷贝

浅拷贝: 浅拷贝只复制某个对象的引用,而不复制对象本身,新旧对象还是共享同一块内存.
深拷贝: 深拷贝会创造一个一摸一样的对象,新对象和原对象不共享内存,修改新对象不会改变原对对象。可以通过序列化和反序列化或者implement Cloneable实现


epoll和poll的区别

1. select模型,使用的是数组来存储Socket连接文件描述符,容量是固定的,需要通过轮询来判断是否发生了1o事件
2.poll模型,使用的是链表来存储Socket连接文件描述符,容量是不固定的,同样需要通过轮询来判断是否发生了I0事件
3.epoll模型,epoll和poll是完全不同的,epoll是一种事件通知模型,当发生了10o事件时,应用程序才进行l0操作,不需要像poll模型那样主动去轮询


简单讲一下java的跨平台原理

        由于各操作系统(windows,liunx等)支持的指令集,不是完全一致的。就会让我们的程序在不同的操作系统上要执行不同程序代码。
        Java通过不同的系统、不同版本、不同位数的java虚拟机(jvm),来屏蔽不同的系统指令集差异而对外提供统一的接口(java API).
        对于我们java开发者而言,只需要按照接口开发即可。如果我系统需要部署到不同的环境时,只需在系统上面安装对应版本的虚拟机即可。


聊聊你对面向对象的理解

面向过程:一种编程思想,它强调的是过程,意思是我们做任何事,都需要亲力亲为
面向对象: 一种编程思想,相对于面向过程,我们的身份可以由原来问题的执行者变为指挥者,进而把生活中很多复杂的问题变得简单化。

有四大基本特征:封装、抽象、继承、多态

封装:即将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变。

继承:子类继承父类的非私有资源和非构造方法,子类可以拓展自己独有功能,也可以对父类的功能进行重写,子类和父类之间存在强耦合.     SpringBoot中AOP默认使用的CGLIB动态代理就是使用了代理对象继承目标对象,使得代理对象和目标对象具有相似的功能,代理对象和目标对象看起来几乎一模一样.

多态:是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定
抽象:就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处,并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。 就是把现实生活中的对象,抽象为类。

在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。遗产继承


分布式架构下,Session 共享有什么方案

session存在于服务器端,cookie存在于客户端
1、采用无状态服务,抛弃session
2、存入cookie(有安全风险)
3、服务器之间进行Session同步,这样可以保证每个服务器上都有全部的Session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;
4、IP绑定策略
使用Nginx(或其他复杂均衡软硬件)中的IP绑定策略,同一个IP只能在指定的同一个机器访问,但是这样做失去了负载均衡的意义,当挂掉一台服务器的时候,会影响一批用户的使用,风险很大;
5、使用Redis存储
把Session放到Redis 中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:
·实现了Session共享;
·可以水平扩展(增加Redis服务器);
·服务器重启Session不丢失(不过也要注意Session在Redis中的刷新/失效机制);
·不仅可以跨服务器Session共享,甚至可以跨平台(例如网页端和APP端)。


简述你对RPC、RMI的理解

RPC:在本地调用远程的函数,远程过程调用,可以基于HTTP/TCP/UDP,可以跨语言实现httpClient
RMI:远程方法调用,java中用于实现RPC的一种机制,RPC的java版本, J2EE的网络调用机制,跨JVM调用对象的方法,面向对象的思维方式

直接或间接实现接口java.rmi.Remote 可以被远端服务访问

远程对象必须实现java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷则以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为"存根”,而服务器端本身已存在的远程对象则称之为“骨架"。其实此时的存根是客户端的一个代理,于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。


Spring的AOP底层原理动态代理机制

①JDK代理

  1. 动态代理创建代理对象的API Proxy.newProxyInstance(loader,interfaces,invocationHandler);
  2. 动态代理的参数几个/分别是谁 1.类加载器 2.接口数组 3.InvocationHandler 处理器
  3. 代理对象调用方法时 ,为了扩展方法内容,调用处理器方法.
package com.jt.demo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKProxy {

    //传入target目标对象获取代理对象的
    //利用代理对象 实现方法的扩展
    public static Object getProxy(Object target) {
        //1.获取类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        //2.获取接口数组类型
        Class[] interfaces = target.getClass().getInterfaces();
        //3.代理对象执行方法时的回调方法(代理对象调用方法时,执行InvocationHandler)
        return Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("事务开启");
                //获取目标方法的返回值
                Object result = method.invoke(target, args);
                System.out.println("事务提交");
                return result;
            }
        });
    }
}

未实现接口是会出现如下异常:

 ②CGLIB代理

  1. cglib动态代理需要导入额外的包,才能使用. 
  2. cglib 要求被代理者有无接口都可以, 但是cglib代理对象是目标对象的子类. (继承)
  3. cglib创建代理对象时慢,但是运行期速度快.
  4. CGlib代理工具API: Enhancer 增强器对象 获取代理对象 enhancer.create(); 回调接口MethodInterceptor接口
package com.jt.proxy;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGlibProxy {

    public static Object getProxy(Object target){
        //1.创建增强器对象
        Enhancer enhancer = new Enhancer();
        //2.设定父级 目标对象
        enhancer.setSuperclass(target.getClass());
        //3.定义回调方法 代理对象执行目标方法时调用
        enhancer.setCallback(getMethodInterceptor(target));
        //4.创建代理对象
        return enhancer.create();
    }
    //需要传递target目标对象
    public static MethodInterceptor getMethodInterceptor(Object target){
        return new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("事务开始");
                //执行目标方法
                Object result = method.invoke(target,args);
                System.out.println("事务提交");
                return result;
            }
        };
    }
}

  1. Spring中默认采用的动态代理的规则是JDK代理,如果被代理者没有接口.则自动使用CGLIB,如果需要修改为cglib代理,可以使用该注解的属性@EnableAspectJAutoProxy(proxyTargetClass = true)
  2. SpringBoot中默认代理模式采用CGLIB代理.如果需要修改为JDK代理则需要修改配置文件即可

 c面试题-1_闪耀太阳的博客-CSDN博客

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值