Java面试总结

1.TCP协议的首部结构

​ TCP尽管是面向字节流的,和UDP面向数据报不一样,但是TCP传送的每一个数据单元却是报文段。每一个TCP报文段分为首部和数据两部分,首部的各个字段都很好的体现了TCP的每个功能。TCP报文段首部的前20个字节是固定的,后面有4*n(整数)个字节是根据需要可选的,所以TCP首部的最小长度是20字节。首部字段分别是:源端口、目的端口、序号、确认号、数据偏移、保留、紧急URG、确认ACK、推送PSH、复位RST、同步SYN、终止FIN、窗口、检验和、紧急指针、选项。

2.说一说HashMap的实现原理

在jdk1.7之前HashMap是基于数组和链表实现的,而且采用头插法。

而jdk1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。也就是说,链表转为红黑树的条件是节点数大于8且数组长度大于64。采用尾插法。当从红黑树删除结点时也需要判断,如果删除后结点数小于6,则将红黑树转为单链表。

为什么是6不是8:如果是8,在删除后又进行添加,就会造成红黑树与单链表之间的重复转化,设置为6起到缓冲的作用。

HashMap默认的初始化大小为 16。当HashMap中的元素个数之和大于负载因子*当前容量的时候就要进行扩充,容量变为原来的 2 倍。(这里注意不是数组中的个数,而且数组中和链/树中的所有元素个数之和!)

HashMap是线程不安全的,其主要体现:

1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。

2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

HashMap

对于在扩容时转移数据到新的数组中的策略:

​ 扩容时,e.hash & oldCap==0的元素留在原来的位置,否则新位置=旧位置+oldcap

​ e.hash & oldCap0的元素留在原来的位置则说明 e.hash & (oldCap-1) = e.hash & (2oldCap-1),因为oldCap是2的n次方,所以只有最高位为1,其余位是0,e.hash & oldCap0只有当e.hash与oldCap对应的位置上为0才成立。当e.hash的这个位置为0时,e.hash & (oldCap-1)与e.hash & (2oldCap-1)是相等的,因为

(2oldCap-1)相对于(oldCap-1)而言只是多了1位1,该位对应hash值的位置上为0,所以该位与hash值上对应位上 & 出来的结果为0。所以e.hash & (oldCap-1) = e.hash & (2oldCap-1)。

​ 例如:oldCap = 4 = 0100,8 = 1000,hash1 = 5 = 0101,hash2 = 9 =1001

​ hash1 & oldCap =0101 & 0100 =0100 != 0

​ hash2 & oldCap =1001 & 0100 =0000 = 0

​ hash1 2^2 位上为0所以 hash2 & (oldCap-1) = 1001 & 0011= 1001 & 0111 = hash2 & (2oldCap-1) ,所以在新旧数组中位置相同。

3.HashMap的put方法说一下。

通过阅读源码,可以从jdk1.7和1.8两个方面来回答

  1. 根据key通过哈希算法与运算得出数组下标

  2. 如果数组下标元素为空,则将key和value封装为Entry对象(JDK1.7是Entry对象,JDK1.8是Node对象)并放入该位置。

  3. 如果数组下标位置元素不为空,则要分情况

​ (i) 如果是在JDK1.7,则首先会判断是否需要扩容,如果要扩容就进行扩容,如果不需要扩容就生成Entry对象,并使用头插法添加到当前链表中。

​ (ii) 如果是在JDK1.8中,则会先判断当前位置上的TreeNode类型,看是红黑树还是链表Node

​ (a) 如果是红黑树TreeNode,则将key和value封装为一个红黑树节点并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value。

​ (b) 如果此位置上的Node对象是链表节点,则将key和value封装为一个Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历过程中会判断是否存在当前key,如果存在则更新其value,当遍历完链表后,将新的Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于8,则会将链表转为红黑树。

​ © 将key和value封装为Node插入到链表或红黑树后,再判断是否需要扩容,如果需要扩容,就结束put方法。

4.内存溢出问题该如何解决

内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。

