米哈游Java面试

137 阅读8分钟

一面

1、Java里map的一个实现?

以hashmap为例,实现了Map接口,但允许使用null键和null值,但null键只能用一次,null值无限制。hashmap是基于hash表实现,提供了O(1)的性能来插入和检索元素,但是hashmap不保证元素的顺序。插入和检索数据的步骤:计算hashcode值,判断当前位置是否存在数据,不存在直接插入;存在的话,用equals( )方法判断key值是否相同,相同则更新value值,不同则插入尾插法插入key-value。检索同理。

2、hashmap是线程安全的吗?如果并发去读写这个hashmap会发生什么?

hashmap不是线程安全的;

hashmap并发去读写会导致:1、多线程的put可能导致元素的丢失;(多线程同时插入,会导致数据丢失)2、put和get并法时,可能导致get为null(hash容量大于阈值,进行扩容,刚创建出新的hash表,被其他线程访问到);3、JDK7中hashmap并发put会造成循环链表,导致get时出现死循环(JDK7用的头插法,JDK8之后采用尾插法)。

3、Concurrent HashMap是怎么解决并发读写问题?

在JDK1.7中,Concurrent HashMap采用了分段锁策略,将一个HashMap分割成Segment数组,其中Segment可以看成一个HashMap,不同的是Segment继承自ReentrantLock,在操作时给Segment赋予一个对象锁,从而保证多线程环境下并发操作安全。

在JDK1.8中,Concurrent HashMap并没有采用分段锁策略,而是在元素的节点上采用CAS + synchronized操作来保证并发的安全性。

4、JDK1.8之后,Concurrent HashMap源码中node数组中自旋的方式?

当新节点需要被添加到node数组的某个位置时,会使用CAS来确保当前位置的值为空,然后原子地将新节点设置为该位置的值。

如果node数组在该位置上有值,则用synchronized进行加锁。

5、CAS概念?

CAS是一种乐观锁的实现方式,全称:比较与交换,是一种无锁的原子操作。通过三个值:要更新的变量、预期值(旧值)、新值。

存在的问题:

ABA问题,通过版本控制/加时间戳;

长时间自旋问题,让JVM支持处理器提供的pause命令,让自旋失败时CPU睡眠一小段时间再继续自旋。

多个共享变量的原子操作,使用锁。

6、CAS怎么保证它的原子性?CAS失败的话会如何自旋?

CAS保证原子性是通过底层CPU硬件指令实现的。Linux 的 X86 下主要是通过cmpxchgl (Compare and Exchange)这个指令在 CPU 上完成 CAS 操作的,但在多处理器情况下,必须使用lock指令加锁来完成。Java中有Unsafe类有几个支持CAS的方法,如:CASobject()、CASint()、CASlong()。

自旋意味着当一个线程尝试执行CAS操作失败时(即发现其他线程已修改了目标值),它不会立即放弃或阻塞等待,而是会不断重试该操作,期待在下一次尝试时能够成功。

7、锁的实现有哪几种方式?

乐观锁/悲观锁、独享锁/共享锁、读锁/写锁、可重入锁、分布式锁、自旋锁、公平锁/非公平锁、可中断锁/不可中断锁、分段锁、锁升级、无锁、偏向锁、轻量级锁/重量级锁、锁优化。

8、可重入锁的概念?

指在同一个线程在调外层方法获取锁的时候,再进入内层方法会自动获取锁。对象锁或类锁内部有计数器,一个线程每获得一次锁,计数器 +1;解锁时,计数器 -1。

Java 中的 ReentrantLock 和 synchronized 都是 可重入锁。可重入锁的一个好处是可一定程度避免死锁。

9、什么是乐观锁?

总是默认多线程中,访问的数据不会被修改,不存在数据不一致等问题。所以不会上锁,只有在提交更新时,才会正式对数据的冲突与否进行检测。分为三个阶段:数据读取、写入校验、数据写入。

10、redis分布式锁具体如何实现?

