详细介绍:LinkedList 源码逐行读:节点结构、头尾插入、双向遍历实现

LinkedList 源码逐行读:节点结构、头尾插入、双向遍历实现

关键词:Java集合、LinkedList、双向链表、节点结构、头尾插入、双向遍历、源码、面试
适合人群:Java初中高级工程师·面试冲刺·代码调优·架构设计
阅读时长:35 min(≈ 5300字)
版本环境:JDK 17(源码行号对应 jdk-17+35)


1. 开场白:面试三连击,能抗算我输

“LinkedList 的节点长啥样?为什么内部类叫 Node 而不是 Entry?”
“头插和尾插时间复杂度都是 O(1),但哪种更省 CPU?”
“双向遍历用 ListIterator,怎么做到一边向前一边向后?”

阿里 P7 面完 100 人,90% 画不出 Node 前后指针。
线上事故:某 RPC 框架用 LinkedList 做连接池,每秒 5k add/remove,YoungGC 从 30 ms 涨到 300 ms,CPU 占用高 40%,定位发现 Node 对象 200 万个。
背完本篇,你能徒手画出内存布局图,手写无锁双向链,顺便给出 3 种场景选型,让 GC 降 80%。


2. 知识骨架:LinkedList 全景一张图

LinkedList
├─private static class Node { E item; Node next; Node prev; }
├─transient Node first;
├─transient Node last;
├─transient int size = 0;
└─implements List, Deque, Queue

核心操作:
linkFirst()linkLast()linkBefore()unlink()listIterator()


3. 身世档案:核心参数一表打尽

字段/参数含义默认值/备注
first头节点引用null
last尾节点引用null
size元素个数0
Node内部静态类前驱 + 数据 + 后继
modCount结构性修改计数迭代器快速失败

4. 原理解码:源码逐行,行号指路

4.1 节点结构:私有静态内部类(行号 910)

private static class Node<E> {
  E item;
  Node<E> next;
    Node<E> prev;
      Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
        }
        }

静态内部类:不持有外部类引用,节省 1 个字节开销。

4.2 头插:linkFirst()(行号 155)

private void linkFirst(E e) {
final Node<E> f = first;
  final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
    last = newNode;      // 空链表
    else
    f.prev = newNode;
    size++;
    modCount++;
    }

4.3 尾插:linkLast()(行号 170)

void linkLast(E e) {
final Node<E> l = last;
  final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
    first = newNode;
    else
    l.next = newNode;
    size++;
    modCount++;
    }

4.4 中间插入:linkBefore()(行号 185)

void linkBefore(E e, Node<E> succ) {
  final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
      succ.prev = newNode;
      if (pred == null)
      first = newNode;
      else
      pred.next = newNode;
      size++;
      modCount++;
      }

4.5 删除节点:unlink()(行号 220)

E unlink(Node<E> x) {
  final E element = x.item;
  final Node<E> next = x.next;
    final Node<E> prev = x.prev;
      if (prev == null) {
      first = next;
      } else {
      prev.next = next;
      x.prev = null;
      }
      if (next == null) {
      last = prev;
      } else {
      next.prev = prev;
      x.next = null;
      }
      x.item = null; // help GC
      size--;
      modCount++;
      return element;
      }

注意:item 置 null,避免游离节点导致内存泄漏。


5. 实战复现:3 段代码 + GC 对比

5.1 头尾插入性能对比

int N = 1_000_000;
// 头插
LinkedList<Integer> ll = new LinkedList<>();
  long t1 = System.nanoTime();
  for (int i = 0; i < N; i++) ll.addFirst(i);
  long t2 = System.nanoTime();
  System.out.println("addFirst: " + (t2 - t1) / 1_000_000 + " ms");
  // 尾插
  ll = new LinkedList<>();
    t1 = System.nanoTime();
    for (int i = 0; i < N; i++) ll.addLast(i);
    t2 = System.nanoTime();
    System.out.println("addLast: " + (t2 - t1) / 1_000_000 + " ms");

输出(JDK 17,i5-11400):

addFirst: 78 ms
addLast: 72 ms

尾插略快:少一次 first.prev 写屏障。

5.2 遍历对比:普通 for vs 增强 for vs 双向迭代器

ListIterator<Integer> it = ll.listIterator();
  long t1 = System.nanoTime();
  while (it.hasNext()) it.next();
  long t2 = System.nanoTime();
  System.out.println("forward: " + (t2 - t1) / 1_000_000 + " ms");
  while (it.hasPrevious()) it.previous();
  long t3 = System.nanoTime();
  System.out.println("backward: " + (t3 - t2) / 1_000_000 + " ms");

双向迭代器前后遍历均 O(n),无额外内存。

5.3 Node 对象数量压测

// JProfiler 观察
LinkedList<Byte> list = new LinkedList<>();
  for (int i = 0; i < 5_000_000; i++) list.add((byte)1);

结果:
Node 实例 500 万,内存 ≈ 500w * 24 B = 114 MB
同等数据 ArrayList<byte[]> 仅 5 MB,差 20 倍。


6. 线上事故:LinkedList 做队列导致 Node 爆炸

背景
RPC 连接池用 LinkedList<Channel> 保存空闲连接,高峰 5 k QPS。

现象
YoungGC 从 30 ms 涨到 300 ms,CPU 占用 +40%。

根因
连接频繁借还,Node 对象每秒新建 1 万,GC 压力巨大。

复盘

  1. 压测复现:Node 对象 200 万,GC 耗时 10 倍。
  2. 修复:替换为 ArrayDeque<Channel>,Node 对象消失。
  3. 防呆清单:
    • 高并发队列优先使用 ArrayDeque / ConcurrentLinkedQueue;
    • 对象池禁用 LinkedList。

7. 面试 10 连击:答案 + 行号

问题答案
1. LinkedList 节点结构?静态内部类 Node,prev/item/next(行号 910)
2. 头插和尾插谁更快?尾插少一次 prev 写,略快(行号 170)
3. 中间插入如何定位?先根据 index < size>>1 决定从前还是后遍历(行号 389)
4. 删除节点为什么 item 置 null?help GC,避免游离引用(行号 235)
5. 线程安全吗?否,可用 Collections.synchronizedList
6. 能用 fori 随机访问吗?可以,但每次需遍历,O(n)
7. 内存占用对比 ArrayList?每个元素额外 Node 24 B,100 万元素多 20 倍
8. 如何实现双向迭代?ListIterator 前后指针(行号 885)
9. 队列场景选型建议?高并发用 ArrayDeque,并发用 ConcurrentLinkedQueue
10. 还能用 LinkedList 吗?低并发、频繁头尾插入、需 null 元素时可考虑

8. 总结升华:一张脑图 + 三句话口诀

[脑图文字版]
中央:LinkedList
├─Node:prev/item/next
├─头插:linkFirst
├─尾插:linkLast
├─删除:unlink + help GC
└─遍历:双向 ListIterator

口诀:
“节点静态省引用,头尾 O(1) 真轻松;Node 爆炸要当心,ArrayDeque 替代雄。”


9. 下篇预告

下一篇《HashMap 源码逐行读:hash 方法、冲突链表、红黑树阈值、扩容死链》将带你手写红黑树旋转、复现 JDK 7 死链环路,敬请期待!


10. 互动专区

你在生产环境踩过 LinkedList Node 内存坑吗?评论区贴出 GC 图 / 堆 Dump,一起源码级排查!

posted @ 2025-11-13 18:13  gccbuaa  阅读(13)  评论(0)    收藏  举报