请解释 Java 中集合框架的层次结构,并举例说明 ArrayList 和 LinkedList 的区别及其适用场景
一、 Java 集合框架的层次结构
Java 集合框架主要由两大根接口组成:Collection 和 Map。
- Collection 接口:存储对象的集合。
- List (有序、可重复):
- ArrayList:基于动态数组实现。
- LinkedList:基于双向链表实现。
- Vector:线程安全(古老,不推荐使用)。
- Set (无序、唯一):
- HashSet:基于 HashMap 实现,速度快。
- TreeSet:支持自然排序或定制排序。
- LinkedHashSet:维护插入顺序。
- Queue (队列):
- PriorityQueue:优先级队列。
- Deque:双端队列(LinkedList 也实现了该接口)。
- List (有序、可重复):
- Map 接口:存储键值对(Key-Value)。
- HashMap:最常用,高效。
- TreeMap:按 Key 排序。
- ConcurrentHashMap:线程安全,常用于高并发场景。
二、 ArrayList 与 LinkedList 的深度对比
这两者都实现了 List 接口,但在底层数据结构和性能表现上有本质区别:
| 特性 | ArrayList | LinkedList |
| 底层结构 | 动态数组(连续内存空间) | 双向链表(非连续内存空间) |
| 随机访问 (get/set) | 极快 (O(1)O(1)O(1)):通过下标直接定位。 |
慢 (O(n)O(n)O(n)):需要从头或尾遍历。 |
| 插入/删除 (add/remove) | 较慢 (O(n)O(n)O(n)):涉及数组扩容和元素移动。 |
快 (O(1)O(1)O(1)):仅需修改指针(若已定位到位置)。 |
| 内存占用 | 较小:主要浪费在预留的末尾空间。 | 较大:每个节点都要存储前驱和后继指针。 |
| 线程安全 | 线程不安全 | 线程不安全 |
什么是 Volatile 关键字?它在多线程环境中的作用是什么?与 Synchronized 的区别是什么?
一、 什么是 Volatile 关键字?
volatile 是 Java 提供的一种轻量级的同步机制。它的核心作用是确保变量的修改对所有线程都是可见的,并防止指令重排序。
在 Java 内存模型(JMM)中,每个线程有自己的工作内存(私有缓存),线程操作变量时会先从主内存拷贝一份。如果一个变量没被 volatile 修饰,线程修改完后可能不会立即写回主内存,导致其他线程读到的是旧值。
二、 Volatile 在多线程中的两大作用
保证内存可见性 (Visibility):
- 一旦一个变量被声明为 volatile,任何线程对它的修改都会立即刷新到主内存中;同时,其他线程在读取该变量时,会强制从主内存中读取最新值,而不是从自己的工作内存。
禁止指令重排序 (Ordering):
- 为了优化性能,编译器和处理器可能会对代码指令进行乱序执行。volatile 通过插入“内存屏障(Memory Barrier)”来防止这种重排序。
- 典型应用: 单例模式中的 DCL(双重检查锁)。如果不加 volatile,在对象初始化时,由于重排序,另一个线程可能会拿到一个“尚未完全初始化完成”的对象引用。
三、 Volatile 与 Synchronized 的区别
虽然两者都用于并发编程,但它们有本质的不同:
| 特性 | Volatile | Synchronized |
| 性质 | 变量修饰符,轻量级。 | 关键字,可修饰方法或代码块,重量级。 |
| 原子性 | 不保证原子性(如 i++ 操作无法保证正确)。 | 保证原子性(同一时间只有一个线程执行)。 |
| 可见性 | 保证可见性。 | 保证可见性(解锁前必须刷新回主内存)。 |
| 阻塞 | 不会造成线程阻塞,性能高。 | 会造成线程阻塞,涉及上下文切换,性能相对较低。 |
| 有序性 | 保证有序性(禁止重排序)。 | 保证有序性(通过互斥保证单线程执行)。 |
四、总结
在实际开发中(如我简历中提到的广告引擎维护):
- 使用 volatile 的场景: 当你需要一个简单的“状态标记位”(如 isReady, isShutdown),且该变量的操作不依赖当前值(即不需要 i++ 这种操作)时,volatile 是性能最优的选择。
- 使用 synchronized 的场景: 当你需要执行复杂的复合操作,或者需要保证操作的“原子性”时(例如在“问卷系统”中扣减优惠券额度、更新计数器),必须使用 synchronized 或 ReentrantLock。
总结: volatile 解决了“看得到”的问题,而 synchronized 解决了“写得准”的问题。在追求高并发性能时,我会优先考虑 volatile 或 Atomic 原子类,只有在必要时才使用较重的锁机制。
描述 ReentrantLock 的实现原理,以及它与 Synchronized 的比较(包括公平锁和非公平锁)
一、 ReentrantLock 的实现原理
ReentrantLock 的核心是基于 AQS(AbstractQueuedSynchronizer,抽象队列同步器) 实现的。
状态管理(state):AQS 内部维护一个 volatile 修饰的 state 变量。
- 当 state = 0 时,表示锁未被占用。
- 线程尝试通过 CAS(Compare-And-Swap) 操作将 state 从 0 修改为 1 来获取锁。
重入性实现:
- 如果获取锁的线程是“当前占有锁的线程”,则 state 加 1,这就是“可重入”的体现。
- 释放锁时,state 减 1,直到 state = 0 时锁才完全释放。
等待队列(CLH 变体):
- 如果 CAS 失败(锁已被占用),线程会被封装成一个 Node 节点,进入一个双向同步队列中挂起(LockSupport.park)。
- 当持有锁的线程释放锁后,会唤醒队列中头节点的下一个节点尝试获取锁。
二、 ReentrantLock vs Synchronized
| 特性 | Synchronized | ReentrantLock |
| 实现层面 | JVM 层面(关键字),由 C++ 实现。 | JDK 层面(API),纯 Java 编写。 |
| 锁的释放 | 代码执行完毕或异常时自动释放。 | 必须在 finally 块中手动释放,否则易死锁。 |
| 灵活性 | 较低。不可中断,不支持超时。 | 较高。支持 tryLock()、lockInterruptibly()。 |
| 条件变量 | 只有 1 个(wait/notify)。 | 支持多个 Condition(精确唤醒某些线程)。 |
| 公平性 | 只支持非公平锁。 | 支持公平锁和非公平锁(默认)。 |
三、 公平锁与非公平锁
这是 ReentrantLock 的一个重要特性:
公平锁 (Fair Lock):
- 机制:严格按照线程等待的先后顺序(FIFO)获取锁。
- 优点:不会产生“线程饥饿”现象。
- 缺点:吞吐量低,因为频繁的线程唤醒和上下文切换开销大。
非公平锁 (Non-fair Lock) - 默认:
- 机制:线程尝试获取锁时,不检查队列,直接尝试 CAS 抢占。抢不到再进队列。
- 优点:吞吐量高。如果一个线程刚释放锁,另一个线程正好来申请,它可以直接使用,省去了唤醒队列线程的时间。
四、 实际项目中的应用场景
使用 synchronized 的场景:如果是简单的线程同步(如对一个计数器自增),我优先选 synchronized。因为 Java 1.6 后引入了偏向锁、轻量级锁,在低竞争下性能极佳,且代码简洁,不会忘记释放锁。
使用 ReentrantLock 的场景:
- 超时控制:比如在请求 AI 模型接口(FastAPI 后端)同步数据时,如果锁等待超过 3 秒还没拿到,我希望记录日志并返回失败,此时 tryLock(3, TimeUnit.SECONDS) 非常有用。
- 多条件唤醒:在实现生产者-消费者模型处理海量订单数据时,可以用两个 Condition(notFull, notEmpty)来精准唤醒生产线程或消费线程,避免 notifyAll 带来的“惊群效应”。