【面试题】并发篇

内容来源于网络,如有侵权请联系删除

什么是 Java 的 StampedLock?

StampedLock 是 Java 8 引入的一个锁机制,与传统的 ReentrantLockReadWriteLock 相比,StampedLock 通过引入乐观读锁和时间戳(stamp)的概念,提升了读写性能,尤其是在读多写少的场景下。

核心特性

  1. 写锁(write lock):独占模式的锁,和 ReentrantLock 类似,保证写操作的排他性。
  2. 悲观读锁(read lock):共享模式的锁,多个线程可以同时持有读锁,但写锁需要等待。
  3. 乐观读锁(optimistic read lock):无需阻塞的读锁机制,允许在没有竞争的情况下进行快速读取。当检测到有写操作发生时,才会回退到悲观读锁或重试。

时间戳(stamp)

  • 每个锁操作都会返回一个 stamp,代表了锁的状态,后续操作需要根据这个 stamp 来验证锁是否有效。

StampedLock 的优缺点

优点

  • 乐观读模式提供了更高效的并发读操作,避免了传统锁机制下的线程阻塞。
  • 更灵活的锁机制,允许不同的锁模式进行切换,适合不同场景。

缺点

  • 不可重入StampedLock 是非重入锁,这意味着同一个线程不能在持有锁时再次获取同类型的锁,否则会造成死锁。
  • 读锁饥饿:在高写入负载的场景下,悲观读锁可能会被长期阻塞,导致读操作饥饿。
  • CPU飙升风险:如果线程使用 writeLock() 或者readLock() 获得锁之后,线程还没执行完就被 interrupt() 的话,会导致CPU飙升,需要用 readLockInterruptibly 或者 writeLockInterruptibly

CPU 飙升示例代码

我本地跑了下复现了:

pic.jpg

内部原理

  • StampedLock 通过一个长整型值来管理状态,低位用于表示锁的类型(写锁、读锁),高位用于表示锁的计数。当乐观读锁被获取时,生成一个时间戳(stamp),并在后续验证时判断是否有写操作发生。
  • validate(stamp) 方法可以有效判断在持有乐观读锁的情况下,是否有其他写操作干扰(被修改了),从而决定是否要变为悲观读锁。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
private double x, y;
private final StampedLock lock = new StampedLock();

// 写操作:移动坐标
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp); // 释放写锁
}
}

// 悲观读操作:获取坐标
public double distanceFromOrigin() {
long stamp = lock.readLock(); // 获取读锁
try {
return Math.sqrt(x * x + y * y);
} finally {
lock.unlockRead(stamp); // 释放读锁
}
}

// 乐观读操作:获取坐标
public double optimisticDistanceFromOrigin() {
long stamp = lock.tryOptimisticRead(); // 获取乐观读锁
double currentX = x, currentY = y;
if (!lock.validate(stamp)) { // 检查乐观读锁是否被其他写操作干扰
stamp = lock.readLock(); // 回退到悲观读锁
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp); // 释放读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}

关键点

  • 写锁用于修改共享资源,多个线程无法同时持有写锁。
  • 悲观读锁保证数据一致性,适合写操作较多的场景。
  • 乐观读锁适用于读多写少的场景,可以在大部分时间无锁读取,大大提高性能。

你使用过 Java 中的哪些阻塞队列?

阻塞队列主要用来阻塞队列的插入和获取操作,当队列满了的时候插入操作会被阻塞,直到队列有空位。当队列为空的时候获取操作会被阻塞,直到队列有值。

常用在实现生产者和消费者场景,在笔试题中比较常见。

常见的阻塞队列包括:

  • ArrayBlockingQueue:一个有界队列,底层基于数组实现。需要在初始化时指定队列的大小,队列满时,生产者会被阻塞,队列空时,消费者会被阻塞。
  • LinkedBlockingQueue:基于链表的阻塞队列,允许可选的界限(有界或无界)。无界模式下可以不断添加元素,直到耗尽系统资源。有界模式则类似于 ArrayBlockingQueue,但吞吐量通常较高。
  • PriorityBlockingQueue:一个无界的优先级队列,元素按照自然顺序或者指定的比较器顺序进行排序。与其他阻塞队列不同的是,PriorityBlockingQueue 不保证元素的 FIFO 顺序。
  • DelayQueue:一个无界队列,队列中的元素必须实现 Delayed 接口,只有当元素的延迟时间到期时,才能被取出。常用于延迟任务调度。
  • SynchronousQueue:一个没有内部容量的队列,每个插入操作必须等待对应的移除操作,反之亦然。常用于在线程之间的直接传递任务,而不是存储任务。

回答重点

阻塞队列主要用来阻塞队列的插入和获取操作,当队列满了的时候插入操作会被阻塞,直到队列有空位。当队列为空的时候获取操作会被阻塞,直到队列有值。

常用在实现生产者和消费者场景,在笔试题中比较常见。

常见的阻塞队列包括:

  • ArrayBlockingQueue:一个有界队列,底层基于数组实现。需要在初始化时指定队列的大小,队列满时,生产者会被阻塞,队列空时,消费者会被阻塞。
  • LinkedBlockingQueue:基于链表的阻塞队列,允许可选的界限(有界或无界)。无界模式下可以不断添加元素,直到耗尽系统资源。有界模式则类似于 ArrayBlockingQueue,但吞吐量通常较高。
  • PriorityBlockingQueue:一个无界的优先级队列,元素按照自然顺序或者指定的比较器顺序进行排序。与其他阻塞队列不同的是,PriorityBlockingQueue 不保证元素的 FIFO 顺序。
  • DelayQueue:一个无界队列,队列中的元素必须实现 Delayed 接口,只有当元素的延迟时间到期时,才能被取出。常用于延迟任务调度。
  • SynchronousQueue:一个没有内部容量的队列,每个插入操作必须等待对应的移除操作,反之亦然。常用于在线程之间的直接传递任务,而不是存储任务。

扩展知识

使用场景

  • ArrayBlockingQueueLinkedBlockingQueue 常用于典型的生产者-消费者场景。例如:任务处理系统中,生产者生成任务,消费者从队列中取出任务并执行。
  • PriorityBlockingQueue 更适合处理带有优先级的任务场景,如任务调度系统。
  • DelayQueue 适用于需要延迟处理的任务,例如:缓存失效策略、定时任务调度等。
  • SynchronousQueue 适合在线程间直接传递数据,而不希望数据被存储在队列中。例如:ThreadPoolExecutor 的直接交接模式中使用 SynchronousQueue 来传递任务。

ArrayBlockingQueue 和 LinkedBlockingQueue 区别

常见的有 ArrayBlockingQueue 和 LinkedBlockingQueue,分别是基于数组和链表的有界阻塞队列。

两者原理都是基于 ReentrantLock 和 Condition 。

ArrayBlockingQueue 基于数组,内部实现只用了一把锁,可以指定公平或者非公平锁。

LinkedBlockingQueue 基于链表,内部实现用了两把锁,take 一把、put 一把,所以入队和出队这两个操作是可以并行的,从这里看并发度应该比 ArrayBlockingQueue 高。

LinkedTransferQueue

LinkedTransferQueue,相对于其他阻塞队列从名字来看它有 Transfer 功能,其实也不是什么神奇功能,一般阻塞队列都是将元素入队,然后消费者从队列中获取元素。

而 LinkedTransferQueue 的 transfer 是元素入队的时候看看是否已经有消费者在等了,如果有在等了直接给消费者即可,所以就是这里少了一层,没有锁操作。