引起内存溢出的原因有很多种,常见的有以下几种:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
3.代码中存在死循环或循环产生过多重复的对象实体;
4.使用的第三方软件中的BUG;
5.启动参数内存值设定的过小;

内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)

第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。

第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。

重点排查以下几点:
1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2.检查代码中是否有死循环或递归调用。

3.检查是否有大循环重复产生新对象实体。

4.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、Map等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。

5.说一说你对Spring IoC的理解

​ IoC是控制反转的意思,是一种面向对象编程的设计思想。在不采用这种思想的情况下,我们需要自己维护对象与对象之间的依赖关系,很容易造成对象之间的耦合度过高。尤其是在一个大型的项目中,对象与对象之间的关系是十分复杂的,这十分不利于代码的维护。IoC则可以解决这种问题,它可以帮我们维护对象与对象之间的依赖关系,并且降低对象之间的耦合度。

​ 在应用开发中开发人员设计组件时往往需要引用和调用其他组件的服务,这种依赖关系如果固化在组件设计中,会造成依赖关系的僵化和维护难度的增加,这个时候使用IoC把资源获取的方向反转,让IoC容器主动管理这些依赖关系,将这些依赖关系注入到组件中,这就会让这些依赖关系的适配和管理更加灵活。

两种注入方式:

  1. 构造方法注入

    就是被注入对象可以在它的构造方法中声明依赖对象的参数列表,让外部知道它需要哪些依赖对象。然后,IoC Service Provider会检查被注入的对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。

  2. setter方法注入

    通过setter方法,可以更改相应的对象属性。所以,当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。setter方法注入虽不像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入。

6. HashMap和HashTable的区别

​ HashTable的操作几乎和HashMap一致,主要的区别在于HashTable为了实现多线程安全,在几乎所有的方法上都加上了synchronized锁,而加锁的结果就是HashTable操作的效率十分低下。

区别
1)Hashtable是线程安全,而HashMap则非线程安全
2)HashMap可使用null作为key,Hashtable不允许null作为key
3)HashMap是对Map接口的实现,Hashtable对Map接口的实现和对Dictionary抽象类的继承
4)HashMap的初始容量是16,Hashtable初始容量是11,两者的填充因子默认都是0.75
5)HashMap与Hashtable计算hash的是方法不同

7. HashMap和TreeMap

​ HashMap是直接实现了Map接口,而TreeMap则是实现了NavigableMap接口,而这个NavigableMap接口拓展了SortedMap接口,SortMap接口又拓展了Map接口。

​ **TreeMap.forEach默认执行中序遍历,按key的升序遍历所有结点。

区别:

1、HashMap无序,TreeMap有序。

2、HashMap覆盖了equals()方法和hashcode()方法,这使得HashMap中两个相等的映射返回相同的哈希值;TreeMap则是实现了SortedMap接口,使其有序。

3、HashMap的工作效率更高,而TreeMap则是基于树的增删查改。更推荐使用HashMap。

4、HashMap基于哈希桶实现,TreeMap是基于红黑树实现。

5、两者都不是线性安全的。

效率区别:

​ TreeMap底层是由树(红黑树)实现的,而HashMap是由哈希桶实现的。由于哈希算法本身的优势,我们再进行增删查改的时候。HashMap的时间复杂度是O(1),是通过哈希函数计算的哈希地址。而我们的红黑树就不具有这样的优势时间复杂度是O(log2n)。

8. ConCurrentHashMap

与HashMap相比,有如下特点

1. 并发安全
2. 支持一些原子操作
3. 支持高并发,读操作完全并行,写操作支持一定的并行
4. 与容器Collections修饰的synchronizedMap想比,迭代不需要加锁,不会抛出与异常
5. 内部使用CAS算法实现,并发性能高。

其高并发的机制主要有以下两点:分段锁、读不需要锁

9. 拦截器与过滤器的区别

归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术

拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVC的访问进行增强

10. synchronized和lock(显示锁)的区别

区别如下:

  • 来源:
    lock是一个接口,而synchronized是java的一个关键字,对应JVM层面 ,synchronized是内置的语言实现;
  • 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
  • 是否响应中断:
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
  • 是否知道获取锁:
    Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
  • Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
  • 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
  • synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。

