2022届校招Java面试题汇总(含题解)

本文将笔者在面试中遇到的真实题目汇总成集,初衷是为了每次面试完能够及时复盘,以便查漏补缺。此外,如果能给他人以参考也是好的。

持续更新中…

真实面经

一、2021年8月17日 阿里集团-CTO线-MMC技术部 研发工程师Java 一面34分钟

1.项目相关

坦克小游戏项目,主要是对于面向对象的综合应用,结合使用了多线程、事件监听、接口、容器等技术…

2.接口和抽象类的区别

1.抽象类中的方法既可以有抽象方法,也可以有普通方法,但是接口中只能包含抽象方法
2.抽象类需要使用abstract关键字来修饰,而接口使用interface关键字来修饰
3.子类使用extends关键字来继承抽象类,使用implement关键字来实现接口
4.子类继承抽象类的时候必须要实现所有的抽象方法,普通方法可以不重写,而接口中的所有方法必须实现
5.抽象类中可以定义成员变量,而接口中只能定义静态常量
6.抽象类在子类实现的时候时单继承,为接口实现时为多继承
7.抽象类和接口都不能实例化,但是抽象类中可以有构造方法,而接口中不能有构造方法
8.抽象类中可以实现接口,并且不实现接口中的方法,而接口只能继承接口,不能实现接口

传送门: 接口和抽象类的区别.1

3.Java中的继承和C++有什么不同

C++中可以实现多继承,而Java只能实现单继承。在Java中,如果想要实现继承多个父类,需要使用接口技术。

4.API和SPI的区别

在面向接口编程的思想中,一个接口对外暴露主要会涉及到调用方和实现方,通常情况下,接口由实现方负责定义和实现,调用方只负责调用,这便是通常意义上的API。但还存在一种情况是,调用方手握大权,接管了定义接口的职责,这时需要不同的实现方去实现该接口,而后调用方再取到这些实现类去用,这便是所谓的SPI。举个例子,在JDK中(调用方)规定了JDBC的驱动接口,由不同的厂商(实现方)进行实现,而后我们在JDK(调用方)中引入这些实现了的驱动包进行使用。

5.线程和进程的区别

进程: 是操作系统资源分配的基本单位,每个进程都有独立的代码和数据空间(程序上下文)。由于进程比较重量级,占据独立的内存,且进程间主要通过管道(Pipe)、命名管道(FIFO)、消息队列(Message)、信号量(Semaphore)、共享内存(Shared Memory)和套接字(Socket)进行通信,所以程序之间切换会有较大的开销,但是相对比较稳定和安全。

线程: 是处理器任务调度和执行的基本单位,属于进程中的一个任务执行单元,可以称之为轻量级进程。线程间通信主要通过共享内存,因此线程之间的上下文切换负担要比进程小得多,但是相对进程而言不够稳定,容易丢失数据。

联系: 通常,一个进程中可以包含多个线程,这些线程共享进程的堆和方法区资源,但每个线程都拥有自己私有的程序计数器、虚拟机栈和本地方法栈。进程中的多线程可以并发执行。一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃将会导致整个进程都死掉。因此多进程要比多线程健壮。

注意1: 程序计数器为什么是私有的?
因为程序计数器主要是用于线程切换后能恢复到正确的执行位置。

注意2: 虚拟机栈和本地方法栈为什么是私有的?
因为虚拟机栈中的每一个方法都是一个栈帧,每个栈帧中存储着当前方法的局部变量表、操作数栈、动态链接(常量池引用)和返回值地址等信息,这些信息属于方法内部的局部信息,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈应该是线程私有的。同理,本地方法栈中存储着虚拟机使用到的Native方法,也应该是线程私有的。

6.线程和协程的区别?什么场景下用到协程?

协程: 是一种用户态的轻量级线程,协程的调度不由操作系统内核管理,而是完全由程序员所控制,也就是在用户态执行。协程的特点在于它在执行过程中只有一个线程参与执行,因此相比多线程,协程之间的切换不涉及任何系统调用或者阻塞调用,同时也不需要多线程的锁机制,因此具有极高的执行效率。

要理解是什么是“用户态的线程”,必然就要先理解什么是“内核态的线程”。 内核态的线程是由操作系统来进行调度的,在切换线程上下文时,要先保存上一个线程的上下文,然后执行下一个线程,当条件满足时,切换回上一个线程,并恢复上下文。 协程也是如此,只不过,用户态的线程不是由操作系统来调度的,而是由程序员来调度的,是在用户态的。

