Kotlin中高频增删ArrayList引发NPE的优化解决方案问询
解决异步高频修改列表导致的遍历NPE问题及内存优化方案
首先,核心问题出在**ArrayList不是线程安全集合**,异步环境下的并发修改(移除首元素+添加尾元素)和遍历操作会破坏集合的内部一致性,最终导致NPE或并发修改异常。深拷贝之所以无效,是因为拷贝过程本身需要遍历原集合,此时仍可能被异步修改干扰。下面结合你的场景给出针对性解决方案:
一、最省心的线程安全替换:用ConcurrentLinkedQueue
你的操作是典型的FIFO滑动窗口,队列结构天然适配「移除头部+添加尾部」的逻辑,ConcurrentLinkedQueue是Java/Kotlin中线程安全的队列实现,它的迭代器是弱一致性的——遍历过程中不会因并发修改抛出异常,也不会返回null元素。
修改后的代码:
// 替换ArrayList为ConcurrentLinkedQueue private val buffer = ConcurrentLinkedQueue<CustomObj>() private fun observeFrequentData() { frequentData.observe(owner, Observer { data -> accelerationData ?: return@Observer GlobalScope.launch { val a = data[0].toDouble() val b = data[1].toDouble() val c = a + b val timestamp = System.currentTimeMillis() val customObj = CustomObj(c, timestamp) // 超过容量时移除头部,O(1)操作(比ArrayList的removeAt(0)高效太多) if (buffer.size >= 5000) { buffer.poll() } // 添加尾部元素,O(1)操作 buffer.offer(customObj) } }) } fun getBuffer() { // 迭代器线程安全,不会出现NPE val mappedData = buffer.map { it.smth } }
二、坚持用列表?加同步锁控制并发
如果业务必须依赖列表结构,可以通过同步锁让修改和遍历操作互斥,彻底避免并发冲突。注意:ArrayList的removeAt(0)是O(n)操作(需要移动后续所有元素),当buffer大小为5000时,性能会比队列差一些。
修改后的代码:
private val buffer = ArrayList<CustomObj>() // 用专用锁对象保证同步 private val bufferLock = Any() private fun observeFrequentData() { frequentData.observe(owner, Observer { data -> accelerationData ?: return@Observer GlobalScope.launch { val a = data[0].toDouble() val b = data[1].toDouble() val c = a + b val timestamp = System.currentTimeMillis() val customObj = CustomObj(c, timestamp) // 修改操作加锁 synchronized(bufferLock) { if (buffer.size >= 5000) { buffer.removeAt(0) } buffer.add(customObj) } } }) } fun getBuffer() { // 遍历操作也必须加锁,保证和修改互斥 val mappedData = synchronized(bufferLock) { buffer.map { it.smth } } }
三、内存最优方案:自定义环形缓冲区
如果追求极致的内存效率和性能,可以实现一个固定大小的环形缓冲区(循环数组)。它的内存占用固定(仅为设定的容量大小),添加和读取操作都是O(1),完全适配你的滑动窗口需求。
简化实现及使用:
class CircularBuffer<T>(private val capacity: Int) { private val array = arrayOfNulls<Any>(capacity) as Array<T> private var head = 0 private var size = 0 private val lock = Any() // 添加元素,满了自动覆盖最旧的头部元素 fun add(element: T) { synchronized(lock) { val tail = (head + size) % capacity array[tail] = element if (size < capacity) { size++ } else { head = (head + 1) % capacity } } } // 获取所有元素的线程安全方法 fun getAll(): List<T> { synchronized(lock) { val result = mutableListOf<T>() for (i in 0 until size) { val index = (head + i) % capacity result.add(array[index]) } return result } } } // 使用环形缓冲区 private val buffer = CircularBuffer<CustomObj>(5000) private fun observeFrequentData() { frequentData.observe(owner, Observer { data -> accelerationData ?: return@Observer GlobalScope.launch { val a = data[0].toDouble() val b = data[1].toDouble() val c = a + b val timestamp = System.currentTimeMillis() val customObj = CustomObj(c, timestamp) // 无需手动移除,缓冲区自动维护大小 buffer.add(customObj) } }) } fun getBuffer() { val mappedData = buffer.getAll().map { it.smth } }
为什么深拷贝解决不了问题?
深拷贝的本质是遍历原集合并复制元素,但遍历过程中原集合仍可能被异步修改:要么迭代器抛出ConcurrentModificationException,要么遍历到被移除的位置拿到null,根本无法从根源解决并发访问的冲突。如果要让深拷贝有效,必须在拷贝时加锁——但这样就和第二种方案逻辑一致,还多了一次拷贝的内存开销,完全没必要。
方案选择建议
- 优先选
ConcurrentLinkedQueue:代码改动最小,线程安全有保障,适合大部分场景 - 追求极致性能/内存选环形缓冲区:固定内存占用,O(1)操作效率最高
- 必须用列表才选同步锁方案:注意
removeAt(0)的性能损耗
内容的提问来源于stack exchange,提问作者MaaAn13
相关产品推荐
相关产品推荐