11. Java中如何避免死锁

​ 首先根据操作系统中发生死锁时的四个条件:

1. 抢占互斥资源
2. 资源不可剥夺
3. 请求互斥资源并保持现有资源
4. 循环等待链

根据这四个条件破坏死锁

1. TreadLocal变量,把互斥变量变为非互斥变量,线程之间互不影响。
2. 可以强制抢占那些拥有资源却在阻塞队列的线程。
3. 可以为线程的锁设置时间,锁住一定时间后自动释放锁。
4. 先满足执行A线程,再满足执行B线程,适当改变业务处理的顺序。
5. 银行家算法

12. hashcode和equals两个方法的作用:

​ 两个方法都定义在Object类中,意味着所有类都有这两个方法

  1. hashcode的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
  2. equals它的作用也是判断两个对象是否相等,如果对象重写了equals()方法,比较两个对象的内容是否相等;如果没有重写,比较两个对象的地址是否相同,价于“==”。

13. final关键字的作用

  1. 修饰在类上,该类不能被继承。

  2. 修饰在方法上,该方法不能被子类重写。

  3. 修饰在变量上,叫常量,该常量必须初始化,初始化之后值就不能被修改,而常量一般全都是用大写来命名。

14. 接口和抽象类的区别

  1. 抽象类的子类要用 extends 来继承;而实现接口要用 implements 。

  2. 抽象类可以定义构造函数,而接口不能。

  3. 抽象类里可以定义 main 方法,但接口不能有 main 方法。

  4. 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。

  5. 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。

  6. 抽象类是对逻辑的归纳,比如动物类可以是抽象类,人类可以extends动物这个抽象类。
    而接口是对功能的归纳,比如可以定义一个“提供数据库访问功能”的 接口,在其中封装若干操作数据库的方法。

15. mysql慢查询

就是查询的很慢的查询,如果开启了慢查询日志,查询慢的查询就会加入满查询日志。

16. 你知道Spring容器启动阶段会干什么吗?

​ IOC容器的工作阶段可以分为两个阶段:容器启动阶段Bean实例化阶段

容器启动阶段Bean实例化阶段
加载配置实例化对象
分析配置信息装配依赖
装配到BeanDefinition生命周期回调
其他后续处理对象其他处理
注册回调接口

17.能说一下Spring Bean生命周期吗?

​ 在Spring中,基本容器BeanFactory和扩展容器ApplicationContext的实例化时机不太一样,BeanFactory采用的是延迟初始化的方式,也就是只有在第一次getBean()的时候,才会实例化Bean;ApplicationContext启动之后会实例化所有的Bean定义。

​ Spring IOC 中Bean的生命周期大致分为四个阶段:实例化(Instantiation)、属性赋值(Populate)、初始化(Initialization)、销毁(Destruction)。

  1. **实例化:**第 1 步,实例化一个 Bean 对象

  2. **属性赋值:**第 2 步,为 Bean 设置相关属性和依赖

  3. **初始化:**初始化的阶段的步骤比较多,5、6步是真正的初始化,第 3、4 步为在初始化前执行,第 7 步在初始化后执行,初始化完成之后,Bean就可以被使用了

  4. **销毁:**第 8~10步,第8步其实也可以算到销毁阶段,但不是真正意义上的销毁,而是先在使用前注册了销毁的相关调用接口,为了后面第9、10步真正销毁 Bean 时再执行相应的方法

18. Spring 中的 Bean 的作用域有哪些?

  • singleton : 在Spring容器仅存在一个Bean实例,Bean以单实例的方式存在,是Bean默认的作用域。
  • prototype : 每次从容器重调用Bean时,都会返回一个新的实例。

以下三个作用域于只在Web应用中适用:

  • request : 每一次HTTP请求都会产生一个新的Bean,该Bean仅在当前HTTP Request内有效。
  • session : 同一个HTTP Session共享一个Bean,不同的HTTP Session使用不同的Bean。
  • globalSession:同一个全局Session共享一个Bean,只用于基于Protlet的Web应用,Spring5中已经不存在了。

19. Spring的单例Bean线程安全问题怎么解决?

