一面
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
三个要点:
- set命令要用
set key value px milliseconds nx; - value要具有唯一性;
- 释放锁时要验证value值,不能误解锁;
事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
- 在Redis的master节点上拿到了锁;
- 但是这个加锁的key还没有同步到slave节点;
- master故障,发生故障转移,slave节点升级为master节点;
- 导致锁丢失。
(2)高级实现:
Redlock
基本思路:让客户端与多个独立的 Redis 节点(这些节点完全互相独立,不存在主从复制或者其他集群协调机制)并行请求申请加锁,如果能在半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
11、实现redis锁时,加锁和解锁是怎么操作?有什么注意事项?边界值情况?
加锁:set key value px milliseconds nx
解锁:delete key
注意事项:
- 确保锁的键是唯一的,可以使用UUID或其他唯一标识符作为值。
- 设置合适的过期时间,避免死锁。
- 在解锁时,需要确保只有持有锁的线程才能解锁,否则可能导致其他线程误删锁。
边界值情况:
- 如果锁的过期时间设置得太短,可能会导致锁被频繁地失效和重新获取,影响性能。
- 如果锁的过期时间设置得太长,可能会导致死锁的风险增加。
- 在高并发场景下,可能会出现多个线程同时尝试获取锁的情况,需要处理竞争条件。
12、redis缓存经典问题,解决办法?
(1) 缓存穿透 ——指查询一个数据库中也不存在的数据,这样的请求不会被缓存,每次请求都会穿透到数据库,增加数据库的压力,且对这类请求缓存没有任何帮助。
解决办法:
- 布隆过滤器,在请求到达数据库之前,先通过布隆过滤器判断该数据是否存在。
- 缓存空对象
(2)缓存击穿——指某个热点key在缓存中刚好失效,而此时有大量的请求并发访问这个key,导致所有请求都直接访问数据库,形成瞬时高峰。
解决办法:
- 互斥锁
- 热点数据永不过期
- 逻辑过期,缓存数据和过期时间,数据过期不直接删除,而是标记为过期,下次访问时,返回旧值,并到数据库加载数据,更新缓存。
(3)缓存雪崩——当大量缓存在同一时间内集中失效,导致所有请求直接打到数据库上,引起数据库压力激增,可能导致服务不可用,称为缓存雪崩。
解决办法:
- 缓存过期时间随机化
- 引入限流和熔断机制
- 使用缓存高可用集群
- 缓存预热
13、布隆过滤器
实现原理:一个超大位数的数组和多个不同Hash算法函数,数组中存放bit位(0/1)。作用是:能够确定查找的数据一定不存在/可能存在。
14、如何建立索引?
15、Mysql的事物隔离级别有哪些?分别解决了什么问题?
- 读未提交
- 读已提交 解决了脏读
- 可重读读 解决了脏读、不可重复读
- 序列化 解决了脏读、不可重复读、幻读
16、解释幻读?和不可重复读的区别?
幻读:先后查询同一数据,多出很多来。
不可重复读:先后查询同一数据,获得结果不一致。
17、TCP建立/断开连接,三次握手、四次挥手?
三次握手:
- 客户端发送SYN包,进入SYN_SENT状态
- 服务端回应ACK包,并向客户端发送SYN包
- 客户端回应ACK,且双方进入established状态
四次挥手:(假设客户端主动要求断开连接)
- 客户端发送FIN包
- 服务端回应ACK包,此时服务端仍可能在发送数据
- 服务端发送FIN包
- 客户端回应ACK
18、TCP和UDP的区别?
TCP:面向连接、提供可靠的服务、面向字节流、连接时点到点的、首部开销20字节;
UDP:无连接、不可靠、面向报文、支持一对一,一对多,多对多通信、首部开销8字节;
19、TCP如何保证可靠性传输?
信道可靠(三次握手、四次挥手)
数据正确(分区编号、校验和、超时重传)
传输控制(拥塞控制、差错控制、滑动窗口)