2月24日面,开始自我介绍+项目,后续开始八股
1. HashMap的原理?
HashMap
是基于哈希表实现的,它存储键值对(Key-Value
),并且通过哈希函数将键映射到存储位置上。以下是其核心原理:
(1) 哈希表的基本结构
-
数组 + 链表/红黑树:
HashMap
内部使用一个数组(Node[] table
)来存储数据。每个数组元素是一个桶(bucket
),桶中存储的是链表或红黑树。 -
节点(Node):每个节点存储键值对(
key-value
),以及指向下一个节点的指针(用于链表结构)。 -
哈希函数:通过
key
的hashCode()
方法获取哈希值,然后通过哈希值与数组长度进行取模操作(hash % table.length
),确定键值对存储在数组的哪个位置。
(2) 数据存储流程
-
计算哈希值:通过
key.hashCode()
获取哈希值,然后通过扰动函数(hash()
方法)进一步优化哈希值的分布。 -
确定存储位置:通过哈希值对数组长度取模,确定键值对存储在数组的哪个桶中。
-
处理冲突:
-
如果桶为空,直接插入节点。
-
如果桶中已经有数据,且键相同,则覆盖旧值。
-
如果键不同,则将新节点插入到链表尾部。
-
如果链表长度超过阈值(默认是 8),则将链表转换为红黑树,以优化查找效率。
-
(3) 数据查找流程
-
计算哈希值并确定桶的位置。
-
遍历链表或红黑树,通过
equals()
方法比较键是否相等。 -
如果找到匹配的键,则返回对应的值。
2. HashMap扩容机制?
扩容是 HashMap
在存储数据时,当数组的负载因子(load factor
)达到一定阈值时,自动调整数组大小的过程。扩容机制如下:
(1) 负载因子(Load Factor)
-
负载因子 =
当前存储的键值对数量 / 数组长度
。 -
默认负载因子为 0.75,表示当数组中存储的键值对数量达到数组长度的 75% 时,触发扩容。
(2) 扩容过程
-
判断是否需要扩容:在插入数据时,如果当前键值对数量超过
数组长度 × 负载因子
,则触发扩容。 -
创建新数组:扩容时,数组长度会扩大为原来的两倍(例如,从 16 扩大到 32)。
-
重新哈希:将原数组中的所有键值对重新计算哈希值,并重新分配到新数组的桶中。
-
更新引用:将新数组替换为原数组。
(3) 扩容的代价
-
扩容是一个相对耗时的操作,因为它需要重新计算哈希值并重新分配数据。
-
为了减少扩容的频率,建议在创建
HashMap
时,根据预期存储的键值对数量,合理设置初始容量。
3. HashMap是线程安全的吗?
HashMap
是非线程安全的。在多线程环境下,可能会出现以下问题:
-
数据丢失:多个线程同时插入数据时,可能会覆盖彼此的数据。
-
死循环:在扩容过程中,链表的结构可能会被破坏,导致死循环。
-
并发修改异常:多个线程同时修改数据时,可能会抛出
ConcurrentModificationException
。
4. 如何实现线程安全?
(1) 使用 Collections.synchronizedMap()
-
可以通过
Collections.synchronizedMap()
方法将HashMap
包装为线程安全的Map
。 -
例如:
java复制
Map<String, Integer> map = Collections.synchronizedMap(new HashMap<>());
-
缺点:虽然包装后线程安全,但每次操作都需要同步整个
Map
,性能较低。
(2) 使用 ConcurrentHashMap
-
ConcurrentHashMap
是线程安全的哈希表实现,适用于高并发场景。 -
它通过分段锁或CAS操作实现线程安全,性能比
Collections.synchronizedMap()
更高。 -
原理:
-
使用多个锁(分段锁)保护不同的数据段,减少锁竞争。
-
在 Java 8 及以后,
ConcurrentHashMap
使用CAS操作和synchronized结合,进一步优化性能。 -
支持全并发的检索操作和适当数量的并发更新操作。
-
(3) 使用 ReentrantLock
手动同步
-
如果需要对
HashMap
进行复杂的线程安全操作,可以手动使用ReentrantLock
或其他锁机制保护数据。 -
例如:
java复制
ReentrantLock lock = new ReentrantLock(); HashMap<String, Integer> map = new HashMap<>(); public void put(String key, Integer value) { lock.lock(); try { map.put(key, value); } finally { lock.unlock(); } } public Integer get(String key) { lock.lock(); try { return map.get(key); } finally { lock.unlock(); } }
-
缺点:需要手动管理锁,代码复杂度较高。
5. Redis有什么数据结构
1. 字符串(String)
字符串是 Redis 最基本的数据类型,键值对中的值可以是字符串形式的任意数据,如文本、数字、JSON 等。
特点:
-
存储简单数据:可以存储简单的文本、数字等。
-
原子操作:支持原子操作,例如对数字进行自增(
INCR
)或自减(DECR
)。 -
过期时间:可以为字符串设置过期时间(TTL),使其在一定时间后自动删除。
2. 列表(List)
列表是一个有序的字符串集合,支持从头部(left)或尾部(right)插入和删除元素。
特点:
-
先进先出(FIFO)或后进先出(LIFO):可以作为队列或栈使用。
-
支持范围操作:可以获取列表的子集。
-
原子操作:插入和删除操作是原子的。
3. 集合(Set)
集合是一个无序的字符串集合,每个元素是唯一的(不允许重复)。
特点:
-
无序性:集合中的元素没有顺序。
-
唯一性:集合中的元素是唯一的。
-
支持集合操作:可以进行交集、并集、差集等操作。
4. 有序集合(Sorted Set,ZSet)
有序集合是一个带有分数(score)的字符串集合,元素按照分数从小到大排序。
特点:
-
有序性:元素根据分数自动排序。
-
唯一性:集合中的元素是唯一的,但分数可以重复。
-
支持范围操作:可以获取指定分数范围内的元素。
5. 哈希表(Hash)
哈希表是一个键值对集合,每个键对应一个字段(field)和值(value)。
特点:
-
结构化存储:适合存储对象的属性。
-
高效访问:可以通过字段名快速访问字段的值。
6. Java中有什么锁?
在Java中,锁(Lock)是用于控制多个线程对共享资源访问的一种机制,主要用于保证线程安全和数据一致性。Java提供了多种锁机制,可以大致分为以下几类:
1.内置锁(Synchronized Lock)
这是Java中最基本的锁机制,也称为“监视器锁”或“互斥锁”。它基于Java对象的监视器机制,通过关键字synchronized
实现。
特点:
-
互斥性:同一时间只有一个线程可以持有某个对象的锁。
-
可重入性:同一个线程可以多次获取同一个锁。
-
自动释放:锁会在同步代码块或方法执行完毕后自动释放。
使用方式:
-
同步方法:
public synchronized void synchronizedMethod() { // 需要同步的代码 }
这里锁的是当前实例对象
this
。 -
静态同步方法:
public static synchronized void staticSynchronizedMethod() { // 需要同步的代码 }
这里锁的是当前类的
Class
对象。 -
同步代码块:
synchronized (this) { // 锁当前实例对象 // 需要同步的代码 } synchronized (MyClass.class) { // 锁当前类的Class对象 // 需要同步的代码 } synchronized (obj) { // 锁任意对象 // 需要同步的代码 }
2.显式锁(Explicit Locks)
显式锁是通过java.util.concurrent.locks
包中的锁接口和类实现的,提供了比内置锁更灵活的锁操作。
-
ReentrantLock(可重入锁)
-
特点:
-
支持可重入。
-
提供了比
synchronized
更灵活的锁操作,例如尝试加锁(tryLock
)、设置锁的等待时间(tryLock(long timeout, TimeUnit unit)
)。 -
支持公平锁和非公平锁(默认是非公平锁)。
-
可以中断线程的等待。
-
-
使用示例:
import java.util.concurrent.locks.ReentrantLock; public class Example { private final ReentrantLock lock = new ReentrantLock(); public void method() { lock.lock(); // 加锁 try { // 需要同步的代码 } finally { lock.unlock(); // 释放锁 } } }
-
-
ReentrantReadWriteLock(可重入读写锁)
-
特点:
-
提供了读锁和写锁的分离,允许多个线程同时读取,但写操作是互斥的。
-
提高了读多写少场景下的性能。
-
-
使用示例:
import java.util.concurrent.locks.ReentrantReadWriteLock; public class Example { private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); public void read() { readLock.lock(); try { // 读操作 } finally { readLock.unlock(); } } public void write() { writeLock.lock(); try { // 写操作 } finally { writeLock.unlock(); } } }
-
-
StampedLock(时间戳锁)
-
特点:
-
提供了乐观锁和悲观锁的结合,适合高并发读操作的场景。
-
支持读锁、写锁和乐观读操作。
-
-
使用示例:
import java.util.concurrent.locks.StampedLock; public class Example { private final StampedLock stampedLock = new StampedLock(); public void read() { long stamp = stampedLock.tryOptimisticRead(); // 尝试乐观读 // 执行读操作 if (!stampedLock.validate(stamp)) { // 检查是否被写操作破坏 stamp = stampedLock.readLock(); // 升级为悲观读锁 try { // 重新执行读操作 } finally { stampedLock.unlockRead(stamp); } } } public void write() { long stamp = stampedLock.writeLock(); try { // 写操作 } finally { stampedLock.unlockWrite(stamp); } } }
-
3.其他锁机制
-
自旋锁:自旋锁是一种基于忙等待的锁机制,线程不会进入阻塞状态,而是不断尝试获取锁。Java的
ReentrantLock
在某些情况下会使用自旋锁(通过LockSupport.park
和LockSupport.unpark
实现)。自旋锁适用于锁持有时间非常短的场景。 -
锁优化:JVM在某些情况下会将多个连续的锁操作合并为一个锁操作,以减少锁的开销。
-
偏向锁:偏向锁是一种优化机制,允许线程在没有竞争的情况下快速获取锁。
-
轻量级锁:轻量级锁是一种在锁竞争较少时的优化机制,通过CAS操作(Compare-And-Swap)实现锁的获取和释放。