欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

史上最全Java面试题(带全部答案,你可能要收藏!)

程序员文章站 2022-04-19 15:34:32
...

JAVA最全面试题

JAVA基础

一、String,Stringbuffer,StringBuilder的区别?

1、执行速度
StringBuilder > StringBuffer > String

我们知道String是字符串常量,不可变对象,因此每次对String进行操作的时候实际上是生成了一个新的String对象,然后将指针指向新的String对象上,之前的String对象就没有了指针引用,当内存中无引用的对象多了之后,就会触发JVM的GC操作了。

StringBuilder和StringBuffer是字符串变量,因此当我们对字符串做操作的时候,实际上都是操作的同一个对象,不会创建新的对象。

2、线程安全
StringBuilder是线程不安全的,而StringBuffer是线程安全的

如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

String:适用于少量的字符串操作的情况

StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况

StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

二、ArrayList和LinkedList有什么区别?

LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

1、数据结构不同
ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
2、效率不同 当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。

三、类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序?

当创建类对象时,先初始化静态变量和静态块,然后是非静态变量和非静态代码块,然后是构造器。由于静态成员只会被初始化一次,所以如果静态成员已经被初始化过,将不会被再次初始化。

四、HashMap和Hashtable理解与对比

HashTable
底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
初始size为11,扩容:newsize = olesize*2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap
底层数组+链表实现,可以存储null键和null值,线程不安全
初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
计算index方法:index = hash & (tab.length – 1)

线程安全性不同,HashMap的方法都没有使用synchronized关键字修饰,都是非线程安全的,而Hashtable的方法几乎都是被synchronized关键字修饰的。但是,当我们需要HashMap是线程安全的时,怎么办呢?我们可以通过Collections.synchronizedMap(hashMap)来进行处理,亦或者我们使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。

五、HashMap和ConcurrentHashMap的区别?

1、HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。

2、ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。

3、ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。

数据库相关

一、数据库引擎,Mysql几种数据库引擎的区别?

1、常用的3种

2、InnoDB Myisam Memory

3、InnoDB跟Myisam的默认索引是B+tree,Memory的默认索引是hash

区别:

1、InnoDB支持事务,支持外键,支持行锁,写入数据时操作快,MySQL5.6版本以上才支持全文索引

2、Myisam不支持事务。不支持外键,支持表锁,支持全文索引,读取数据快

3、Memory所有的数据都保留在内存中,不需要进行磁盘的IO所以读取的速度很快, 但是一旦关机的话表的结构会保留但是数据就会丢失,表支持Hash索引,因此查找速度很快

二、锁机制?死锁的场景及解决?

MySQL各存储引擎使用了三种类型(级别)的锁定机制:表级锁定,行级锁定和页级锁定。

死锁:数据库是一个多用户使用的共享资源,当多个用户 并发地存取数据的时候,在数据库中就会发生多个事务同时存取同一个数据的情况,加锁是进行数据库并发控制的一种非常重要的技术。在实际应用中,如果两个事务需要一组有冲突的锁,而不能继续进行下去,这时便发生了死锁。

死锁出现的常见原因:
1.事务之间对资源访问顺序的交替
此类常见于两个用户分别首先访问A表和B表,并锁住当前表,但是双方都需要访问对方锁住的表,都在等待对方释放锁,也就是经典的刀叉问题。像这样的死锁,基本上就是程序员的编程逻辑出现了问题,需要调整程序的逻辑。

2.并发修改同一记录
此类常见于用户A查询一条纪录,然后修改该条纪录;这时用户B修改该条纪录,这时用户A的事务里锁的性质由查询的共享锁企图上升到独占锁,而用户B里的独占锁由于A有共享锁存在所以必须等A释放掉共享锁,而A由于B的独占锁而无法上升的独占锁也就不可能释放共享锁,于是出现了死锁。这种死锁由于比较隐蔽,但在稍大点的项目中经常发生。像这样的解决办法就是使用乐观锁,在表的字段中增加version字段,更新时进行查询version是否为当前取到version。乐观锁可以避免长事务中的数据库加锁开销。最不推荐的是采用悲观锁进行控制,悲观锁会使排队等待的时间相当漫长,会发生灾难性的后果。