常见的有这么些解决办法:

  1. 将Bean定义为多例
    这样每一个线程请求过来都会创建一个新的Bean,但是这样容器就不好管理Bean,不能这么办。
  2. 在Bean对象中尽量避免定义可变的成员变量
    削足适履了属于是,也不能这么干。
  3. 将Bean中的成员变量保存在ThreadLocal中⭐
    我们知道ThredLoca能保证多线程下变量的隔离,可以在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal里,这是推荐的一种方式

20. synchronized底层原理 是可重入锁吗

​ JVM基于进入和退出Monitor对象来显示方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法的结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit与之配对。

Synchronize是可重入锁,一个获得锁的线程可以再次进入一个同步块方法

22. Java线程池

使用线程池的优势
  1. 提高效率,创建好一定数量的线程放在池中,等需要使用的时候就从池中拿一个,这要比需要的时候创建一个线程对象要快的多。

  2. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

  3. 提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

  4. 最大线程数 = 核心线程数 + 救济线程数

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6IyFfSor-1693302991743)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%8F%82%E6%95%B0.png)]

四种创建线程池的方式

Executors类(并发包)提供了4种创建线程池方法,这些方法最终都是通过配置ThreadPoolExecutor的不同参数,来达到不同的线程管理效果。

  1. newCacheTreadPool

​ 创建一个可缓存的线程池,如果线程池长度超过处理需要,可以灵活回收空闲线程,没回收的话就新建线程

  1. newFixedThread

​ 创建一个定长的线程池,可控制最大并发数,超出的线程进行队列等待。

  1. newScheduleThreadPool

​ 可以创建定长的、支持定时任务,周期任务执行。

  1. newSingleExecutor

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

推荐通过new ThreadPoolExecutor开发中多数用这个类创建线程。该类有四个构造方法,分别传入不同的参数,详情见API文档

ThreadPoolExecutor核心参数:

new ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

参数含义解释
corePoolSize线程池中的核心线程数核心线程生命周期无限,即使空闲也不会死亡。
maximumPoolSize线程池中最大线程数任务队列满了以后当有新任务进来则会增加一个线程来处理新任务,****(线程总数 < maximumPoolSize)
keepAliveTime闲置超时时间当线程数大于核心线程数时,经过keepAliveTime时间将会回收救急线程
unit超时时间的单位(时/分/秒等)例如:TimeUnit.MICROSECONDS(与救急线程有关)
workQueue线程池中的任务队列存放任务(Runnable)的容器
threadFactory为线程池提供创建新线程的线程工厂*
rejectedExecutionHandler拒绝策略新增一个任务到线程池,如果线程池任务队列超过最大值之后,并且已经开启到最大线程数时,默认为抛出ERROR异常
多线程四种拒绝策略
  1. AbortPolicy abortPolicy = new ThreadPoolExecutor.AbortPolicy();

    • 当任务添加到线程池中被拒绝时,直接丢弃任务,并抛出RejectedExecutionException异常
  2. DiscardPolicy discardPolicy = new ThreadPoolExecutor.DiscardPolicy();

    • 当任务添加到线程池中被拒绝时,丢弃被拒绝的任务,不抛异常。
  3. DiscardOldestPolicy discardOldestPolicy = new ThreadPoolExecutor.DiscardOldestPolicy();

    • 当任务添加到线程池中被拒绝时,丢弃任务队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
  4. CallerRunsPolicy callerRunsPolicy = new ThreadPoolExecutor.CallerRunsPolicy();

    • 被拒绝任务的处理程序,直接在execute方法的调用线程中运行被拒绝的任务。

      总结:就是被拒绝的任务,直接在主线程中运行,不再进入线程池。

线程池执行流程

废话不多说,直接上图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MTsbGMmS-1693302991743)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B.png)]

关于线程池看这个博客线程池执行原理及源码解析(execute、addWorker、runWorker、getTask、processWorkerExit 执行流程详解) - 简书 (jianshu.com)

​ 在线程池创建的时候并不会创建线程,除非 使用prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。