(1)普通实现

  setnx+lua,或者知道set key value px milliseconds nx

  三个要点:

  1. set命令要用set key value px milliseconds nx
  2. value要具有唯一性;
  3. 释放锁时要验证value值,不能误解锁;

  事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

  1. 在Redis的master节点上拿到了锁;
  2. 但是这个加锁的key还没有同步到slave节点;
  3. master故障,发生故障转移,slave节点升级为master节点;
  4. 导致锁丢失。

  (2)高级实现:

   Redlock

  基本思路:让客户端与多个独立的 Redis 节点(这些节点完全互相独立,不存在主从复制或者其他集群协调机制)并行请求申请加锁,如果能在半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

11、实现redis锁时,加锁和解锁是怎么操作?有什么注意事项?边界值情况?

加锁:set key value px milliseconds nx

解锁:delete key

注意事项:

  • 确保锁的键是唯一的,可以使用UUID或其他唯一标识符作为值。
  • 设置合适的过期时间,避免死锁。
  • 在解锁时,需要确保只有持有锁的线程才能解锁,否则可能导致其他线程误删锁。

边界值情况:

  • 如果锁的过期时间设置得太短,可能会导致锁被频繁地失效和重新获取,影响性能。
  • 如果锁的过期时间设置得太长,可能会导致死锁的风险增加。
  • 在高并发场景下,可能会出现多个线程同时尝试获取锁的情况,需要处理竞争条件。

12、redis缓存经典问题,解决办法?

(1) 缓存穿透 ——指查询一个数据库中也不存在的数据,这样的请求不会被缓存,每次请求都会穿透到数据库,增加数据库的压力,且对这类请求缓存没有任何帮助。

解决办法:

  1. 布隆过滤器,在请求到达数据库之前,先通过布隆过滤器判断该数据是否存在。
  2. 缓存空对象

(2)缓存击穿——指某个热点key在缓存中刚好失效,而此时有大量的请求并发访问这个key,导致所有请求都直接访问数据库,形成瞬时高峰。

解决办法:

  1. 互斥锁
  2. 热点数据永不过期
  3. 逻辑过期,缓存数据和过期时间,数据过期不直接删除,而是标记为过期,下次访问时,返回旧值,并到数据库加载数据,更新缓存。

(3)缓存雪崩——当大量缓存在同一时间内集中失效,导致所有请求直接打到数据库上,引起数据库压力激增,可能导致服务不可用,称为缓存雪崩。

解决办法:

  1. 缓存过期时间随机化
  2. 引入限流和熔断机制
  3. 使用缓存高可用集群
  4. 缓存预热

13、布隆过滤器

实现原理:一个超大位数的数组和多个不同Hash算法函数,数组中存放bit位(0/1)。作用是:能够确定查找的数据一定不存在/可能存在。

14、如何建立索引?

15、Mysql的事物隔离级别有哪些?分别解决了什么问题?

  1. 读未提交
  2. 读已提交 解决了脏读
  3. 可重读读 解决了脏读、不可重复读
  4. 序列化 解决了脏读、不可重复读、幻读

16、解释幻读?和不可重复读的区别?

幻读:先后查询同一数据,多出很多来。

不可重复读:先后查询同一数据,获得结果不一致。

17、TCP建立/断开连接,三次握手、四次挥手?

三次握手:

  1. 客户端发送SYN包,进入SYN_SENT状态
  2. 服务端回应ACK包,并向客户端发送SYN包
  3. 客户端回应ACK,且双方进入established状态

  四次挥手:(假设客户端主动要求断开连接)

  1. 客户端发送FIN包
  2. 服务端回应ACK包,此时服务端仍可能在发送数据
  3. 服务端发送FIN包
  4. 客户端回应ACK

18、TCP和UDP的区别?

TCP:面向连接、提供可靠的服务、面向字节流、连接时点到点的、首部开销20字节;

UDP:无连接、不可靠、面向报文、支持一对一,一对多,多对多通信、首部开销8字节;

19、TCP如何保证可靠性传输?

信道可靠(三次握手、四次挥手)

数据正确(分区编号、校验和、超时重传)

传输控制(拥塞控制、差错控制、滑动窗口)