首页 面试题总结
文章
取消

面试题总结

1.Sychronized的偏向锁、轻量级锁、重量级锁

在jdk1.6版本之前,Sychronized只有重量级锁,而这种锁在使用时,会切换用户状态和操作系统的内核状态,大大提高了系统的性能,所以在后来加入了偏向锁、轻量级锁,这是为了更好的提升系统的性能

2什么情况下Interger会自动拆箱

在Java中,当Integer类型的对象需要转换为基本数据类型(如int、double等)时,会发生拆箱操作。以下是几种情况下Integer会进行拆箱:

  1. 调用基本数据类型的方法或操作符:例如,将Integer对象与其他基本数据类型相加、相减、相乘等操作时,Integer对象会自动拆箱成对应的基本数据类型。
  2. 赋值给基本数据类型变量:当将一个Integer对象赋值给int类型的变量时,会触发拆箱操作。
  3. 方法参数需要基本数据类型:如果一个方法的参数是基本数据类型,而你传递给它一个Integer对象作为实参,编译器会自动将Integer对象拆箱为对应的基本数据类型。
  4. 比较操作:当使用关系运算符(如==、!=、<、>等)对Integer对象进行比较时,会进行拆箱操作。

需要注意的是,拆箱操作可能会引发NullPointerException异常,特别是当Integer对象为null时进行拆箱操作。因此,在拆箱之前,建议先进行null值的判断。

3.String a = ”a“和String a= new String(“a”)创建字符串的区别

  • 直接定义的String a = ”A”,是存储在字符串常量池中的,new String(“A”)是存储在堆内存中的
  • 直接定义的String A = “A”,在编译阶段创建,newString(“A”)是在运行时才会创建的
  • 直接定义的String A=”A”,在变量池中只有一个,但是newString(“A”)在堆内存中,只要时创建一个,就会生成一个对象

4.jdk、jre、jvm的区别

  • jdk是程序员所使用的开发工具包,它包括了编译运行程序的各种工具和资源,包括Java编译器、Java运行时环境,以及常用的Java类库等
  • jre是java运行环境,用来运行java程序的字节码文件,jre中包括了jvm以及jvm所需要的各种工具和资源,与jdk不同的是,他是给普通用户使用的,普通用户只需要安装jre就可以运行java文件
  • jvm是java虚拟机,是jre的一部分,他是整个java实现跨平台的最核心的部分,负责运行字节码文件
  • 我们写Java代码,⽤txt就可以写,但是写出来的Java代码,想要运⾏,需要先编译成字节码,那就需要编译器,⽽JDK中就包含了编译器javac,编译之后的字节码,想要运⾏,就需要⼀个可以执⾏字节码的程序,这个程序就是JVM(Java虚拟机),专⻔⽤来执⾏Java字节码的。
  • JVM在执⾏Java字节码时,需要把字节码解释为机器指令,⽽不同操作系统的机器指令是有可能不⼀样的,所以就导致不同操作系统上的JVM是不⼀样的,所以我们在安装JDK时需要选择操作系统。另外,JVM是⽤来执⾏Java字节码的,所以凡是某个代码编译之后是Java字节码,那就都能在JVM上运⾏

5.面向对象的四大特性

抽象:

继承:

封装:

多态:

6.接口和抽象类的区别

7.HashMap的扩容机制

1.负载因子

从代码中我们可以看到,在向HashMap中添加元素过程中,如果 元素个数(size)超过临界值(threshold) 的时候,就会进行自动扩容(resize),并且,在扩容之后,还需要对HashMap中原有元素进行rehash,即将原来通中的元素重新分配到新的桶中。

在HashMap中,临界值(threshold) = 负载因子(loadFactor) * 容量(capacity)。

loadFactor是装载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容。(相见HashMap中傻傻分不清楚的那些概念)

2.为什么要扩容呢

扩容需要对其容量进行扩充,并且还要进行rehash,这个过程其实很耗时的,但是我们为什么还要扩容呢?这是因为根据hash函数计算出来的地址,有可能出现hash碰撞,HashMap是由数组+链表构成的,这个时候可以将数据放到链表上,我们可以把树一直挂到链表上,但是这时候有个问题,链表如果太长的话,那么数据的查询就像在链表上一样了,链表的查询速度非常低,所以为了保证HashMap的读取速度,我们要想办法尽量保证HashMap的冲突不要太高。

