C#中List<Thread>扩容时旧线程不被删除的底层原理探究
为什么List扩容后旧线程仍能正常运行?
核心原因:List存储的是Thread对象的引用,而非实例本身
List<T>底层依赖数组存储元素,但对于Thread这类引用类型,数组中保存的是指向堆上实际对象的内存引用,而非对象实例本身。
扩容过程中的引用复制逻辑
当List触发扩容时,内部执行的核心步骤是:
- 创建一个容量更大的新数组
- 通过
Array.Copy(_items, 0, array, 0, _size)将原数组中的引用地址完整复制到新数组 - 废弃原数组,将新数组设为当前存储容器
这个过程中,新旧数组里的引用指向的是同一个Thread实例,并没有对Thread对象本身做任何复制或移动。
Thread实例不会被GC回收的关键
一旦调用Thread.Start(),.NET的CLR线程调度器会自动持有该Thread对象的引用,直到线程完全执行完毕。哪怕原数组因为没有引用指向它而被GC回收,Thread实例本身依然有CLR的引用支撑,不会被垃圾回收器清理,因此线程能正常运行到结束。
结合测试代码的具体解释
你在循环中创建并启动线程后,每个Thread实例都被CLR调度器持有引用。即使List扩容废弃了原数组,甚至手动调用GC.Collect()强制回收原数组,这些Thread实例依然存活,能按预期完成Sleep和输出操作,最终得到0 1 2 3 4 5 6 7的结果。
额外说明:引用类型与值类型的差异
如果List存储的是值类型(比如int),扩容时会直接复制实例本身;但对于引用类型,容器的变化只会影响引用的存储,不会改变实例本身的生命周期——实例的存活由所有指向它的引用共同决定,和存储它的容器是否被回收无关。
内容的提问来源于stack exchange,提问作者Han Han
相关产品推荐
相关产品推荐