​ 当核心线程执行的任务完成后,就会通过getTask()方法从阻塞队列中获取新的任务执行,这就是线程互用的原理。当阻塞队列中没有线程时有两种方案,一种是不回收worker对象,一种是超时回收worker对象。执行完任务的worker对象会在阻塞队列中阻塞,直到有新任务进入阻塞队列。

23. Spring解决循环依赖问题

bean创建的基本过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4imlOJtj-1693302991743)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/Bean%E7%9A%84%E5%88%9B%E5%BB%BA.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-neCY7SEH-1693302991743)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E4%B8%89%E7%BA%A7%E7%BC%93%E5%AD%98.png)]

​ AOP的执行是在初始化阶段执行,生成代理对象。对于一般的循环依赖,用两级缓存就够了,三级是为了aop动态代理对象的循环依赖而设计的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PtXOrT08-1693302991744)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E4%B8%89%E7%BA%A7%E7%BC%93%E5%AD%98aop.png)]

出现循环依赖的情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8khtFVwk-1693302991744)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E4%B8%89%E7%BA%A7%E7%BC%93%E5%AD%981.png)]

解决不含代理对象的循环依赖

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rc3KbPZU-1693302991744)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E4%B8%89%E7%BA%A7%E7%BC%93%E5%AD%982.png)]

解决包含代理对象的循环依赖

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RzzkJ9sq-1693302991744)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E4%B8%89%E7%BA%A7%E7%BC%93%E5%AD%984.png)]

24. 包装类的缓冲区

​ 包装类的缓冲区由java自动装箱实现,当我们在将两个Integer对象进行比较时,值的范围在-128—+127时,两个对象进行比较时,他们是相等的。这是因为在java的缓冲区中,为在-128—+127之间的值的包装类都创建了对象,自动装箱时,如果值在这个范围内就会将对象自动指向缓存中的包装类对象。

​ 简单的说,如果整型字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,超出这个范围的数值才会真正的new一个对象出来。所以上面的当我们把比较的两个数换成64以后,虽然表面上看是两个不同的对象,但其实由于数值在包装类的高速缓冲区中,他们的地址是相同的,所以才会有这两段程序的输出结果是不一样的结果。基本数据类型包装类中的Byte、Short、Integer、Long的高频缓存范围为-128到127;Character的高频缓存为-128到127;Float、Double没有高频缓存区。Integer是唯一一个可以修改高频缓存范围的包装类。通过在VM optons中如下设置:-XX:AutoBoxCacheMax=8866 即修改缓存最大值为8866。

25. Java代码编译和执行的过程

  1. 编译:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flpQq2Ue-1693302991745)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/java%E7%BC%96%E8%AF%91.png)]

  1. 解释
  2. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HGa9ogb0-1693302991745)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/class%E5%8A%A0%E8%BD%BD.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BRhHgDOB-1693302991745)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/java%E8%A7%A3%E9%87%8A.png)]

问题:什么是解释器(Interpreter),什么是JIT编译器?
解释器:当Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
JIT (Just In Time Compiler) 编译器:就是虚拟机将源代码直接编译,成和本地机器平台相关的机器语言。

问题:为什么说Java是半编译半解释型语言?

​ JDK1.0时代,将Java语 言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
a也发展出可以直接生成本地代码的编译器。
现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。

27. 重载和重写的区别

一、概念:

​ 重载:

​ 在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数 类型不同即可。

​ 重写:

​ 在子类中根据需要对从父类中继承来的方法进行改造,也称 为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。

二、重载和重写的特点:
重载的特点:
与返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类型)。调用时,根据方法参数列表的不同来区别。
重写的特点:
1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表 。
2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型 (返回类型为类)。void类型和基本类型的返回值必须一样。
3. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限 。
4. 子类不能重写父类中声明为private权限的方法 。
5. 子类方法抛出的异常不能大于父类被重写方法的异常 (父类异常的子类)。

28. 线程之间的通信

使用线程好处:充分利用CPU资源,相较于进程,线程的切换消耗资源少

操作系统中:

操作系统中分为属于同一个进程的线程之间消息通信和属于不同进程的线程之间的通信。

  • 同一进程内的线程是共享进程的资源的所以通信很方便。

  • 不同进程内的线程就涉及到进程之间的通信:

    1. 共享存储

    2. 管道通信(半双工,全双工需要设置两个管道)

    3. 消息传递:直接(挂在对方的消息队列)和间接(信箱传递)

Java中:

  1. 共享变量的方式(设置一个信号量对象,内部采用线程安全的方法,让多个线程对其访问)

  2. wait()/notify机制

  3. Lock/Condition机制:当使用LOCK对象保持同步时,JAVA为我们提供了Condition类来协调线程的运行。

  4. 管道

    (1)创建管道输出流PipedOutputStream pos和管道输入流PipedInputStream pis

    (2)将pos和pis匹配,pos.connect(pis);

    (3)将pos赋给信息输入线程,pis赋给信息获取线程,就可以实现线程间的通讯了

29. springboot如何保证线程安全

1、两个概念

​ 有状态的bean:对象中有实例变量(成员变量),可以保存数据,是非线程安全的。

​ 无状态的bean:对象中没有实例变量(成员变量),不能保存数据,可以在多线程环境下共享,是线程安全的。

2、spring的线程安全问题

​ 一般不会出现线程安全问题。在spring中,绝大部分bean都是无状态的,因此即使这些bean默认是单例的,也不会出现线程安全问题的。比如controller、service、dao这些类,这些类里面通常不会含有成员变量,因此它们被设计成单例的。如果这些类中定义了实例变量,就线程不安全了,所以尽量避免定义实例变量。

​ 对于有状态的bean,spring采用ThreadLocal进行处理,把成员变量存到ThreadLocal中,这样就可以为每个访问单例的线程创建一个成员变量的副本,把各个线程对成员变量的操作隔离开,使它们成为线程安全可以共享的对象。也可以使用原型模式(prototype),每次使用时都会重新生成一个对象,解决了线程不安全的问题。无状态的Bean适合使用不变模式,即单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,适合使用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。

总结:无状态用单例;有状态,原型加本地。

30. Linux上运行java程序

javac hello.java --> 生成hello.class文件 --> java hello.class运行

31. HashMap的数组长度为什么是2的n次方

​ 在进行hash运算时,java中用hash & (length-1)来替换hash % length ,因为计算机中的&运算要比%运算快很多,但是两者相等的情况是length是2的n次方。所以,java中使用 hash & (length-1)来替换hash % length。

32. 使用Callable实现多线程的步骤

(1)第一步:创建Callable子类的实例化对象

(2)第二步:创建FutureTask对象,并将Callable对象传入FutureTask的构造方法中

​ (注意:FutureTask实现了Runnable接口和Future接口)

(3)第三步:实例化Thread对象,并在构造方法中传入FurureTask对象

(4)第四步:启动线程

33. 线程池中的线程抛出异常后如何处理

一个线程抛出异常并不会妨碍其他线程的执行,线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。当线程异常,会调用 ThreadPoolExecutor.runWorker() 方法最后面的 finally 中的 processWorkerExit(),会将此线程 remove 并重新 addworker() 一个线程。

  1. 执行线程的是execute()方法时:

    ThreadPoolExecutor.runWorker() 方法中 task.run()即执行我们的方法时,如果异常的话会 throw x; 所以可以看到异常。

  2. 执行线程的是submit()方法时:

    ​ 当执行方式是 submit 时,堆栈异常没有输出。但是调用 Future.get() 方法时,可以捕获到异常;

    ThreadPoolExecutor.runWorker() 方法中 task.run() 其实还会继续执行 FutureTask.run() 方法,再在此方法中 c.call() 调用我们的方法,如果报错会 setException(),并不会抛出异常。当我们去 get() 时,才会将异常抛出。

execute 源码执行流程

  1. 开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增 coreThread 去执行任务),执行到 task.run() 时会去执行提交的任务。 如果任务执行失败,或 throw x 抛出异常;
  2. 之后会到 finally 中的 afterExecute() 扩展方法,我们可以扩展该方法对异常做些什么;
  3. 之后因为线程执行异常会跳出 runWorker 的外层循环,进入到 processWorkerExit() 方法,此方法会将执行任务失败的线程删除,并新增一个线程;
  4. 之后会到 ThreadGroup.uncaughtException 方法,进行异常处理。如果没有通过 setUncaughtExceptionHandler() 方法设置默认的UncaughtExceptionHandler,就会在 uncaughtException() 方法中打印出异常信息。