3.索引不当导致全表扫描
此类常见于在事务中执行了一条不满足的语句,执行全表扫描,把行级锁上升为表级锁,多个这样的事务执行之后,就很容易产生死锁和阻塞。类似的情况还有当表中的数据量非常庞大而索引建的过少或不合适的时候,使得经常发生全表扫描,最终应用系统会越来越慢,最终发生阻塞或死锁。解决其办法有,SQL语句中不要使用太复杂的关联多表的查询;使用“执行计划”对SQL语句进行分析,对于有全表扫描的SQL语句,建立相应的索引进行优化。

4.事务*范围大且相互等待
·保持事务简短并在一个批处理中
在同一数据库中并发执行多个需要长时间运行的事务时通常发生死锁。事务运行时间越长,其持有排它锁或更新锁的时间也就越长,从而堵塞了其它活动并可能导致死锁。保持事务在一个批处理中,可以最小化事务的网络通信往返量,减少完成事务可能的延迟并释放锁。
·使用低隔离级别
确定事务是否能在更低的隔离级别上运行。执行提交读允许事务读取另一个事务已读取(未修改)的数据,而不必等待第一个事务完成。使用较低的隔离级别(例如提交读)而不使用较高的隔离级别(例如可串行读)可以缩短持有共享锁的时间,从而降低了锁定争夺(比如这次的S NK和X IK 是InnoDB引擎Repeatable Read级别才有的)。

三、数据库优化方案?

1、SQL以及索引的优化
首先要根据需求写出结构良好的SQL,然后根据SQL在表中建立有效的索引。但是如果索引太多,不但会影响写入的效率,对查询也有一定的影响。

2、合理的数据库是设计

  1. 系统配置的优化

  2. 硬件优化
    更快的IO、更多的内存。一般来说内存越大,对于数据库的操作越好。但是CPU多就不一定了,因为他并不会用到太多的CPU数量,有很多的查询都是单CPU。另外使用高的IO(SSD、RAID),但是IO并不能减少数据库锁的机制。所以说如果查询缓慢是因为数据库内部的一些锁引起的,那么硬件优化就没有什么意义。

四、搜索执行计划分析?如何合理的构建高性能索引,索引的建立准则。最左原则理解?

使用explain语句去查看分析结果

InnoDB 是通过 B+Tree 结构对 ID 建索引,然后在叶子节点中存储记录。采用 InnoDB 引擎的数据存储文件有两个,一个定义文件,一个是数据文件。

若建索引的字段不是主键 ID,则对该字段建索引,然后在叶子节点中存储的是该记录的主键,然后通过主键索引找到对应的记录。

B+Tree和B-Tree之间一个很大的不同是B+树的节点上不储存value,只储存key,而叶子节点上储存了所有kv集合,并且节点之间都是有序的。
这样的好处是每一次磁盘IO能够读取的节点更多,也就是树的度可以设置的更大一些,因为每次磁盘IO读取的磁盘页数是一定的,例如每次磁盘IO能够读取1页=4kb,那么省去value的情况下同样一页数据能够读取更多的key,这样就大大减少了磁盘的IO次数。
此外,B+树是排序好的数据结构,数据库中><或者order by等都可以直接依赖这一特性。

五、悲观锁和乐观锁的区别,怎么实现?

悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。

乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作。

JVM

一、为什么要对堆内存分代

其实不分代完全可以,分代的唯一理由就是优化GC性能。你先想想,如果没有分代,那我们所有的对象都在一块,GC的时候我们要找到哪些对象没用,这样就会对堆的所有区域进行扫描。而我们的很多对象都是朝生夕死的,如果分代的话,我们把新创建的对象放到某一地方,当GC的时候先把这块存“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

