无锁环境下线程分别对List执行添加与移除操作的风险
List<T>的Add与Remove的风险 首先纠正一个误解:List<T>的Remove操作不会触发扩容,只有Add在当前元素数量达到数组容量时才会触发扩容;Remove仅会将目标索引之后的元素向前移动一位覆盖目标位置,再将内部的_size字段减1,不会主动缩容(缩容需手动调用TrimExcess)。
当一个线程执行Add、另一个线程执行Remove时,由于List<T>的所有操作都不是线程安全的,内部状态(_items数组、_size元素计数、_version版本号)的修改没有同步保护,会引发以下具体风险:
数据丢失或重复
假设List当前_size为5,线程A执行Add:已将新元素写入_items[5],但还未执行_size++;此时线程B执行Remove(移除索引3的元素),会将_items[4]移到_items[3]、_items[5]移到_items[4],再将_size改为4。随后线程A完成_size++,将_size设为6。此时List的_size显示为6,但实际有效元素只有4个(_items[0-3]),_items[4]是原_items[5]的元素,_items[5]是线程A刚写入的新元素,导致元素重复;或者反过来,线程B的移动操作覆盖了线程A刚写入的元素,导致数据丢失。元素移除错误
若执行的是Remove(T item)(按元素移除),线程B在遍历_items查找目标元素时,线程A正在执行Add修改数组内容或_size,会导致B遍历到不一致的数组状态:要么找不到实际存在的元素(因为_size还没更新,B只遍历到旧的_size-1索引),要么误判元素位置,移除了错误的元素。_size计数与实际元素数量不匹配_size的修改(_size++/_size--)不是原子操作,本质是“读取-修改-写入”三步。并发下可能出现:线程A读取_size为5,线程B同时读取_size也为5;A执行加1后写入6,B执行减1后写入4。最终_size为4,但实际元素数量是5,导致后续操作要么访问数组越界(比如Add时直接写入_items[4],但实际数组容量可能不足),要么无法访问到已存在的元素。扩容过程中的数据损坏
当Add触发扩容时,线程A会新建一个更大的数组,将原_items的元素复制到新数组;此时线程B执行Remove,修改了原_items的元素位置或_size,导致A复制到新数组的数据是混乱的(比如包含已被移除的元素,或丢失了部分元素)。复制完成后A将_items指向新数组,_size设为旧_size+1,此时List的内部状态完全错误,后续所有操作都会出现异常。版本号混乱引发的异常
List<T>内部维护_version字段,每次修改操作都会递增该值,用于枚举时校验集合是否被修改。并发下_version的递增不是原子操作,可能导致多次修改后_version的值与实际修改次数不匹配;若后续有依赖_version的逻辑(比如自定义迭代器),会触发InvalidOperationException,甚至出现无异常的逻辑错误。
内容的提问来源于stack exchange,提问作者Piggy Chu