submit 源码执行流程

  1. 将传进来的任务封装成 FutureTask,同样走 execute 的方法调用,然后直接返回 FutureTask;
  2. 开始执行任务,新增或者获取一个线程去执行任务(比如刚开始是新增 coreThread 去执行任务);
  3. 执行到 task.run() 时,因为是 FutureTask,所以会去调用 FutureTask.run()
  4. FutureTask.run() 中,c.call() 执行提交的任务。如果抛出异常,并不会 throw x,而是 setException() 保存异常;
  5. 当我们阻塞获取 submit() 方法结果时 get(),才会将异常信息抛出。当然因为 runWorker() 没有抛出异常,所以并不会删除线程。
  • execute执行无返回值,submit提交的线程有返回值Future<Integer> future = executor.submit(new Callable<Integer>(),通过Future对象来接收

34. Synchronazed底层实现原理

35. Redis实现乐观锁

使用watch命令和事务结合

127.0.0.1:6379> set money 10
OK
127.0.0.1:6379> watch money      #监视money对象,监视其是否发生改变
OK
127.0.0.1:6379> multi #开启事务
127.0.0.1:6379> incrby money 10  #让money自增10
127.0.0.1:6379> exec  #提交事务,若在提交事务之前有其他线程修改了money的值,那这时候就会导致事务提交失败!
#需要 unwatch后重新watch,达到自旋的效果

36. Redis主从复制

一主二从,写一个读两个

A->B A->C

只要一个redis时,默认为主机。只要认一个当老大即可,老大只有一个。

SLAVEOF 127.0.0.1 6379 # 找127.0.0.1 6379 当自己的老大

主机只能写,从机只能读。从机有主机全部数据。

测试:主机断开连接,从机依旧连接了主机,但是没有写操作,主机回来后,依然可以拿到主机的全部数据。该情况仅限配置文件位置的主机。如果通过命令行配置的主机,在主机宕机后,从机会变成单一的主机。

复制原理:

slave 启动成功连接到master后会发送一个sync同步命令

两种方式:

  • 全量复制:slave接收到master的所有数据文件后,将其存盘并加载到内存中。
  • 增量复制:master将收到的新的修改指令依次传到slave,保持主从机的数据一致。

​ master接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master将传送所有数据到slave,并完成一次完全同步。只要重新连接master就会进行一次完全同步。

37. 哨兵机制

自动选老大。
谋朝篡位自动版,能够后台监控主机是否故障,如果故障了根据选票数自动将从库转为主库

哨兵是一种特殊的模式,首先redis提供了哨兵的命令,哨兵是一个特殊的进程,其原理是哨兵通过发送命令,等待 redis服务器响应,从而监控运行的多个redis实例。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uyGL4hEW-1693302991745)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E5%93%A8%E5%85%B5%E6%9C%BA%E5%88%B6.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7tNGcRZ-1693302991746)(https://wangrongyi.oss-cn-chengdu.aliyuncs.com/typora/%E5%A4%9A%E5%93%A8%E5%85%B5.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZPF6XzaT-1693302991746)(.\asset\多哨兵2.png)]

如果主机回来了,只能归并到新的主机下,当做从机。

Sentinel的三个作用:

  • 监控
  • 故障转移
  • 通知

Sentinel如何判断一个redis实例是否健康:

  • 每隔一秒发送一次ping命令,如果超过一定时间没有响应则认为是主观下线
  • 如果大多数Sentinel都认为实例主观下线,则判定服务下线

故障转移步骤有哪些:

  • 首先选定一个slave作为新的master,执行slaveof no one
  • 然后让所有节点都配置slaveof新的master
  • 修改故障节点 配置,添加slaveof新的master

38. JVM中堆和栈的区别

  • 栈是线程私有的;堆是线程公有的。
  • 栈主要用于存储局部变量对象的引用变量;堆主要用于存储实例化的对象,数组,由JVM动态分配内存空间。
  • 栈中的变量超过其作用域后,JVM会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用;在堆中分配的内存,由JVM自动垃圾回收器来管理。
  • 如果栈内存没有可用的空间存储方法调用和局部变量,JVM会抛出java.lang.StackOverFlowError;而如果是堆内存没有可用的空间存储生成的对象,JVM会抛出java.lang.OutOfMemoryError。
  • 栈的内存要远远小于堆内存。

39. redis持久化机制

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

两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备AOF更适合做热备.
tip:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

  • 冷备份发生在数据库已经正常关闭的情况下,当正常关闭时会提供给我们一个完整的数据库;
  • 热备份是在数据库运行的情况下,进行备份数据库的方法
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也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的,大家可以思考下为啥哟。

RDB与AOF混合持久化:

实际操作:AOF和RDB一起用,第一时间用RDB恢复数据,然后AOF做补全处理。

$redis-cli
127.0.0.1:6379> config set aof-use-rdb-preamble yes
OK
#打开混合开关执行BGREWRITEAOF生成RDB-AOF混合文件
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
127.0.0.1:6379> set name wry
OK
127.0.0.1:6379> quit

​ 开启混合持久化之后,AOF文件的前半段是RDB格式的全量数据后半段是redis命令格式的增量数据,前半段采用读RDB的函数读取,直到RDB结束标记为止,后半段采用读取AOF的函数读取。

  • 优点:

​ 混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,减少数据丢失。

  • 缺点:

​ 兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。

40. springboot自动装配原理

启动类的@SpringBootApplication注解由@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解组成,三个注解共同完成自动装配;

  • @SpringBootConfiguration 注解标记该启动类为配置类

  • @ComponentScan 注解实现启动时扫描启动类所在的包以及子包下所有标记为bean的类由IOC容器注册为bean

  • @EnableAutoConfiguration通过 @Import 注解导入 AutoConfigurationImportSelector类,然后通过AutoConfigurationImportSelector 类的 selectImports 方法去读取需要被自动装配的组件依赖下的spring.factories文件配置的组件的类全名,并按照一定的规则过滤掉不符合要求的组件的类全名,将剩余读取到的各个组件的类全名集合返回给IOC容器并将这些组件注册为bean

41. 自动注入@Autowire和@Resource的区别

@Autowired注解由Spring提供,只按照byType注入;@resource注解由J2EE提供,默认按照byName自动注入。

@Autowired默认按类型进行装配,@Resource默认按照名称进行装配。

  • @Autowired与@Resource都可以用来装配bean,都可以写在字段setter方法上

  • @Autowired默认按类型装配,默认情况下必须要求依赖对象存在,如果要允许null值,可以设置它的required属性false。如果想使用名称装配可以结合@Qualifier注解进行使用。

  • @Resource**,默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行名称查找。如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。

43. Mybatis批量插入的三种方法

  1. 循环单次插入

    • 不停调用dao层插入方法,效率最低,不推荐使用
  2. MyBatis拼接原生SQL,一次性插入()

    <insert id="saveBatchByNative">
            INSERT INTO `USER`(`NAME`,`PASSWORD`) VALUES
            <foreach collection="list" separator="," item="item">
                (#{item.name},#{item.password})
            </foreach>
    </insert>
    
    • 优点:一次性执行,效率最高
    • 缺点:MyBatis会将批量插入拼接为一个SQL执行,但是MySQL默认能执行的长SQL为4MB,所以要么调节执行SQL的大小set global max_allowed_packet = 10*1024*1024,要么就控制批量插入数据大小。该方法不利于拓展。
  3. 使用MybatisPlus的批量插入功能

    @Mapper
    public interface UserMapper extends BaseMapper<User>{
        
        boolean saveBatchCustom(List<User> list);
    }
    
    • MP 是将要执行的数据分成 N 份,每份 1000 条,每满 1000 条就会执行一次批量插入,所以它的性能要比循环单次插入的性能高很多。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值