Java面试题库(长期)

本文内容来自互联网各种面试实例,以及自己的面试经历,主要是中级开发的面试题

初中级java面试主要分为几个部分:

0、序
1、java基础
2、 java多线程
3、 jvm知识
4、 spring等框架知识
5、 常用实践,如session同步
6、 其他知识,例如tomcat
7、 笔试算法

序言

首先,外貌要干净整洁,这个是必须的。其次守时,既不能晚点也不能早到,最好在约定时间的前十分钟
面试主要分为几个部分,首先是java基础,这类占比

较少
;其次是对java及spring框架的的深入理解,如多线程,ioc,apo,spring bean的生命周期,这类占比

较重
;再往后就是常用的工具的理解,如jvm的常用配置,年轻代老年代,gc,tomcat等容器怎么处理请求,这类问题占比

适中
;最后就是一些广度的问题(实际的经验),对自己项目的理解,用到了那些工具,遇到了哪些问题,解决的方法
最后,一定要做一些面试的准备,刷面试题、练习面试,建议至少提前一个月做准备,机会是留给有准备的人的

1、java基础

1.1、List、Set、Map的异同
List(列表)

List
的元素以线性方式存储,可以存放重复对象,List主要有以下两个实现类:

ArrayList
长度可变的数组,可以对元素进行随机的访问,向ArrayList中插入与删除元素的速度慢。 JDK8 中ArrayList扩容的实现是通过grow()方法里使用语句newCapacity = oldCapacity + (oldCapacity >> 1)(即1.5倍扩容)计算容量,然后调用Arrays.copyof()方法进行对原数组进行复制。
LinkedList
采用链表数据结构,插入和删除速度快,但访问速度慢。

Set(集合)

Set
中的对象不按特定(HashCode)的方式排序,并且没有重复对象,Set主要有以下两个实现类:

HashSet
: HashSet按照哈希算法来存取集合中的对象,存取速度比较快。当HashSet中的元素个数超过数组大小/*loadFactor(默认值为0.75)时,就会进行近似两倍扩容(newCapacity = (oldCapacity << 1) + 1)。

TreeSet
:TreeSet实现了SortedSet接口,能够对集合中的对象进行排序。

Map(映射)

Map
是一种把键对象和值对象映射的集合,它的每一个元素都包含一个键对象和值对象。 Map主要有以下两个实现类:

HashMap
:HashMap基于散列表实现,其插入和查询<K,V>的开销是固定的,可以通过构造器设置容量和负载因子来调整容器的性能。
LinkedHashMap:类似于HashMap,但是迭代遍历它时,取得<K,V>的顺序是其插入次序,或者是最近最少使用(LRU)的次序。

TreeMap
:TreeMap基于红黑树实现。查看<K,V>时,它们会被排序。TreeMap是唯一的带有subMap()方法的Map,subMap()可以返回一个子树。
比较 List Set Map 继承接口 Collection Collection 常见实现类 AbstractList(其常用子类有ArrayList、LinkedList、Vector) AbstractSet(其常用子类有HashSet、LinkedHashSet、TreeSet) HashMap、HashTable 常见方法 add( )、remove( )、clear( )、get( )、contains( )、size( ) add( )、remove( )、clear( )、contains( )、size( ) put( )、get()、remove( )、clear( )、containsKey( )、containsValue( )、keySet( )、values( )、size( ) 元素 可重复 不可重复(用
equals()判断) 不可重复 顺序 有序 无序(实际上由HashCode决定) 线程安全 Vector线程安全 Hashtable线程安全

1.2、String、StringBuffer、StringBuilder的使用

1.3、HashMap、TreeMap、LinkedHashMap的特点

1.4、HashMap内部结构、算法
底层实现:HashMap底层整体结构是一个数组,数组中的每个元素又是一个链表。每次添加一个对象(put)时会产生一个链表对象(Object类型),Map中的每个Entry就是数组中的一个元素(Map.Entry就是一个<Key,Value>),它具有由当前元素指向下一个元素的引用,这就构成了链表。
存储原理:当向HsahMap中添加元素的时候,先根据HashCode重新计算Key的Hash值,得到数组下标,如果数组该位置已经存在其他元素,那么这个位置的元素将会以链表的形式存放,新加入的放在链头,最先加入的放在链尾,如果数组该位置元素不存在,那么就直接将该元素放到此数组中的该位置。
去重原理:不同的Key算到数组下标相同的几率很小,新建一个<K,V>放入到HashMap的时候,首先会计算Key的数组下标,如果数组该位置已经存在其他元素,则比较两个Key,若相同则覆盖写入,若不同则形成链表。
读取原理:从HashMap中读取(get)元素时,首先计算Key的HashCode,找到数组下标,然后在对应位置的链表中找到需要的元素。
扩容机制:当HashMap中的元素个数超过数组大小/*loadFactor(默认值为0.75)时,就会进行2倍扩容(oldThr << 1)。

1.5、concurrent包下面有那几大类
atomic
locks
Executor
Queue
Dueue
ConcurrentXX
Scheduled
Callable
Future

2、java多线程

2.1、lock和synchronized

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:

如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。

总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

Lock是一个接口,而synchronized是关键字。
synchronized会自动释放锁,而Lock必须手动释放锁。
Lock可以让等待锁的线程响应中断,而synchronized不会,线程会一直等待下去。
通过Lock可以知道线程有没有拿到锁,而synchronized不能。
Lock能提高多个线程读操作的效率。

synchronized能锁住类、方法和代码块,而Lock是块范围内的

第一大不足:由于我们没办法设置synchronized关键字在获取锁的时候等待时间,所以synchronized可能会导致线程为了加锁而无限期地处于阻塞状态。

第二大不足:使用synchronized关键字等同于使用了互斥锁,即其他线程都无法获得锁对象的访问权。这种策略对于读多写少的应用而言是很不利的,因为即使多个读者看似可以并发运行,但他们实际上还是串行的,并将最终导致并发性能的下降。

虽然synchronized已经作为一个关键字被固化在Java语言中了,但它只提供了一种相当保守的线程安全策略,且该策略开放给程序员的控制能力极弱。

2.2、单机上一个线程池正在处理服务,如果忽然断电了怎么办(正在处理和阻塞队列里的请求怎么处理)?

2.3、为什么要使用线程池?

在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在处理实际的用户请求的时间和资源要多得多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM里创建太多的线程,可能会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。

2.4、线程池有什么作用?

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目可以防止出现资源不足的情况。

1
2
3
4
5
6
7
8
9
1.使用new Thread()创建线程的弊端:  
每次通过new Thread()创建对象性能不佳。
线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
缺乏更多功能,如定时执行、定期执行、线程中断。

2.使用Java线程池的好处:
重用存在的线程,减少对象创建、消亡的开销,提升性能。
可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
提供定时执行、定期执行、单线程、并发数控制等功能。

2.5、说说几种常见的线程池及使用场景。
场景:
单个任务处理时间短
将需处理的任务数量大

2.6、线程池都有哪几种工作队列?
ArrayBlockingQueue 数组型阻塞队列
LinkedBlockingQueue 链表型阻塞队列
DelayQueue 延时队列
SynchronousQueue 同步队列
PriorityBlockingQueue 优先阻塞队列

2.7、怎么理解无界队列和有界队列?
有界队列:
1.初始的poolSize < corePoolSize,提交的runnable任务,会直接做为new一个Thread的参数,立马执行 。

2.当提交的任务数超过了corePoolSize,会将当前的runable提交到一个block queue中,。

3.有界队列满了之后,如果poolSize < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。

4.如果3中也无法处理了,就会走到第四步执行reject操作。

与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。

2.8、线程池中的几种重要的参数及流程说明

中止
:Abort策略,默认策略,新任务提交时直接抛出未检查的异常RejectedExecutionException,该异常可由调用者捕获。

抛弃
:Discard策略,新提交的任务被抛弃。

抛弃最旧的
:DiscardOldest策略,队列的是“队头”的任务,然后尝试提交新的任务。(不适合工作队列为优先队列场景)

调用者运行
: CallerRuns策略,为调节机制,既不抛弃任务也不抛出异常,而是将某些任务回退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务。

2.9、线程池中的几种重要的参数及流程说明

1
2
3
4
5
6
7
8
9
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}

corePoolSize

  • 池中所保存的线程数,包括空闲线程。需要注意的是在初创建线程池时线程不会立即启动,直到有任务提交才开始启动线程并逐渐时线程数目达到corePoolSize。若想一开始就创建所有核心线程需调用prestartAllCoreThreads方法。

maximumPoolSize
-池中允许的最大线程数。需要注意的是当核心线程满且阻塞队列也满时才会判断当前线程数是否小于最大线程数,并决定是否创建新线程。

keepAliveTime

  • 当线程数大于核心时,多于的空闲线程最多存活时间

unit

  • keepAliveTime 参数的时间单位。

workQueue

  • 当线程数目超过核心线程数时用于保存任务的队列。主要有3种类型的BlockingQueue可供选择:无界队列,有界队列和同步移交。将在下文中详细阐述。从参数中可以看到,此队列仅保存实现Runnable接口的任务。

threadFactory

  • 执行程序创建新线程时使用的工厂。

handler

  • 阻塞队列已满且线程数达到最大值时所采取的饱和策略。java默认提供了4种饱和策略的实现方式:中止、抛弃、抛弃最旧的、调用者运行。将在下文中详细阐述。

2.10、线程和cpu核心数的关系

线程数=Ncpu / (1-阻塞系数)
IO密集型,阻塞系数接近于1
计算密集型,阻塞系数接近于0

3、jvm知识

3.1、happened-before原则

1.程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作

2.监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁

3.volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读

4.传递性:如果A happens-before B,且B happens-before C,那么A happens-before C

5.start规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作

6.join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回

3.2、类的加载

image
https://blog.csdn.net/wen7280/article/details/53856790

类装载的条件:

Java虚拟机不会无条件的装载Class类型。

Java虚拟机规定:一个类或者接口在初次使用时,必须进行初始化。

这里的使用指的是主动使用,主动使用有以下几种情况:

当创建一个类的实例时,比如使用new关键字,或者通过反射、克隆、反序列化方式。

当调用类的静态方法时,即当使用了字节码invokestatic指令

当使用类或者接口的静态字段时(final常量除外,此种情况只会加载类而不会进行初始化),即使用getstatic或者putstatic指令(可以使用jclasslib软件查看生成的字节码文件)

当使用java.lang.reflect包中的方法反射类的方法时

当初始化子类时,必须先初始化父类

作为启动虚拟机、含有main方法的那个类

除了以上情况属于主动使用外,其他情况均属于被动使用,被动使用不会引起类的初始化,只是加载了类却没有初始化。

4、spring等框架知识

4.1、spring mvc处理请求

image

SpringMVC核心处理流程:

1、DispatcherServlet前端控制器接收发过来的请求,交给HandlerMapping处理器映射器

2、HandlerMapping处理器映射器,根据请求路径找到相应的HandlerAdapter处理器适配器(处理器适配器就是那些拦截器或Controller)

3、HandlerAdapter处理器适配器,处理一些功能请求,返回一个ModelAndView对象(包括模型数据、逻辑视图名)

4、ViewResolver视图解析器,先根据ModelAndView中设置的View解析具体视图

5、然后再将Model模型中的数据渲染到View上

4.2、Springbootapplication注解的原理

image

http://majunwei.com/view/201708231840127244.html

5、常用实践,session同步

5.1、session同步

使用redis作为session持久化存储。首先用户连接进来,把session放在本地一份,redis一份,在本地有记录的情况下使用本地缓存(设置极小的时间过期,如2s)。当用户再次连接进来,在本地记录过期,或者本地没有session的情况下,使用redis的记录,

5.2、redis的简单介绍

5.3、缓存的使用

6、其他知识,如tomcat

6.1、tomcat如何处理请求

http://images2017.cnblogs.com/blog/1131840/201712/1131840-20171215142449996-360033997.jpg
Tomcat的两个核心组件:Connector 和 Container

1.Connector组件

Connector组件将在某个指定的端口上侦听客户请求,接收浏览器发过来的tcp连接请求,创建一个Request和一个Response对象分别用于和其你去端交换数据,然后会产生一个线程来处理这个请求并把产生的Request和Response对象传给Engine,从Engine中获得响应并返回给客户端。

Tomcat有两个经典的Connector,一个直接侦听来自浏览器的HTTP请求,另外一个侦听来自其他的WebServer的请求。Cotote HTTP/1.1 Connector在端口8080处侦听来自客户浏览器的HTTP请求,Coyote JK2 Connector在端口8009处侦听其他WebServer的Servlet/JSP请求。 Connector 最重要的功能就是接收连接请求然后分配线程让 Container来处理这个请求,所以这必然是多线程的,多线程的处理是 Connector 设计的核心

2.Container组件

Container组件的体系结构如下:
http://images2017.cnblogs.com/blog/1131840/201712/1131840-20171215142450871-601807957.jpg

Container

Container是容器的父接口,该容器的设计用的是典型的责任链的设计模式,它由四个自容器组件构成,分别是Engine、Host、Context、Wrapper。这四个组件是负责关系,存在包含关系。通常一个Servlet class对应一个Wrapper,如果有多个Servlet则定义多个Wrapper,如果有多个Wrapper就要定义一个更高的Container,如Context。 Context定义在父容器 Host 中,其中Host 不是必须的,但是要运行 war 程序,就必须要 Host,因为 war 中必有 web.xml 文件,这个文件的解析就需要 Host 了,如果要有多个 Host 就要定义一个 top 容器 Engine 了。而 Engine 没有父容器了,一个 Engine 代表一个完整的 Servlet 引擎。

Engine

Engine 容器比较简单,它只定义了一些基本的关联关系 Host 容器

Host

Host 是 Engine 的字容器,一个 Host 在 Engine 中代表一个虚拟主机,这个虚拟主机的作用就是运行多个应用,它负责安装和展开这些应用,并且标识这个应用以便能够区分它们。它的子容器通常是 Context,它除了关联子容器外,还有就是保存一个主机应该有的信息。

Context

Context 代表 Servlet 的 Context,它具备了 Servlet 运行的基本环境,理论上只要有 Context 就能运行 Servlet 了。简单的 Tomcat 可以没有 Engine 和 Host。Context 最重要的功能就是管理它里面的 Servlet 实例,Servlet 实例在 Context 中是以 Wrapper 出现的,还有一点就是 Context 如何才能找到正确的 Servlet 来执行它呢? Tomcat5 以前是通过一个 Mapper 类来管理的,Tomcat5 以后这个功能被移到了 request 中,在前面的时序图中就可以发现获取子容器都是通过 request 来分配的

Wrapper

Wrapper 代表一个 Servlet,它负责管理一个 Servlet,包括的 Servlet 的装载、初始化、执行以及资源回收。Wrapper 是最底层的容器,它没有子容器了,所以调用它的 addChild 将会报错。 Wrapper 的实现类是 StandardWrapper,StandardWrapper 还实现了拥有一个 Servlet 初始化信息的 ServletConfig,由此看出 StandardWrapper 将直接和 Servlet 的各种信息打交道。
说明:除了上述组件外,Tomcat中还有其他重要的组件,如安全组件security、logger日志组件、session、mbeans、naming等其他组件。这些组件共同为Connector和Container提供必要的服务。

完整请求过程如下:

http://images2017.cnblogs.com/blog/1131840/201712/1131840-20171215142451699-1297214931.jpg

1.用户在浏览器中输入网址localhost:8080/test/index.jsp,请求被发送到本机端口8080,被在那里监听的Coyote HTTP/1.1 Connector获得;

2.Connector把该请求交给它所在的Service的Engine(Container)来处理,并等待Engine的回应;

3.Engine获得请求localhost/test/index.jsp,匹配所有的虚拟主机Host;

4.Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机),名为localhost的Host获得请求/test/index.jsp,匹配它所拥有的所有Context。Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为“ ”的Context去处理);

5.path=“/test”的Context获得请求/index.jsp,在它的mapping table中寻找出对应的Servlet。Context匹配到URL Pattern为/*.jsp的Servlet,对应于JspServlet类;

6.构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet()或doPost(),执行业务逻辑、数据存储等;

7.Context把执行完之后的HttpServletResponse对象返回给Host;

8.Host把HttpServletResponse对象返回给Engine;

9.Engine把HttpServletResponse对象返回Connector;

10.Connector把HttpServletResponse对象返回给客户Browser。

7、笔试算法

7.1、

7.2、

7.3、

7.4、让您做一个电商平台,您如何设置一个在买家下订单后的”第60秒“发短信通知卖家发货,您需要考虑的是 像淘宝一样的大并发量的订单。

1、具有排序功能的队列

2、Redis+定时器

3、队列,死信

参考地址:https://mp.weixin.qq.com/s/Dzv-i8n7waJVac-N7MJCvA