二、年轻代

JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次MinorGC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC(young GC),年龄就会增加1岁,当它的年龄增加到一定程度(15岁)时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制整理算法,复制整理算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面

三、老年代

当年轻带随着不断地Minor GC ,from survivor中的对象会不断成长,当from survivor中的对象成长大15岁的时候,就会进入老年代,随着Minor GC的持续进行,老年代中对象也会持续增长,最终老年代的空间也会不够用,此时就会执行老年代的GC–>MajorGC。MajorGC使用的算法是标记清除算法或者标记-压缩算法。

四、JVM内存调优

首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。

对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数,过多的GC和Full GC是会占用很多的系统资源(主要是CPU),影响系统的吞吐量。特别要关注Full GC,因为它会对整个堆进行整理,导致Full GC一般由于以下几种情况:

1、旧生代空间不足
  调优时尽量让对象在新生代GC时被回收、让对象在新生代多存活一段时间和不要创建过大的对象及数组避免直接在旧生代创建对象

2、Pemanet Generation空间不足
  增大Perm Gen空间,避免太多静态对象

统计得到的GC后晋升到旧生代的平均大小大于旧生代剩余空间

控制好新生代和旧生代的比例

3、System.gc()被显示调用
  垃圾回收不要手动触发,尽量依靠JVM自身的机制

4、调优手段主要是通过控制堆内存的各个部分的比例和GC策略来实现,下面来看看各部分比例不良设置会导致什么后果
1). 新生代设置过小

一是新生代GC次数非常频繁,增大系统消耗;二是导致大对象直接进入旧生代,占据了旧生代剩余空间,诱发Full GC

2). 新生代设置过大

一是新生代设置过大会导致旧生代过小(堆总量一定),从而诱发Full GC;
二是新生代GC耗时大幅度增加,一般说来新生代占整个堆1/3比较合适

3). Survivor设置过小

导致对象从eden直接到达旧生代,降低了在新生代的存活时间

4). Survivor设置过大

导致eden过小,增加了GC频率

另外,通过-XX:MaxTenuringThreshold=n来控制新生代存活时间,尽量让对象在新生代被回收

5、由内存管理和垃圾回收可知新生代和旧生代都有多种GC策略和组合搭配,选择这些策略对于我们这些开发人员是个难题,JVM提供两种较为简单的GC策略的设置方式
1). 吞吐量优先

JVM以吞吐量为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,来达到吞吐量指标。这个值可由-XX:GCTimeRatio=n来设置

2). 暂停时间优先

JVM以暂停时间为指标,自行选择相应的GC策略及控制新生代与旧生代的大小比例,尽量保证每次GC造成的应用停止时间都在指定的数值范围内完成。这个 值可由-XX:MaxGCPauseRatio=n来设置

Redis

一、什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?

缓存穿透

一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

如何避免?

1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 了之后清理缓存。
2:对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该 bitmap 过滤。

缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。

如何避免?

1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
2:做二级缓存,A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期
3:不同的 key,设置不同的过期时间,让缓存失效的时间点尽量均匀

二、单线程的redis为什么这么快

1、纯内存操作
2、单线程操作,避免了频繁的上下文切换
3、采用了非阻塞I/O多路复用机制

三、假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

四、追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

五、Redis 应用场景

缓存
计数器
发布订阅构建消息系统
排行榜

Spring

一、什么是AOP,AOP的作用是什么,可以做哪些事?

在面向切面编程中,我们将一个个对象某些类似的方面横向抽象成一个切面,对这个切面进行一些如权限验证,事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。

日志记录、事务控制、权限控制、性能统计、异常处理及事务处理

二、Spring下IOC容器和DI(依赖注入Dependency injection)?

IOC容器:就是具有依赖注入功能的容器,是可以创建对象的容器,IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。通常new一个实例,控制权由程序员控制,而"控制反转"是指new实例工作不由程序员来做而是交给Spring容器来做。。在Spring中BeanFactory是IOC容器的实际代表者。