yield这个关键字就是用来产生中断, 并保存当前的上下文的,比如说程序的一段代码是访问远程服务器,那这个时候CPU就是空闲的,就用yield让出CPU,接着执行下一段的代码,如果下一段代码还是访问除CPU以外的其它资源,还可以调用yield让出CPU。 继续往下执行,这样就可以用同步的方式写异步的代码了。

使用场景: 协程适用于IO阻塞且需要大量并发的场景,当发生IO阻塞,由协程的调度器进行调度,通过将数据流yield掉,并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复协程栈,并把阻塞的结果放到这个线程上去运行。

7.怎么理解容器的线程安全与线程不安全?里面具体做了什么样的实现?

容器的线程不安全: 指的是当多个线程同时对非线程安全的集合容器进行增删改查的时候,由于容器对于线程的不同步,而导致集合中元素的数据一致性受到破坏。例如某一个线程在读取集合中的某个数据时,这一数据被另一个线程修改了,而第一个线程并没有获取到同步的数据从而引发错误。

容器的线程安全: 指的是在容器在多线程访问的情况下,其内部集合做了同步处理,从而能够保证数据一致性。

具体实现: 对集合容器内部的方法进行加锁,以map为例,hashMap由于内部完全没有锁,所以它是线程不安全的容器,而hashTable则是直接将整个Map对象加上了synchronized锁,所以他是线程安全的。但需要注意的是,由于hashTable内部存在着大量的重量级锁,因此其效率非常低,目前基本上已经被淘汰了,取而代之的是较细粒度的synchronousMap(只在方法中加synchronized代码块,不加在整个方法上)和concurrentHashMap。

此外,线程安全的集合包括:Vector、HashTable、synchronousMap、concurrentHashMap等。

线程不安全的集合包括:ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等。

8.怎么理解乐观锁和悲观锁?

乐观锁: 在操作数据时认为别人不会修改数据,因此不加锁,在更新数据时采用比较判断的方式进行操作,如果当前数据被修改过,则放弃操作,否则就执行操作。典型的乐观锁有CAS机制,它的操作过程包括compare 和 set,在操作数据时,先去比较待修改对象是否为它自身所持有的对象,然后比较该对象的数据是否等于期望数据,如果都为是,那就将该对象数据修改为新的数据。采用CAS操作实现乐观锁的类有java.util.concurrent.atomic包下的原子操作类,如AtomicInteger、AtomicLong等。但是对于CAS操作,容易产生ABA问题,如果修改操作的对象是引用数据类型,需要加版本号机制来解决。

版本号机制: 在表中加一个 version 字段;
当读取数据时,连同这个 version 字段一起读出;
数据每更新一次就将此值加一;
当提交更新时,判断数据库表中对应记录的当前版本号是否与之前取出来的版本号一致,如果一致则可以直接更新,如果不一致则表示是过期数据需要重试或者做其它操作。

悲观锁: 在操作数据时总是认为别人会修改数据,因此需要加锁,在更新数据时,会锁住已经抢到的资源,待操作执行完毕才释放。典型的悲观锁实现方式为synchronized关键字(锁方法或代码块)、ReentrantLock独占锁和MySQL中的排他锁(锁数据)。synchronized是一个重量级锁,在多线程访问共享数据时同一时刻只能由单个线程抢到资源去执行,效率v较低,同时,在线程切换的过程中还会涉及到操作系统内核态与用户态的切换,这些操作会消耗额外的资源。

但是随着JVM对synchronized的锁升级,synchronized锁的性能也得到了进一步的优化,目前效率提高了不少。

9.情景题:数据库中有一个商品库存的字段,通过多线程访问来做扣减操作,这时要采用什么方式来保证数据的同步?怎么理解行级锁?如果并发量更高的话,比如100件库存同时有1000个人在抢,要怎么保证数据同步?
10.怎么理解自旋锁?为什么还会有自旋锁?

线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。 同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。 所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。

怎么等待呢?执行一段无意义的循环即可(自旋)。 自旋等待不能替代阻塞,先不说对处理器数量的要求,虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。 如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。 所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。

自旋锁在JDK 1.4.2中引入,默认关闭,但是可以使用-XX:+UseSpinning开启,在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整; 如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应自旋锁:
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。 所谓自适应就意味着自旋的次数不再是固定的,它由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

它怎么做呢? 线程如果自旋成功了,那么下次自旋的次数会更多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

答案传送门:

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值