Spark源码中的ReentrantLock
Spark 核心源码中的 ReentrantLock
实例
让我们来看几个最经典和重要的例子,这些例子完美地诠释了为什么需要用到 ReentrantLock
而不是简单的 synchronized
。
1. BlockInfoManager
- 块信息管理器
这是最能体现 ReentrantLock
优势的例子之一。
源码位置:
org.apache.spark.storage.BlockInfoManager
作用: 管理所有存储在 Executor 中的数据块(Block)的元信息(如位置、状态等)。多个线程会并发地访问和修改这些元信息。
实现:
private val lock = new ReentrantLock(true) // !!! 注意:这里创建了一个公平锁
为什么使用
ReentrantLock
:公平性要求: 通过
true
参数指定为公平锁。这意味着等待时间最长的线程会优先获取锁,可以有效防止某些线程在极高并发下发生“饥饿”(Starvation)现象,保证了元信息更新的公平性。可重入性: 管理代码中存在嵌套调用,可重入特性保证了同一线程可以多次获取锁。
细粒度控制: 配合
lock.newCondition()
可以创建多个条件变量(Condition
),实现更复杂的线程间通信,但这在该类中不是主要目的。
2. DiskBlockManager
- 磁盘块管理器
源码位置:
org.apache.spark.storage.DiskBlockManager
作用: 管理磁盘上块的创建、存储和删除。
实现:
private val subDirsLock = new ReentrantLock() // 非公平锁
为什么使用
ReentrantLock
:性能优先: 此处默认使用非公平锁。因为文件操作(创建子目录)的竞争通常不会特别激烈,非公平锁的吞吐量更高,避免了线程切换的开销。
减少同步范围: 使用
lock.lock()
和lock.unlock()
可以更精确地控制临界区范围,而不是像synchronized
那样锁定整个方法或代码块。
为什么公平锁比非公平锁慢?
恢复一个挂起的线程与该线程真正执行之间存在严重的延迟,因为CPU的寄存器缓存和高速缓存都可能失效,需要重新载入数据。这时,如果将锁交给执行时间很短的任务,那么可以充分利用在被唤起线程真正开始执行之前的数据准备时间,从而提高了性能。从此处可以看出,当线程持有锁的时间比较长(执行任务需要的时间长)或者请求锁的平均时间间隔比较长,则应该使用公平锁。
原文链接:https://blog.csdn.net/Wengzhengcun/article/details/87861639