不受待见的 LinkedList
LinkedList 的作者 Joshua Bloch 曾在Twitter提到:
“Does anyone actually use LinkedList? I wrote it, and I never use it.”
可见,就连作者自己都不愿意用它。
LinkedList 怎么了?
一句话总结:LinkedList 基于对象构建(而非数组),这极大地影响了性能,而且受限于线性结构,它没有充分利用对象存储的灵活性。
与紧密存储的数组相比,链表的存储是离散的,分布在更大的内存范围内。
为什么链表的离散存储是个问题?
- 指针开销大:每个节点都需要存储
next和prev指针,带来额外的内存消耗。 - CPU 缓存不友好:数组的元素是连续存储的,CPU 访问时可以利用缓存预取(Cache Prefetch),而链表的离散存储会导致频繁的缓存未命中(Cache Miss)。
- GC 负担重:LinkedList 需要频繁分配和回收对象,容易加重 GC 负担,影响性能。
- JVM 没有特殊优化:JVM 不会对 LinkedList 进行特殊优化,导致链表的存储空间可能远大于实际数据的大小,而数组可以利用对象数组存储,提高空间利用率。
如何弥补 LinkedList 的缺憾?
链表的悲剧:线性结构 + 离散存储的无奈
- 线性结构 限制了链表的访问模式,使得查找变慢。
- 离散存储 导致大量的指针跳转,降低 CPU 缓存命中率,影响访问速度。
改进方向:利用更高效的数据结构,弥补链表的劣势
如果链表无法避免离散存储的缺陷,我们可以通过更高效的数据结构来优化它,例如:
- 块链表(Block List) :将多个元素存储在一个块中,减少指针开销,提高缓存友好性。
- 分层链表(Skip List) :增加索引层,降低查找复杂度(接近 O(log n))。
- B+ 树:利用树形结构,提高查询和修改效率,同时保持缓存友好性。
现实应用:业界的优化方案
在实际应用中,单纯的 LinkedList 已经很少被使用,业界通常采用:
- 跳表(Skip List) —— Redis 使用的底层数据结构之一,比链表更高效。
- 块存储(Block Storage) —— 例如 RoaringBitmap,通过块结构减少内存碎片。
- B+ 树 —— 数据库索引的核心结构,优化了存储和查询效率。
总结
LinkedList 的问题在于:离散存储带来的额外开销,使其性能远不如基于数组的数据结构。JVM 并不会对 LinkedList 进行优化,导致它在现代计算环境下显得低效。
优化方向:如果必须使用链表,应该考虑 跳表、块链表、B+树 等更高效的数据结构,而不是简单的双向链表。
现实应用:业界已经广泛采用更优的存储结构,单纯的 LinkedList 已经很少被使用。