3.如何保证HashMap的冲突不会太高呢?

冲突太高无非两种情况:

  1. 数组长度太短
  2. hash函数设计的不够合理,导致将数据分到同一个或者几个桶中,分配不均

所以解决Hsah碰撞也是从这两方面入手。

为了避免哈希碰撞,HashMap需要在合适的时候进行扩容,需要设置一个合适的负载因子

4.负载因子设置多少合适呢?

在JDK官方文档中有一段描述,一般来说,默认的负载因子(0.75)在时间和空间成本上提供了较好的权衡,所以最好的负载因子的值是0.75

8.Java锁机制

  • 偏向锁:指的是当一个线程访问一个对象的时候,这个对象的对象头中mark word,有一个名为线程id的属性,这时线程id会记录下此线程的id,以后这个线程再进行访问的话,可以直接访问这个对象
  • 轻量级锁:当有其他线程访问这个对象的时候,偏向锁会自动升级成轻量级锁,此时不会再用对象头中的线程id属性记录线程了,而是把mark word和lock record绑定起来,其他线程会进入自旋状态,这个自旋时间也不是固定时间,而是根据上一次在同一个锁上自选的时间和锁状态这两个条件决定的,这就是适应性自旋
  • 重量级锁: 如果此时有其他的线程也想访问这个对象,那么轻量级锁会升级为重量级锁,这时候需要通过Monitor对线程进行控制

直接使用monitor不行吗?可以的,但如果有多个线程使用同一个资源,但是他们没有竞争,线程1,1点使用,线程2,2点使用,没有竞争。如果还用monitor是不是有点杀鸡用牛刀了。为了提升性能,提出了轻量级锁。不再用对象关联monitor了,而是对象的mark word字段和线程栈的 lock record进行交换作为锁。如果有锁重入,就会创建多个lock record放入线程栈,那么问题有来了,如果重入多了,多次创建lock record不是也消耗性能吗?所以提出了偏向锁。对线头设置线程id,解决重入问题。 在理清下这些锁在什么场景下使用,1,重量级锁:多个线程有竞争;2.轻量级锁:多个线程但是没有竞争,这点尤为重要,轻量级锁是解决不了锁竞争的。轻量级锁解决的问题是在多线程没有竞争下,仍旧关联monitor的问题。3. 偏向锁:只有一个线程,没有其他线程。

共享锁和排他锁

  • 共享锁(S锁):

    也称为读锁。

    如果事务T对数据对象A加上S锁,则可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁。这保证了其他事务可以读取A,但在事务T释放对象A上的S锁之前不能对A做任何修改。

  • 排他锁(X锁):

    也成为写锁。

    事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁,这保证了其他事务在T释放A上的锁之前不能在读取和修改A。

二、队列的分类

Java 中的这些队列可以从不同的维度进行分类,例如可以从阻塞和非阻塞进行分类,也可以从有界和无界进行分类,而本文将从队列的功能上进行分类,例如:优先队列、普通队列、双端队列、延迟队列等

file

1、阻塞队列与非阻塞队列
1.1 阻塞队列

阻塞队列(Blocking Queue)提供了可阻塞的 put 和 take 方法,它们与可定时的 offer 和 poll 是等价的。如果队列满了 put 方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么 take 方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put 方法和 take 方法就永远不会阻塞。在java包”java.util.concurrent”中,提供六个实现了”BlockingQueue”接口的阻塞队列:

  1. ArrayBlockingQueue 用数组实现的有界阻塞队列;
  2. LinkedBlockingQueue 基于链表实现的有界阻塞队列
  3. PriorityBlockingQueue是一个带优先级的队列,基于堆数据结构的;
  4. DelayQueue是在PriorityQueue基础上实现的,底层也是数组构造方法,是一个存放Delayed 元素的无界阻塞队列;
  5. SynchronousQueue 一个没有容量的队列 ,不会存储数据;
  6. LinkedBlockingDeque 是双向链表实现的双向并发阻塞队列;
1.2非阻塞队列

所有无Blocking Queue的都是非阻塞,并且它不会包含 put 和 take 方法。