DI(依赖注入Dependency injection) :在容器创建对象后,处理对象的依赖关系。

依赖注入spring的注入方式:

	set注入方式
	静态工厂注入方式
	构造方法注入方式
	基于注解的方式

三、@Autowired与@Resource的区别?

@Autowired默认按类型装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用,如下:

Java代码
@Autowired() @Qualifier(“baseDao”)
private BaseDao baseDao;

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

Java代码
@Resource(name=“baseDao”)
private BaseDao baseDao;

四、Spring Bean 生命周期?

1.Spring 容器 从 XML 文件中读取 Bean 的定义,并实例化 Bean。

2.Spring 根据 Bean 的定义填充所有的属性。

3.如果 Bean 实现了 BeanNameAware 接口,Spring 传递 bean 的 ID 到 setBeanName 方法。

4.如果 Bean 实现了 BeanFactoryAware 接口, Spring 传递 beanfactory 给 setBeanFactory 方法。

5.如 果 有 任 何 与 bean 相 关 联 的 BeanPostProcessors , Spring 会 在postProcesserBeforeInitialization()方法内调用它们。

6.如果 bean 实现 IntializingBean 了,调用它的 afterPropertySet 方法,如果 bean 声明了初始化方法,调用此初始化方法。

7.如果有 BeanPostProcessors 和 bean 关联,这些 bean 的 postProcessAfterInitialization()

方法将被调用。

8.如果 bean 实现了 DisposableBean,它将调用 destroy()方法。

五、SpringMVC执行原理?

1、用户发送请求至 DispatcherServlet(前端控制器);

2、DispatcherServlet 收到请求调用 HandlerMapping(处理器映射器);

3、HandlerMapping 找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet;

4、DispatcherServlet 调用 HandlerAdapter(处理器适配器);

5、HandlerAdapter 经过适配调用具体的 Controller (处理器,也叫后端控制器);

6、Controller 执行完成返回 ModelAndView 对象;

7、HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet;

8、DispatcherServlet 将 ModelAndView 传给 ViewReslover(视图解析器);

9、ViewReslover 解析后返回具体 View;

10、DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中);

11、DispatcherServlet 响应用户。

高阶题

一、高并发的解决方案

https://blog.csdn.net/sanyaoxu_2/article/details/78992113
1.应用和静态资源分离
2.页面缓存
3.集群与分布式
4.反向代理
5.CDN
6.底层的优化
7.数据库集群和库表散列

二、海量数据的解决方案

1、使用缓存
2、页面静态化
3、数据库优化
3.1 数据库表结构涉及
3.2 数据类型的选用
3.3 sql优化
3.4 索引优化
3.5 配置优化
4、分离数据库中的活跃数据
5、读写分离

三、多线程的sleep方法和wait方法有什么区别?

sleep()来自 Thread 类,wait()来自 Object 类;

调用 sleep()方法,线程不会释放对象锁。而调用 wait 方法线程会释放对象锁;

sleep()睡眠后不出让系统资源,wait 让其他线程可以占用 CPU;

sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒。而 wait()需要配合 notify() 或者 notifyAll()使用。

四、启动一个线程是run()还是start()?它们的区别?

start();

run():封装了被线程执行的代码,直接调用仅仅是普通方法的调用。
start():启动线程,并由JVM自动调用run()方法。

五、执行execute()方法和submit()方法的区别是什么呢?

1)execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

2)submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

六、有三个线程T1,T2,T3,怎么确保它们按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

七、如果你提交任务时,线程池队列已满。这时会发生什么?

这个问题问得很狡猾,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么ThreadPoolExecutor’s submit()方法将会抛出一个RejectedExecutionException异常。

八、Dubbo的注册中心集群挂掉,发布者和订阅者之间还能通信么?

可以的,启动dubbo时,消费者会从zookeeper拉取注册的生产者的地址接口等数据,缓存在本地。

每次调用时,按照本地存储的地址进行调用。