Java面试题总结(持续更新)
2021/12/25 14:11:03
本文主要是介绍Java面试题总结(持续更新),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
JavaSE
Java基础
-
解释型语言和编译型语言的区别?Java是解释型语言还是编译型语言?
编译型语言:把做好的源程序全部编译成二进制代码的可运行程序。然后,可直接运行这个程序。
解释型语言:把做好的源程序翻译一句,然后执行一句,直至结束!
编译型语言,执行速度快、效率高;依靠编译器、跨平台性差些。
解释型语言,执行速度慢、效率低;依靠解释器、跨平台性好。
个人认为,java是解释型的语言,因为虽然java也需要编译,编译成.class文件,但是并不是机器可以识别的语言,而是字节码,最终还是需要 jvm的解释,才能在各个平台执行,这同时也是java跨平台的原因。所以可是说java即是编译型的,也是解释型,但是假如非要归类的话,从概念上的定义,恐怕java应该归到解释型的语言中。
-
JIT是什么
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lKmKwpFE-1640407809272)(D:\Ego\Java\面试\20191015222132356.png)]
java编译为字节码文件后,如果jvm发现某段代码为“热点代码”,那么就会用JIT直接编译并进行优化。不是热点代码的话jvm就用解释器去解释。
热点代码有两类:1.被多次调用的方法 2.被多次循环执行的方法体
jvm识别热点代码需要进行热点探测,探测算法有两种:
-
基于采样
jvm会周期性对各个线程栈顶进行检查,如果某些方法经常出现在栈顶,它就是热点方法。实现简单高效,但容易收到线程阻塞或其他外因干扰
-
基于计数器
为每个方法或代码块建立计数器,超过一定阈值就认为是热点方法。结果严谨,但实现麻烦
HotSpot采用第二种,并且有两类计数器:
-
方法调用计数器
调用一次次数加一,超过一定时间(半衰周期)没有调用的话,次数会减半,称为“热度衰减”。
-
回边计数器
统计循环体执行的次数。字节码中遇到控制流向后跳转的指令称为回边,遇到回边就加一
-
-
正则表达式和Java?
正则表达式就是一种字符串的匹配模式,可以将一些复杂的规则用一个表达式来描述。
Java的String类提供了支持正则表达式操作的方法:matches() replaceAll() replaceFirst() split()
此外也可以用Pattern类表示正则表达式对象
-
Java如何跳出多重嵌套循环?
在最外层循环前加一个标记,比如a: 然后break a即可跳出,但是不建议这样用,会让代码可读性变差
java也有关键字goto,但是没有用
-
int和Integer的区别?
Java是面向对象语言,所以为了能将基本数据类型当对象操作,Java为他们各自提供了包装类型,Integer就是int的包装类型,从Java5开始引入了自动装箱/拆箱,二者可以相互转换
-
如何输出一个某种编码的字符串
String a = "abcde"; //将a以UTF-8的编码方式获得字节数组,再以GBK的编码方式编码为b String b = new String(a.getBytes("UTF-8"), "GBK"); System.out.println(b);
-
请你讲讲数组(Array)和列表(ArrayList)的区别?什么时候应该使用Array而不是ArrayList?
Array声明的时候就要确定长度,而且长度不可变,只能存储同一数据类型,可以存储基本数据类型
ArrayList是一个集合,长度可变,可以存放不同数据类型,不可以存放基本数据类型(可以存他们的包装类)。
当能确定数据类型和个数时可以用Array
-
什么是自动拆装箱?
Java八种基本数据类型都有各自对应的包装类,基本类型和对应的包装类在很多场景下可以自动转换,不需要手动去转换。
实现原理:
- 自动装箱:基本类型–>包装类 。调用 包装类.valueOf() , 比如Integer.valueOf(1)
- 自动拆箱:包装类–>基本类型。调用xxxValue,比如有一个Integer对象a,则为a.intValue()
哪些地方自动拆装箱:
- 向集合里存基本数据类型,会自动装箱
- 包装类型和基本类型比较大小时,包装类型会自动拆箱
- 包装类型四则运算,会自动拆箱
- 三目运算会自动拆箱,所以如果值为null,会报空指针异常
- 函数参数与返回值
-
为什么会出现4.0-3.6=0.40000001?
计算机计算十进制时要先转换成二进制,而二进制无法精确表示十进制小数,所以会出现误差
可以用BigDecimal来解决这个问题。
-
十进制的数在内存中是怎么存的?
补码的形式
-
Java8新特性?
-
对HashMap的数据结构进行优化
HashMap1.8以前是数组+链表,1.8以后是数组+链表/红黑树
-
Lambda表达式,本质是一段匿名内部类
-
函数时接口,给Lambda提供更好的支持
-
Stream API:创建Stream,中间操作,终止操作
-
Optional类:用来解决空指针异常
-
接口中可以定义m默认实现方法和静态方法
-
日期API
-
-
Object若不重写hashCode()的话,hashCode()如何计算出来的?
Object的hashCode是本地方法,用c/c++实现的,直接返回该对象的内存地址
-
请你解释为什么重写equals还要重写hashcode?
-
提升效率
向HashMap中put时,首先会计算对象的哈希码,然后看看对应的位置上有没有元素,如果没有的话就可以直接插入了,就不需要从头到尾和每个元素都进行equals判断了。
-
还有为了保证HashMap和HashSet中的去重性。因为equals相同的对象,hashcode必须相同,如果只重写equals的话,两个属性相同的对象按照规则hash值也一样,但是没有hashcode的话会调用Object默认的的hashcode,这样值就不相同了。比如两个相同的属性的对象,hashcode值不同,HashMap认为是两个不同的对象,都会存进去,但是我们想要的结果是只存一个,这就违反了HashMap的唯一性了。
-
关键字
-
请你谈谈关于Synchronized和lock
synchronized Lock Java关键字 一个接口 线程执行完或发生异常会释放锁,不会出现死锁 需要在finally中手动释放锁,不然容易造成死锁 不可以中断等待锁的线程 可以中断等待锁的线程 不可以判断锁的状态 可以判断锁的状态 大量线程竞争时性能低 大量线程竞争时性能高 -
请你介绍一下volatile?
volatile可以用来保证可见性和有序性。
先说下内存模型:计算机执行程序时,每条指令都是在CPU上执行,那就涉及到了数据的读写。CPU的运行速度很快,而向主存读写数据的速度很慢,所以就在两者之间加了高速缓存。先把主存的数据复制一份到高速缓存中,然后CPU就可以从高速缓存中进行读写,最后高速缓存再将值刷新到主存中。那么多线程就会出现问题,比如主存中有一个数为0,现在有两个线程想对它加一,那么结果应该为2。但是可能第一个线程计算为1之后还没来得及写入主存,第二个线程就进行运算,也就是读取的也是0,那么最后结果为1。所以就提供了两种办法:
- 在总线上加LOCK#锁,那么对于某个变量只能有一个线程访问,但是效率低下。
- 缓存一致性协议。当一个CPU写数据时,如果其他CPU中也有这个变量的副本,会让其他CPU重新读取。
这就是可见性问题。
有序性问题就是说,JVM在执行时会对程序执行顺序进行优化,比如int a = 1, int b = 2。在实际执行时可能会将他们的顺序交换,但不会造成影响,因为这两个变量没有依赖关系,这就是指令重排序。单线程下是不会造成问题,但是多线程就可能出现问题。比如第一个线程先定义了某个变量,然后定义了一个标志位为true,第二个线程中假设标志位为true的话就使用第一个变量,那么重排序后,第一个线程可能设置标志位为true提前了,第二个线程认为可以使用了,但其实变量还没定义,那么就会出错。Java内存模型具备一定的有序性,即happens-before原则
在Java中,Java模型为了获得更好的性能,允许处理器使用高速缓存,也允许编译器进行指令重排序,所以也会出现这两个问题。
那么volatile修饰的变量有两个作用:
- 某个线程修改了这个变量后,其他线程立即可见。
- 禁止指令重排序。
但是volatile不可以保证原子性,比如要对a = 0 自增,开启两个线程,每个线程循环100次a++,那么结果可能是小于200的。因为a++不是原子操作,它分为三个步骤,先读取a,再加一,再写会,那么第一个线程第一次加的时候,可能刚读取完,然后被阻塞了,第二个线程再读的时候还是原来的值,加完之后写了回去,这时第一个线程阻塞完毕继续再原来的基础上加1,然后写回,那么两次操作其实只加了1。
volatile底层原理是,在生成汇编代码时会多出一个lock前缀指令,这个指令相当于一个内存屏障,它提供了3个功能:
- 保证重排序时,不会把它后面的指令排序到它前面,也不会把前面的指令排到后面
- 强制对缓存的修改操作立即写入主存
- 如果是写操作,其他CPU对应的缓存行无效
面向对象
-
重载和重写的区别?
两个都是实现多态的方式。重载是编译时多态,重写是运行时多态。重载发生在同一个类中,要求方法名一样,参数列表不一样,对返回值没有要求;重写发生在子类与父类中,子类重写父类的方法要求方法名,参数列表一样,返回值一样或者为父类返回值的字类,访问修饰符不能小于父类,不能抛出新的异常或更宽泛的异常
-
面向对象的六原则一法则?
- 单一职责原则:一个类只做它该干的事情,也就是实现高内聚
- 开闭原则:一个软件实体应该对扩展开放,对修改闭合。这样增加新功能时只用派生一些新类,而不用修改原来的代码
- 依赖倒转原则:尽可能使用抽象类型而不是具体类型
- 里氏替换原则:任何时候都可以用子类替换掉父类。如果这样做出现了问题那继承一定是错误的
- 接口隔离原则:接口要小而专,不能大而全
- 合成聚合服用原则:优先使用合成和聚合关系复用代码
- 迪米特法则:一个对象尽可能对其他对象了解的少,也就是做到低耦合
-
在try块中可以抛出异常吗?
可以,比如IO流读取File的时候,外层try catch包含创建流的代码,内层try catch来操作流,这样如果流创建失败直接抛异常,就不用关闭流了。
-
抽象类和接口的区别?
-
语法上:
- 抽象类可以有构造方法,接口不能有
- 抽象类可以包含非抽象的普通方法,接口不可以
- 抽象类可以有成员变量,接口不可以
- 一个类可以实现多个接口,但只能继承一个抽象类
-
应用上:
接口是横向的,抽象类是纵向的,接口约定了一个共同的行为,而抽象类是把一些子类的共性抽取出来,可以帮他们完成一部分方法的实现。所以需要横向扩展就用接口,纵向扩展就用抽象类。
举个例子,比如某个项目的所有Servlet都需要进行权限判断,记录日志、异常等操作,就可以定义一个抽象类,定义一个抽象方法,里面写具体的业务逻辑,然后定义一个非抽象的方法,去完成权限判断,记录日志的操作,然后去调用这个抽象方法,那么子类去继承他的时候只需要重写业务逻辑的抽象方法就可以了,别的操作就自动帮他完成了。
-
-
请说明一下final, finally, finalize的区别?
-
final:
修饰属性表示这个属性不可变,是一个常量。
修饰方法表示这个方法不可以被重写。
修饰类表示这个类不可以被继承。
-
finally:
用于异常处理,无论是否抛出异常,finally中的代码一定会执行,所以一般用于资源的关闭。
-
finalize:
是Object类中的一个方法,当垃圾收集器回收时,被回收的对象会调用此方法,以供其他资源回收
-
-
请说明面向对象的特征有哪些方面?
-
封装
就是要做到高内聚低耦合,把一个对象的属性和行为都封装到一个类中,把成员变量定义为私有。
-
继承
就是把父类的属性和方法继承过来,然后添加一些自己需要的新的东西,做到了可重用性和扩展性
-
多态
就是父类引用指向子类对象。分为编译时多态和运行时多态,重载实现了编译时多态,重写实现了运行时多态
-
-
请说明Comparable和Comparator接口的作用以及它们的区别?
两个接口都是来定义排序规则的。
-
Comparable:
相当于内部比较器,比如对集合进行排序一般会用到Collections.sort()方法,但是集合中的这个类必须实现Comparable接口,并重写他的compareTo方法,才可以直接用Collections进行排序
-
Comparator:
相当于外部比较器,如果一个类我们没办法对他进行扩展,也就是无法继承Comparable,那就可以使用Comparator,在用Collections.sort()方法时,里面传入集合以及一个匿名内部类,这个内部类去实现Comparator接口,并重写compare()方法。比如String类型中,默认的比较规则是按照字典序排序,如果我们想忽略大小写进行排序的话,就可以使用Comparator
-
-
请你讲讲什么是泛型?
泛型就是参数化类型,就是将操作的数据类型指定为一个参数,可以在类、接口、方法中使用。
泛型提高了代码的重用率,编译时可以检查类型安全,消除了强制转换,减少了出错的机会。
-
请解释一下extends 和super 泛型限定符?
extends用来定义上界,super定义下界。
-
上界的list只能get,不能add。
因为extends表示它和它的子类,但是具体add哪一个子类是不确定的,所以干脆就不让它add
但是get的话,无论是哪一个子类,都可以向上转型成上界
-
下界的list只能add,不能get。
super表示它和它的父类,父类那么多不知道add哪个,所以只能add他和他的子类,虽然不知道add哪个,但是都可以向上转型成下界。
而get的话,那么多父类是不能向下转型的,除非用Object来接收。
所以如果想取数据就使用extends,想存数据就用super
-
-
请说明”static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?
static表明让成员变量或成员方法所属于一个类,而不是对象,被static修饰的成员变量和方法可以直接通过类名.的方式被访问,独立于对象。
static还可以当作静态代码块,在类第一次被加载的时候执行,只会被执行一次,一般用作初始化,提高性能。
Java不可以覆盖private或static修饰的方法。private只用本类能访问到,子类是访问不到的,更别说覆盖了。
而static修饰的静态方法,跟任何实例无关,在编译时就绑定了,但覆盖是在运行时动态绑定,所以概念上不适用。
-
请列举你所知道的Object类的方法并简要说明
- hashCode():本地方法,用来获取对象的哈希值,用来确定对象的存储位置
- equals():用于确定两个对象是否相同
- clone():用来创建并返回当前对象的一个拷贝
- toString():返回对象的字符串表示形式
- getClass():本地方法,返回运行时的Class对象,被final修饰,不能被重写
- notify():本地方法,final修饰不能被重写,唤醒一个在此对象监视器上等待的线程
- notifyAll():同notify(),只是唤醒的是所有线程
- 三个wait():本地方法,final修饰不能被重写,用来暂停线程的执行。没参数的会一直等待下去,一个参数的传入等待时间,两个参数的传入等待时间和额外时间
- finalize():垃圾收集器回收时对象调用此方法进行资源的回收
-
类和对象的区别?
类是一个抽象的概念,是对具有共同特征的实体的集合。
对象是类的实例,是真实的个体,和真实世界一一对应的。
-
String为什么不可变?为什么要这样设计?
因为String的本质是char数组,而char数组是被final修饰的,所以不可以变。
为什么要这样设计:
-
字符串常量池的需要
Java堆内存中有一块区域是字符串常量池。当创建一个String对象时,如果常量池已经有这个字符串了,那就不会再创建,而是直接将引用指向它。如果有好几个引用都指向了一个常量池中的对象,这个时候如果有一个引用想要改变它的话,那所有的引用指向的就都被改变了,显然不合理。
-
效率
String中有hash来保存它的哈希码,String设置为不可变的话,每次使用的时候就不需要重复计算哈希码,提高了效率
-
安全
Java很多类都使用了String,比如网络连接,反射等等,如果String是可变的话h会引起安全隐患
-
集合
-
List、Map、Set三个接口,存取元素时,各有什么特点?
List和Set都是单列集合,Map是双列集合。
List有先后顺序,Set和Map是无序的。
-
List:
存的时候调用add()方法。
取的时候用get()或者用Iterator接口遍历
-
Set:
不能有重复的元素。存的时候调用add()方法,返回值为boolean,如果为true说明集合中没有这个元素,成功存进去,如果为false说明已经存在了。
取的时候用Iterator接口遍历
-
Map:
以键值对形式存储,不能有重复的键。存的时候调用put()方法
取的时候可以调用get(),根据key来找相应的value;也可以通过keySet()获取所有key的集合;也可以通过values()获取所有value的集合;也可以通过entrySet()获取所有键值对的集合
Set和Map都有哈希存储的版本和排序树存储的版本,哈希存储版本存取非常高效,而排序树版本可以自定义排序规则
-
-
ArrayList、LinkedList、Vector的区别和实现原理
-
存储结构
ArrayList和Vector基于数组实现,LinkedList基于双向链表实现。
-
线程安全
ArrayList和LinkedList不是线程安全的,可以用Collections中的静态方法synchronizedList()把他们变成线程安全的。Vector是线程安全的,大部分方法都包含synchronized,所以效率比较低,已经被遗弃了。
-
扩容机制
ArrayList如果元素个数超过数组长度,会产生一个新的数组,容量是原来的1.5倍,然后将原来的数据复制过来,再加上新的数据。
-
效率
ArrayList和Vector查询效率是O(1),平均增删的效率是O(n)
LinkedList平均查询效率是O(n),增删效率是O(1)
-
-
请判断List、Set、Map是否继承自Collection接口?
List和Set继承自Collection,Map不是
-
请讲讲你所知道的常用集合类以及主要方法
List、Set、Map
List:具体实现有ArrayList和LinkedList,常用方法有get(),add(),remove(),contains(),size()
Set:具体实现有HashSet和TreeSet,常用方法有add(),remove(),contains(), size()
Map:具体实现有HashMap和TreeMap,常用方法有get(),put(),remove(),containsKey(),containsValue(),keySet(),values(),entrySet()
-
Collection和Collections的区别?
Collection是集合类的父接口,实现有List和Set
Collections是集合类的一个帮助类,提供了一系列静态方法来操作集合类,主要方法有:
- sort:排序方法,默认是升序,也可以实现Comparator接口来自定义排序规则
- reverse:翻转集合中元素的顺序
- shuffle:对集合随机排序
- copy:将第二个参数集合中的元素复制到第一个参数集合中
- synchronizedList/synchronizedSet/synchronizedMap:将集合变为线程安全
-
请说说快速失败(fail-fast)和安全失败(fail-safe)的区别?
快速失败:当用迭代器遍历一个集合时,如果这个集合被修改,那么就会抛出异常,所以不能在多线程下并发修改。原理:迭代时会用一个modeCount变量,如果集合遍历时被修改,那么modeCount的值就会改变,迭代器每次调用hasNext和next前都会判断modeCount和expectedmodeCount是否一样,如果不一样就会抛出异常。
安全失败:迭代器遍历集合时,集合被修改也不会抛出异常。原理:迭代前会对原有集合进行一次拷贝,迭代器对拷贝的集合进行迭代,所以就算原有集合被修改,拷贝的集合也没有被修改,也就不会抛出异常
-
请你说说Iterator和ListIterator的区别
两者都是迭代器。
Iterator:可以应用于List和Set,只能向后遍历,而且只能获取和删除。
ListIterator:
只能应用于List,是对Iterator的一个增强。
不仅可以向后遍历,也可以向前遍历,即多了hasPrevious()和previous()。
可以获取当前索引的位置,即nextIndex()和previousIndex()。
可以对当前对象进行修改,即set()
可以插入对象,即add()
-
请你说明一下ConcurrentHashMap的原理?
1.7:使用了分段锁Segment,Segment就类似于HashMap,也就是相当于一个二级哈希表。在put时,先根据key找到对应的Segment,然后在对应的Segment里面尝试获取锁,获取到锁之后就进行插入,和HashMap类似,最后再释放锁。在get时就很简单,因为它的value被volatile修饰了,所以保证了可见性,这样就不需要加锁了,效率就得到了提高。
1.8:数据结构和1.8的HashMap类似,不使用Segment,而是采用了CAS和synchronized来实现并发安全的。put时,先尝试用CAS写入,如果失败就通过自旋保证成功。如果hashcode==-1就需要进行扩容。如果都不满足的话,就用synchronized进行写入。最后判断是否要转成红黑树。
-
请说明ArrayList是否会越界?
会发生。因为ArrayList是线程不安全的,所以多线程情况下可能会发生越界。
-
HashMap的容量为什么是2的n次幂?
为了散列更均匀,减小哈希碰撞。因为在计算key对应的下标时,计算方法是(n-1)&hash值,这其实就相当于取模的位运算形式,如果容量是2的n次幂的话,减去1之后就是低位全是1的形式,这样和hash进行与运算时会大大减小hash冲突。
-
如果一直在list的尾部添加元素,用哪种方式的效率高?
在尾部添加元素的时间复杂度都是O(1),但是ArrayList需要扩容,而LinkedList需要new结点,所以在千万级别一下的时候,扩容的优势还不明显,LinkedList效率更高,但在千万级别以上ArrayList效率更高
-
请你解释一下hashMap具体如何实现的?
jdk1.7以前是通过数组+链表实现的,jdk1.8改为了数组+链表/红黑树。使用时,先初始化HashMap,可以指定容量大小n,它会自动调用tabSizefor()函数来得到第一个大于等于n且为2的整数次幂的数,如果不指定容量的话,就默认为16。然后插入时,会先根据key计算得到hash值,然后通过hash值得到索引。拿到索引后先判断那个位置上有没有结点,如果没有的话直接插入,如果有的话,判断和头节点的key是否相等,如果相等的话直接覆盖,如果不相等的话,如果头节点是红黑树结点,就按照红黑树结点的查找方式遍历,如果是链表结点的话,按链表结点方式遍历,如果在遍历中找到key相同的结点就覆盖,如果没有的话,就插入到尾部,链表的话插入完判断结点是否超过8个而且数组长度超过64,如果超过的话就转为红黑树。最后判断是否需要扩容。
-
HashMap扩容机制
首先判断老表的容量是否超过上限,如果超过上限的话,将扩容阈值修改为Integer最大值,如果没超过的话,将容量和阈值都扩大为2倍,并指向新数组,然后遍历老数组,如果当前索引只有一个结点,则重新计算索引位置然后放到新数组;如果当前索引不止一个结点,则计算e.hash&oldCap,如果为0,则直接放到原索引位置,如果为1,则放到原索引+oldCap的位置。因为计算索引时,用的是(n-1)&hash,那么扩容为2倍之后,n-1和原来相比,就是在高位多出来一个1,低位还是一样的,所以只用判断这一位即可
多线程
-
如何实现线程安全?
线程安全主要体现在原子性、可见性、有序性
实现原子性:使用原子类、synchronized、lock
实现可见性:volatile、synchronized、lock
实现有序性:volatile、happens-before原则
-
线程的基本状态以及状态之间的关系?
基本状态:新建、就绪、运行、阻塞、死亡
- 当线程被new出来后处于新建状态
- 线程调用start处于就绪状态
- 就绪的线程得到CPU的执行权后就进入运行状态
- 当运行态的线程因为某些原因放弃CPU,比如调用wait(),sleep(),就进入阻塞态。
- 休眠结束或被唤醒之后重新进入就绪态
- 执行完run方法或遇到了未捕获的异常就会进入死亡态
-
什么是守护线程?
线程分为用户线程和守护线程,守护线程就是在后台提供一种通用服务的线程,比如垃圾回收器。当所有用户线程结束后,守护线程就会结束,因为没有存在的意义了。可以用过thread.setDaemon(true)来设置一个线程为守护线程,必须在start之前设置。守护线程中产生的新线程也是守护线程。守护线程中不可以有读写操作和计算逻辑,因为守护线程随时都有可能停止。
-
线程池的状态和状态之间的关系?
5种状态: running、shutdown、stop、tidying、terminated
- 线程池被创建后处于running,可以接收任务和处理任务
- running时调用shutdown()方法进入shutdown,不接受新任务但可以处理已排队的任务
- 调用shutdownNow()方法进入stop,不接受新任务也不能处理已排队的任务,并且中断正在处理的任务
- shutdown下,任务队列为空且没有执行中的任务,会转为tidying;stop中执行任务为空也会转为tidying
- tidying执行完terminated()会转为terminated
-
如何停止线程池?
- shutdown()
- shutdownNow()
- shutdown() + awaitTermination()
-
Java锁?
并发编程中,为了避免共享数据产生数据不一致的问题,使用锁来锁定代码块、方法。
锁分为:悲观锁、乐观锁、公平锁、非公平锁、独占锁、共享锁、自旋锁、可重入锁、偏向锁/轻量级锁/重量级锁
-
什么是死锁?原因和必要条件?怎么解决?
死锁就是多个进程因为争夺资源而陷入的一种僵局,如果无外力作用就会一直保持下去。
原因:竞争资源和推进顺序非法
必要条件:
- 互斥条件:也就是竞争共享资源
- 请求和保持条件:进程因请求资源而阻塞时,不会释放已有的资源
- 不剥夺条件:进程已有的资源使用完之前不可以被剥夺,只能使用完自己释放
- 环路等待条件:死锁发生时,必然有一个进程资源环型链
怎么解决:
-
预防死锁:
破坏4个必要条件之一即可:
- 第一个条件不能破坏,因为加锁本来就是为了保证互斥
- 请求和保持:一次性分配资源
- 不可剥夺:如果获得一部分资源,别的资源获取不到时,释放自己已有的资源
- 环路等待:按顺序申请资源
-
避免死锁:银行家算法
-
检测死锁
-
解除死锁
-
什么是活锁和饥饿?
活锁跟死锁相反,死锁是因为争夺资源而陷入僵局,活锁是都可以获取到资源,但是都相互谦让陷入的一种僵局,活锁有可能自行解开。比如让线程休眠一段时间,让别的线程先获取到资源。
饥饿是一个线程一直获取不到资源,资源一直被别的线程获取,导致他可能永远等待。可能的原因是这个线程的优先级低,所以一直被高优先级的线程争夺到资源。
-
无锁技术
- 原子类
- ThreadLocal
- copy-on-write
- concurrent开头的并发工具类
-
什么是happens-before?
JVM会对代码进行优化,进行指令重排序,happens-before就保证了重排序后依然可以正确执行代码。他的原则是如果一个操作happens-before另一个操作,那么第一个操作的结果对第二天操作可见。
有6条规则:
- 先写的代码先执行
- 释放锁先于获取锁
- volatile修饰的变量,写操作先于读操作
- 线程的start()先于线程的其他操作
- 传递规则
- 被调用join()的线程的操作先于join()的返回
-
sleep()和wait()的区别?
sleep是Thread类的静态方法,wait是Object类的成员方法
sleep可以在任何地方使用,wait只能在同步代码块或同步方法中使用
sleep释放CPU资源,但不释放锁,休眠时间到后继续执行,wait释放锁,进入等待队列,被唤醒后才有机会获取锁
-
notify和notifyAll的区别?
一个线程中调用了一个对象的wait方法,该线程就会释放该对象的锁,进入该对象的等待池,等待池中的线程不会去竞争该对象的资源。一个对象还有一个锁池,只有在锁池中的线程才会去竞争该对象的锁。notify就是随机唤醒等待池中的一个线程进入锁池,notifyAll将等待池中所有的线程唤醒进入锁池
-
线程池中submit()和execute()方法有什么区别?
submit参数为runnable或callable,execute参数为runnable
submit有返回值,execute没有返回值
-
ThreadLocal有什么作用?有哪些使用场景?
ThreadLocal是本地线程存储,为每个线程创建一个该变量的副本,可以做到数据间的隔离。
JDBC连接connection就使用到了,为每个线程创建一个自己的连接,这样就保证了他们是在各自的连接上进行数据库操作,不会出现A线程关了连接,而B线程还在用的情况
-
如何保证多个线程同时启动?
使用countDownLatch。run方法中调用countDownLatch的await(),就会将线程阻塞在此处。然后调用start。
在需要同时启动的地方调用countDownLatch的countDown()
-
说说对于sychronized同步锁的理解
每个java对象都有一个内置锁,当线程运行到非静态的synchronized方法上时,就会自动获取该实例对象的锁,此时别的线程无法获得它的锁,当方法执行完毕后,会释放锁。
网络编程
-
BIO、NIO、AIO有什么区别?
BIO:同步阻塞,线程发起IO操作,不管内核是否准备好IO操作,线程都会一直阻塞直到完成。适用于连接数少的架构。
NIO:同步非阻塞,发送的请求会注册到多路复用器上,多路复用器轮询到连接有IO请求时才会开启一个线程。适用于连接数多且连接比较短的架构。
AIO:异步非阻塞,线程发起IO请求立即返回,内核做完IO操作后才会通知线程。适用于连接数多且连接比较长的架构。
-
如何读取文件a.txt中第10个字节?
FileInputStream fis = new FileInputStream("a.txt"); fis.skip(9); int b = fis.read();
-
节点流和处理流区别?
节点流:可以从某个节点读数据或写数据的流
处理流:对已有的流进行封装,提供更丰富的处理,构造方法必须是其他流的对象,比如BufferedReader
-
缓冲流的优缺点?
不带缓存的流读一个字节或字符就写一个字节或字符,缓冲流读取到字节或字符先放入缓冲区,当缓冲区满了再一次性写出去。
优点:减少了写的次数,提高效率
缺点:接受端无法即时接收到数据
Spring
-
spring的好处
- 方便解耦和开发,将对象的创建和依赖关系交给spring来处理
- 支持AOP编程,可以方便的实现权限拦截和监控等功能
- 声明式事务,通过配置就可以完成对事务的支持
- 方便集成别的框架
-
什么是IOC?
IOC就是控制反转,是一种设计思想,把对象的创建交给spring来处理,可以解耦合,提高程序的复用性
-
什么是AOP?
AOP就是面向切面编程,可以将一些与业务无关,但是被业务共同调用的逻辑封装起来,增加代码的复用性,降低模块间的耦合
-
spring注入方式
接口注入,setter注入,构造器注入
-
spring的bean是线程安全吗?
spring不能保证bean是线程安全的,因为默认bean是单例的,这样就可能存在竞争,就会造成线程不安全。
可以用ThreadLocal或锁来解决这个问题。
-
spring自动装配bean有哪些?
XML方式:
- no:不进行自动装配
- byName:根据名字自动装配
- byType:根据类型自动装配
- constructor:根据构造器自动装配
注解方式:
- @Autowired:通过类型自动装配,想要通过名字装配可以再加一个@Qualify注解来指定名称
- @Resource:先通过名称装配,找不到就通过类型装配
-
Bean的生命周期
分为4个阶段:实例化,属性赋值,初始化,销毁。
实例化的时候,可以通过InstantiationAwareBeanPostProcessor接口来进行扩展,可以在实例化之前调用它的方法来替换原本的bean作为代理,这也是AOP能实现的关键点。可以在实例化之后,属性赋值之前调用它的方法来阻断属性填充。
初始化阶段,可以调用BeanPostProcessor接口,一系列Aware接口,InitializingBean接口,来进行扩展。BeanPostProcessor接口作用在初始化的前后,Aware接口可以拿到Spring的一些资源,比如BeanName,BeanFactory,ApplicationContext,InitializingBean接口可以自定义一些初始化操作。初始化时调用init-method指定的方法。
销毁阶段,可以通过DisposableBean接口来进行扩展,最后调用destroy-method指定的方法进行销毁
-
BeanFactory 和 FactoryBean 的区别
BeanFactory 是一个工厂类,用来管理Bean的,最核心的功能就是加载Bean,也就是getBean()方法。
FactoryBean 是一个特殊的Bean,实现该接口的类可以实现它的getObject方法来自定义创建Bean实例
-
BeanFactory 和 ApplicationContext 的区别
BeanFactory 是一个基础的IOC容器,提供了完整的IOC功能。
ApplicationContext是BeanFactory的一个子接口,是一个高级的IOC容器,提供了更多的功能。
二者加载Bean的时机也不同。BeanFactory 是延迟加载,调用getBean()时才加载,而ApplicationContext在启动后就预加载所有单实例的Bean,到时候可以直接拿来使用
-
Spring 的 AOP 有哪几种创建代理的方式?
有JDK动态代理和Cglib代理。
JDK动态代理只能代理实现了接口的类,因为通过JDK动态代理生成的类已经实现了Proxy类,所以不能继承别的类了。
而Cglib代理没有这个限制,但是他不可以代理被final修饰的类或方法,因为他的本质是通过继承实现的
-
Spring是如何解决的循环依赖?
Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects),二级缓存为早期曝光对象earlySingletonObjects,三级缓存为早期曝光对象工厂(singletonFactories)。当A、B两个类发生循环引用时,在A完成实例化后,就使用实例化后的对象去创建一个对象工厂,并添加到三级缓存中,如果A被AOP代理,那么通过这个工厂获取到的就是A代理后的对象,如果A没有被AOP代理,那么这个工厂获取到的就是A实例化的对象。当A进行属性注入时,会去创建B,同时B又依赖了A,所以创建B的同时又会去调用getBean(a)来获取需要的依赖,此时的getBean(a)会从缓存中获取,第一步,先获取到三级缓存中的工厂;第二步,调用对象工工厂的getObject方法来获取到对应的对象,得到这个对象后将其注入到B中。紧接着B会走完它的生命周期流程,包括初始化、后置处理器等。当B创建完后,会将B再注入到A中,此时A再完成它的整个生命周期。
-
为什么要使用三级缓存呢?二级缓存能解决循环依赖吗?
如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。
-
Spring 的事务传播行为有哪些?
- REQUIRED:Spring 默认的事务传播级别,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。
- REQUIRES_NEW:每次都会新建一个事务,如果上下文中有事务,则将上下文的事务挂起,当新建事务执行完成以后,上下文事务再恢复执行。
- SUPPORTS:如果上下文存在事务,则加入到事务执行,如果没有事务,则使用非事务的方式执行。
- MANDATORY:上下文中必须要存在事务,否则就会抛出异常。
- NOT_SUPPORTED :如果上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
- NEVER:上下文中不能存在事务,否则就会抛出异常。
- NESTED:嵌套事务。如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
SpringMVC
-
什么是SpringMVC ?简单介绍下你对springMVC的理解?
SpringMVC 是Spring框架的一部分,把model,view,controller层分离,把复杂的web应用分成逻辑清晰的几部分,可以做到解耦,简化开发,方便开发人员之间的配合
-
SpringMVC的流程?
- 用户发送请求给DispatcherServlet
- DispatcherServlet调用HandlerMapping,请求获取Handler
- HandlerMapping根据url找到对应handler,返回给DispatcherServlet
- DispatcherServlet调用HandlerAdapter,请求执行Handler
- HandlerAdapter执行Handler,Handler执行完将ModelAndView返回给HandlerAdapter
- HandlerAdapter将ModelAndView返回给DispatcherServlet
- DispatcherServlet将ModelAndView传给ViewResolver进行视图解析
- ViewResolver解析完将View返回给DispatcherServlet
- DispatcherServlet对View渲染,返回给用户
-
如何解决POST请求中文乱码问题,GET的又如何处理呢?
POST:在web.xml里配置CharacterEncodingFilter 过滤器
GET:两种方法
- 可以将tomcat的编码修改,与工程保持一致
- 可以对参数进行重新编码
-
@RequestMapping的作用是什么?
用来标识HTTP请求地址与Controller之间的映射关系。
可以加在类上,也可以加在方法上,完整的路径是类上的@RequestMapping的value加上方法上的RequestMapping的value
-
SpringMvc的控制器是不是单例模式?如果是,有什么问题?怎么解决?
是,在多线程访问时会出现线程不安全的问题。
解决方法:控制器里对可变状态量使用ThreadLocal,为每个线程生成一个副本,互不影响
SpringBoot
-
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
-
SpringBoot自动装配原理
Springboot通过启动类的@SpringBootApplication注解来实现自动装配。该注解是一个组合注解。包含三个注解
- @SpringBootConfiguration:用来进行spring的配置,该注解有@Configuration注解,@Configuration注解有@Component,所以说明他也是spring的一个组件
- @EnableAutoConfiguration:用来开启自动配置的注解。该注解主要由@AutoConfigurationPackage和@Import注解组成。@AutoConfigurationPackage注解也使用@Import注解,里面传入了Registrar.class,这个类里面有一个方法可以获得扫描包的路径。@Import注解中传入了一个组件选择器,里面有一个方法可以将需要导入的组件的全类名返回,这些组件就会被添加到容器中
- @ComponentScan:用类扫描spring组件的注解
这篇关于Java面试题总结(持续更新)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-01基于Python+Vue开发的医院门诊预约挂号系统
- 2024-10-01基于Python+Vue开发的旅游景区管理系统
- 2024-10-01RestfulAPI入门指南:打造简单易懂的API接口
- 2024-10-01初学者指南:了解和使用Server Action
- 2024-10-01Server Component入门指南:搭建与配置详解
- 2024-10-01React 中使用 useRequest 实现数据请求
- 2024-10-01使用 golang 将ETH账户的资产平均分散到其他账户
- 2024-10-01JWT用户校验课程:从入门到实践
- 2024-10-01Server Component课程入门指南
- 2024-09-30Dnd-Kit学习:新手快速入门指南