注意:以下是笔者整理的一些Java常见的面试题,并根据自己的理解作了如下回答,仅供参考
Java基础
什么对象可以作为HashMap的key
不可变的对象,即状态修改,不会引起hashCode和equals的变化的对象,换句话说就是严格遵守5条equals通用约定的对象。java集合类都依赖hashCode和equals方法,一个对象如果由于内部域变化导致hashCode不一致,在HashMap中前后产生的hash槽位可能不一致,即便一致,它后续判断前后hashCode是否一致,是否equals。这样子会引发很多混乱的问题,而且也为后期调试带来很大的麻烦。
HashMap, Hashtable, ConcurrentHashMap区别
HashMap:
- 非线程安全
- 默认容量16
- 负载因子0.75
- 懒初始化,当第一次插入数据,才初始化数组
- 每次扩容2倍
- 当链表长度超过8且数组长度超过64,链表转红黑树,否则扩容
- 当红黑树节点数小于6,则转为链表
- hash函数是对key的hash值的高16位和低16位做异或运算,这样做的原因是尽可能离散,减小hash的冲突率
Hashtable:
- 线程安全
- 默认容量11
- 负载因子0.75
- 每次扩容2倍
- 锁力度最粗,对整个Hashtable对象加synchronized锁
- 维护一个共享变量count
ConcurrentHashMap:
- 线程安全
- 其他特性和HashMap一样,懒初始化,初始化数组通过cas保证线程安全
- 对于节点的添加也是采用cas保证线程安全
- 锁力度细,对单个数组槽位加synchronized锁(锁分离)
- 当一个线程插入数据,需要扩容时,另外的线程要插入数据,会停下来帮助扩容。每次扩容步数是16,每个线程分别负责16个槽位
- size方法是通过counterCells和baseCount做sum运算得来。原理类似LongAdder,如果只维护一个变量,通过cas操作去保证线程安全,那在高并发下,竞争锁激烈会出现CPU大量空轮询
String, StringBuilder, StringBuffer区别
StringBuilder:
- 可变长度
- 默认16
- 非线程安全
StringBuffer:
- 可变长度
- 默认16
- 线程安全
双亲委派
ClassLoader采取双亲委派的设计模式,AppClassLoader->ExtClassLoader->BootstrapClassLoader按照这样的顺序去询问并加载类,若父类加载器加载不到,则子类加载器加载,
BootstrapClassLoader的加载目录是JAVA_HOME/jre/lib,可以查看System.getProperty("sun.boot.class.path")
ExtClassLoader的加载目录是 JAVA_HOME/jre/lib/ext;JAVA_HOME/lib/ext,可以查看System.getProperty("java.ext.dir")
AppClassLoader的加载目录 CLASSPATH,可以查看System.getProperty("java.class.path")
具体的系统参数可以参考这里
这么做的目的是防止有人恶意篡改jdk的核心代码并执行。
安全机制
反射原理
MethodAccessor有两个实现,一个是代理实现,一个是本地实现。默认Method会调用代理实现,而代理实现里面的被代理对象默认是NativeMethodAccessorImpl,在方法调用15次之后,被代理对象会变成动态生成字节码的实现,这个性能是本地实现的40-50倍左右,这里有两个参数可以设置-Dsun.reflect.noInflation, -Dsun.reflect.inflationThreshold,我们可以看到在ReflectionFactory里有读取并设置
动态代理
被代理类要实现InvokeHandler
hashcode里31*i,31这个魔数的原理
首先31是一个不大不小的质数,用31当计算因子的话,hash冲突率很小,这是通过实验得出的结论。还有现代的jvm都有对31*i做优化,优化成i«5-i,这样把乘法运算转化成移位运算和减法运算,执行效率也提高了。
并发
JUC常用的API
- 锁: AbstractQueuedSynchronizer, ReentrantReadWriteLock,
- 原子:AtomicInteger,AtomicLong,AtomicDouble,…,AtomicReference,LongAdder(高并发下吞吐量比AtomicLong要高得多),DoubleAdder
- 并发:Semaphore, CyclicBarrier, CountDownLatch, ConcurrentHashMap, CopyOnWriteArrayList, ThreadPoolExecutor, ForkJoinPool
LongAdder与AtomicLong的原理
CountDownLatch, Semaphore, CyclicBarrier 适用的场景
CountDownLatch可以用做并发器工具
Semaphore可以用做限流器
ThreadLocal原理,注意事项,参数传递,内存泄漏原因
每个Thread对象都有一个ThreadLocalMap,key是ThreadLocal的弱引用,为了避免使用者漏了调用Thread#remove(),而一般Thread对象的生命周期和整个web应用的声明周期是一样的,这样会导致内存泄漏。JVM在minor gc的时候会把key回收,这样子key就会为null,当再次调用ThreadLocal#get,ThreadLocal#set, ThreadLocal#remove会调用ThreadLocalMap#expungeStaleEntry清空ThreadLocalMap里所有key为null对应的value。
内置锁,显式锁,锁优化,锁消除,锁粗化,锁偏向,轻量级锁,重量级锁,偏向锁,自旋锁
内置锁(synchronized) 重量级锁,通过操作系统互斥量(mutex)来尝试竞争锁。
显式锁(ReentrantLock) 轻量级锁,通过自旋的方式来尝试竞争锁。可中断可超时
锁消除:JVM即时编译器在运行时,对一些代码进行逃逸分析,若堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以认为是线程私有的,同步加锁自然无须进行,比如如下代码。锁消除必须是server
模式,且开启逃逸分析(-XX:+DoEscapeAnalysis
jdk1.8默认是开启的),开启锁消除(-XX:+EliminateLocks
jdk1.8默认是开启的)
1
2
3
4
5
public String concat(String str1, String str2) {
StringBuffer sb = new StringBuffer(str1);
sb.append(str2);
return sb.toString();
}
锁粗化:把相邻的同步块合并到一块。避免更多的加锁解锁带来的性能损耗
锁偏向:当synchronized代码块同时只有一个线程访问的时候,jdk会消除同步操作,避免锁带来的性能开销。开启锁偏向(-XX:+UseBiasedLocking
jdk 1.8默认是开启的j)
偏向锁
jdk1.8默认开启偏向锁,线程在获取对象锁之前,先判断锁标记是否01
且是否偏向标志,如果是,判断对象的偏向线程ID是否是当前线程,如果是,直接进入同步代码块;如果不是,尝试竞争锁。如果竞争成功,则把偏向线程ID改为当前线程,并进入同步代码块;如果竞争失败,说明当前有多个线程竞争锁,此时进入安全点(Safe Point)
,消除偏向锁,把锁标志设置为00
,也就是轻量级锁。也就是锁升级,这个过程会JVM会STW
(Stop The World),会使吞吐量下降。
轻量级锁
重量级锁
自旋锁:jdk默认开启自旋锁,它通过自旋来竞争锁,这在占有锁的时间很短的情况下,性能是很高的,因为避免了线程阻塞,要知道java的线程阻塞,是需要调用系统的线程阻塞,这需要从用户态到内核态的切换,需要保留寄存器,变量值等等上下文。
关于锁分拆,锁分离可参考这里,锁分离的经典实现可以参考Jdk 1.8 的 ConcurrentHashMap。
对象头
markword
wait, sleep 分别是谁的方法,区别是什么,为什么wait, notify要放在同步块中
区别:
- wait是Object的方法,sleep是Thread的方法
- 最主要sleep方法不会释放锁,而wait方法会释放锁,使得其他线程可以使用同步控制块或者方法
- 使用范围上,wait, notify, notifyAll 只能在同步方法或者同步代码块内才能使用,而sleep可以在任意地方使用
- sleep必须捕获异常,而wait, notify, notifyAll 不需要捕获异常
wait 让当前线程等待,直到其他线程调用这个obj的notify或者notifyAll唤醒该线程,或者调用该线程的interrupt,(前提是该线程得先拿到obj锁),
sleep是让当前线程睡眠一段时间,加在线程上
wait, notify若没有放在同步块中,会抛出异常IllegalMonitorStateException
,至于为什么会出现这个问题,这就从一个多线程编程里面臭名昭著的Lost Wake-Up Problem
说起,这个问题是语言无关性的。Java为了避免这个问题引入这种机制,Java官网有此问题的介绍
Thread的6种状态
- NEW:
- RUNNABLE
- BLOCKED:
synchronized
- WAITING:
Object#wait
,Thread#join
,LockSupport#park
会导致线程处于这种状态 - TIMED_WAITING:
Object#wait(long)
,Thread#join(long)
,LockSupport#parkNanos
,LockSupport.parkUntil
会导致线程处于这种状态 - TERMINATED
线程池参数,整个流程描述,及其原理aqs, cas
线程池参数:
- 核心线程数
- 最大线程数
- 线程闲置时间
- 任务队列
- 拒绝策略
- 线程创建工厂(创建线程或线程池时请指定有意义的线程名称,方便出错时回溯)
web方向
servlet是否线程安全,如何改造
Servlet不是线程安全的,Servlet容器接收到request请求,会从线程池里取出一个线程,之后调用Servlet的service方法,要知道Servelt对象在容器中是单例,如果多个request请求同时调用,会有并发线程安全问题的。
改造:引入juc ConcurrentHashMap或者ThreadLocal
session与cookie区别,get和post区别,文件上传用post还是get
TCP三次握手,TCP四次挥手
TCP三次握手
TCP四次挥手
https四次握手
http请求行,请求头,请求体,响应行,响应头,响应体
请求(请求行+请求头+请求体):
- 请求行:Method
\s
URL\s
HttpVersion\n
(如GET https://www.google.com HTTP/1.1
) - 请求头:Key: Value;
\n
- 空行:
\r\n
- 请求体
响应(响应行+响应头+响应体):
- 响应行:HttpVersion
\s
StatusCode\s
ReasonPhrase\n
(如HTTP/1.1 200 OK
) - 响应头:Key: Value;
\n
- 空行:
\r\n
- 响应头
http请求报文结构和http响应报文结构
http请求报文结构
- 报文首部
- 请求行
- 请求头
- 请求首部字段
- 通用首部字段
- 实体首部字段
- 空行
- 报文主体
http响应报文结构
- 报文首部
- 状态行
- 响应头
- 响应首部字段
- 通用首部字段
- 实体首部字段
- 空行
- 报文主体
常用的首部字段
通用首部字段
首部字段名 | 首部说明 |
---|---|
Cache-Control | 控制缓存的行为 |
Connection | 逐跳首部、连接的管理 |
Date | 创建报文的日期时间 |
Pragma | 报文指令 |
Trailer | 报文末端的首部一览 |
Transfer-Encoding | 指定报文主体的传输编码方式 |
Upgrade | 升级为其他协议(如:websocket握手协议会用到) |
Via | 代理服务器的相关信息 |
Warning | 错误通知 |
请求首部字段
首部字段名 | 首部说明 |
---|---|
Accept | 用户代理可处理的媒体类型 |
Accept-Charset | 优先的字符集 |
Accept-Encoding | 优先的内容编码 |
Accept-Language | 优先的语言(自然语言) |
Authorization | Web认证信息 |
Except | 期待服务器的特定行为 |
From | 用户的电子邮箱地址 |
Host | 请求资源所在的服务器 |
If-Match | 比较实体标记(ETag) |
If-Modified-Since | 比较资源的更新时间 |
If-Node-Match | 比较实体标记(与If-Match相反) |
If-Range | 资源未更新时发送实体Byte的范围请求 |
If-Unmodified-Since | 比较资源的更新时间(与If-Modified-Since相反) |
Max-Forwards | 最大传输逐跳数 |
Proxy-Authorization | 代理服务器要求客户端的授权信息 |
Range | 实体的字节范围请求 |
Referer | 对请求中的URI的原始获取方 |
TE | 传输编码的优先级 |
User-Agent | HTTP客户端程序的信息 |
响应首部字段
首部字段名 | 首部说明 |
---|---|
Accept-Ranges | 是否接受字节范围请求 |
Age | 推算资源创建经过时间 |
ETag | 资源的匹配信息 |
Location | 令客户端重定向至指定URI |
Proxy-Authenticate | 代理服务器对客户端的认证信息 |
Retry-After | 对再次发起请求的时机要求 |
Server | HTTP服务器的安装信息 |
Vary | 代理服务器缓存的管理信息 |
WWW-Authenticate | 服务器对客户端的认证信息 |
实体首部字段
首部字段名 | 首部说明 |
---|---|
Allow | 资源可支持的HTTP方法 |
Content-Encoding | 实体主体适用的编码方式 |
Content-Language | 实体主体的自然语言 |
Content-Length | 实体主体的内容大小(单位:Byte) |
Content-Location | 替代对应资源的URI |
Content-MD5 | 实体主体的报文摘要 |
Content-Range | 实体主体的位置范围 |
Content-Type | 实体主体的媒体类型 |
Expires | 实体主体过期的日期时间 |
Last-Modified | 资源的最后修改日期时间 |
websocket握手协议
请求报文首部字段如下几项:
1
2
3
4
5
6
7
GET ws://example.com HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-Websocket-Key: JCKoHgCKVlst7Y6pBZUOqA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Protocol: chat
Sec-Websocket-Version: 13
响应报文首部字段如下几项:
1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 52jlsVmIIUrr/tdc/2sfCfBZMGA=
字段含义,可详见RFC6455:
- Sec-Websocket-Key:是客户端随机的一个16字节长度的值,通过base64编码。这个是从客户端发送到服务端,用来让服务端证明它收到的信息是有效的握手协议。
- Sec-Websocket-Version:客户端支持的websocket版本。如果服务端不支持这个版本,服务端必须中断握手,并且响应适当的错误码(如:
426 Upgrade Required
)。 - Sec-WebSocket-Protocol:应用级别的子协议。
- Sec-WebSocket-Extensions:服务端协议级的扩展。
- Sec-WebSocket-Accept:根据客户端传上来的Sec-Websocket-Key计算得来。服务端会先在Key值后面拼接上一个常量字符串”258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后根据
SHA-1
散列算法计算出20字节长度的值,然后再用base64编码
成功握手确立WebSocket连接之后,通信时不再使用HTTP的数据帧,而采用WebSocket独立的数据帧。
session的存储
session存储在服务端内存
tomcat 存放在ManagerBase
如何防止表单重复提交
JVM
JMM
Java常用工具
jps, jinfo, jmap, jstack, jstat, jhat
JVM GC
垃圾回收器
Serial
ParNew
Parallel scavenge
CMS
Serial Old
Parallel Old
G1
垃圾回收算法
复制回收算法
- 优点:快速
- 缺点:浪费50%的内存利用率
标记清除算法
- 优点:相对快速
- 缺点:会产生内存碎片
标记整理算法
- 优点:几乎没有内存碎片
- 缺点:相对慢
什么时候有可能会触发Full GC
- Perm空间不足
- CMS GC时出现Promition Failure 和 Concurrent Mode Failure(concurrent mode failure发生的原因一般是CMS正在进行,但是由于老年代空间不足,这时候JVM会STW,同时会终止CMS,并进行
- Serial Old GC)的时候会触发Full GC
- 当JVM统计每次从新生代晋升到老年代的平均对象大小大于当前老年代剩余空间时会触发Full GC
- 手动触发,当用户执行jmap hive时会触发Full GC
强引用,软引用,弱引用,虚引用
happen-before
数据库
悲观锁,乐观锁
解释数据库事务及特性
隔离级别
- RU(Read Uncommit)
- RC (Read Commit)
- RR (Read Repeat)
- Serialized
redo.log, undo.log
binlog主从复制
MVCC, Next-Key Lock
Spring
使用Spring给我们带来的好处
BeanFactory 与 FactoryBean
BeanFactory 与 ApplicationContext区别
BeanDefinition 与 BeanWrapper
Spring Bean生命周期
Spring Bean 各作用域
Spring Bean是否线程安全
Spring内部生成一个BeanWrapper持有原生的Bean,所以Bean是否线程安全跟Spring没有关系,只跟Bean的实现者也就是开发人员有关。
IOC和AOP原理
IOC初始化流程
定位
加载
注册
springboot的流程
springboot和springcloud相关组件
Spring设计模式
分布式
谈谈CAP与BASE
ZooKeeper满足了CAP中的哪些特性
ZooKeeper是属于CP,当集群中Leader出故障,故障恢复的过程中,整个ZooKeeper集群是不可用的,直到选出一个Leader来