2、有界队列和无界队列
2.1 有界队列

是指有固定大小的队列,比如设定了固定大小的 ArrayBlockingQueue,又或者大小为 0 的 SynchronousQueue。

2.2 无界队列

指的是没有设置固定大小的队列,但其实如果没有设置固定大小也是有默认值的,只不过默认值是 Integer.MAX_VALUE。

3、双端队列

Deque是一个双端队列接口,继承自Queue接口,Deque的实现类是LinkedList、ArrayDeque、LinkedBlockingDeque,其中LinkedList是最常用的。

Java双端队列-CSDN博客

4、优先队列

优先队列(PriorityQueue)是一种特殊的队列,它并不是先进先出的,而是优先级高的元素先出队。 优先队列是根据二叉堆实现的。最大堆和最小堆。

5、延迟队列

延迟队列(DelayQueue)是基于优先队列 PriorityQueue 实现的,它可以看作是一种以时间为度量单位的优先的队列,当入队的元素到达指定的延迟时间之后方可出队。

三、队列的使用场景

最典型的就是线程池,不同的线程池都是基于不同的队列来实现多任务等待的。

1.LinkedBlockingQueue使用场景:

在项目的一些核心业务且生产和消费速度相似的场景中:订单完成的邮件/短信提醒。 订单系统中当用户下单成功后,将信息放入ArrayBlockingQueue中,由消息推送系统取出数据进行消息推送提示用户下单成功。如果订单的成交量非常大,那么使用ArrayBlockingQueue就会有一些问题,固定数组很容易被使用完,此时调用的线程会进入阻塞,那么可能无法及时将消息推送出去,所以使用LinkedBlockingQueue比较合适,但是要注意消费速度不能太低,不然很容易内存被使用完。

2.PriorityBlockingQueue使用场景:

在项目上存在优先级的业务:VIP排队购票 用户购票的时候,根据用户不同的等级,优先放到队伍的前面,当存在票源的时候,根据优先级分配

3.DelayQueue使用场景 :

由于是基于优先级队列实现,但是它比较的是时间,我们可以根据需要去倒叙或者正序排列(一般都是倒叙,用于倒计时)。所以适用于:

订单超时取消功能、网站刷题倒计时 用户下订单未支付时,超时则释放订单中的资源,如果取消或者完成支付,我们再将队列中的数据移除掉。

4.SynchronousQueue使用场景:

参考线程池newCachedThreadPool()。 如果我们不确定每一个来自生产者请求数量但是需要很快的处理掉,那么配合SynchronousQueue为每个生产者请求分配一个消费线程是最简洁的办法。

cking Queue)提供了可阻塞的 put 和 take 方法,它们与可定时的 offer 和 poll 是等价的。如果队列满了 put 方法会被阻塞等到有空间可用再将元素插入;如果队列是空的,那么 take 方法也会阻塞,直到有元素可用。当队列永远不会被充满时,put 方法和 take 方法就永远不会阻塞。在java包”java.util.concurrent”中,提供六个实现了

10.spring中依赖注入的方式有哪些

三种依赖于注解的注入方法

1.变量注入

image-20240313132601744

  • 优点:注入方式简单,非常简洁
  • 缺点:注入的对象不能用final修饰,可能会导致循环依赖问题,并且启动的时候不会报错,只有在使用那个bean的时候才会报错

2.构造器注入

image-20240313132635399

  • 优点:1.显示注入必须强制注入,通过强制指明依赖注入来保证这个类的运行,防止发生NullPointerException 2.注入的对象可以用final修饰 3.可以避免循环依赖问题,如果存在循环依赖的话,spring项目启动的时候就会报错

  • 缺点:当有十几个甚至更多对象需要注入时,构造函数的代码臃肿,看起来不太舒服

3.setter方式注入

image-20240313133010317

  • 优点:1.依赖注入中使用的依赖对象是可选的,意思是注入的依赖对象是可以为NULL的2.允许在类构造器完成过后重新注入
  • 缺点:注入的对象不能用fianl修饰

11ConcurrentHashMap

JDK1.7

image-20240323215634356

JDK1.8

image-20240323215651944

本文由作者按照 CC BY 4.0 进行授权
热门标签