My Profile Photo

庄津津的技术博客


众里寻他千百度,蓦然回首,那人却在灯火阑珊处


Java常见面试题整理


注意:以下是笔者整理的一些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三次握手流程

TCP四次挥手

TCP四次挥手流程

https四次握手

Https握手时序图

http请求行,请求头,请求体,响应行,响应头,响应体

请求(请求行+请求头+请求体):

  • 请求行:Method\sURL\sHttpVersion\n (如 GET https://www.google.com HTTP/1.1
  • 请求头:Key: Value;\n
  • 空行:\r\n
  • 请求体

响应(响应行+响应头+响应体):

  • 响应行:HttpVersion\sStatusCode\sReasonPhrase\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

  1. Perm空间不足
  2. CMS GC时出现Promition Failure 和 Concurrent Mode Failure(concurrent mode failure发生的原因一般是CMS正在进行,但是由于老年代空间不足,这时候JVM会STW,同时会终止CMS,并进行
  3. Serial Old GC)的时候会触发Full GC
  4. 当JVM统计每次从新生代晋升到老年代的平均对象大小大于当前老年代剩余空间时会触发Full GC
  5. 手动触发,当用户执行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来

缓存穿透怎么解决

redis的io模型

如何保证redis高可用

redis是单线程还是多线程

线上CPU占用过高怎么排查

一致性hash

分库分表

RPC