ConcurrentSkipListMap存在数据时调用firstKey()仍抛出NoSuchElementException求助
问题根因分析
- 竞态条件导致调用瞬间Map为空:
ConcurrentSkipListMap仅保证单个操作的原子性,无法保障多线程组合操作的原子性。你的应用存在两个并行操作Map的线程:WebSocket推送线程负责更新、删除Map元素,独立循环线程负责读取Map首个元素。当WebSocket线程刚好将Map内所有元素删除(比如深度更新将所有现有价格档位的交易量置0,还未写入新价格档位)的瞬间,循环线程恰好调用firstKey()/firstEntry(),就会触发NoSuchElementException,哪怕后续毫秒级就有新元素写入Map,也不影响调用瞬间抛出异常。 - 裁剪Map的逻辑存在隐忧:你在裁剪Map长度时使用
StaticData.ask.keySet().toArray()[10]获取第11个元素的key,哪怕已经提前判断size()>10,但判断和toArray是两个独立操作,执行toArray时如果刚好有元素被删除导致size不足11,会触发数组越界异常,极端情况下甚至可能在裁剪时误删所有元素,进一步触发首元素读取异常。 - 静态变量可见性风险:
StaticData.ask未加volatile修饰,存在指令重排序导致循环线程拿到空引用的风险,初始化逻辑如果触发异常未重试,也会导致ask一直为空。
修复方案
- 安全读取首元素:不要直接调用
firstKey()/firstEntry(),如果不需要移除元素可以在调用时捕获NoSuchElementException后跳过本次循环,如果允许移除元素可以改用pollFirstEntry()读取,返回null直接跳过本次循环即可,示例代码如下:
public void loopProcess() { Analyzer analyzer = new Analyzer(); while (true) { Map.Entry<Double, Double> entry; try { entry = StaticData.ask.firstEntry(); } catch (NoSuchElementException e) { // 为空则短暂休眠降低CPU占用,跳过本次分析 Thread.sleep(1); continue; } analyzer.analyze(entry); } }
- 优化Map裁剪逻辑:改用更安全的方式裁剪,避免数组越界和误删所有元素,示例如下:
// 裁剪到只剩10个元素 while (StaticData.ask.size() > 10) { StaticData.ask.pollLastEntry(); }
- 完善静态变量定义:给
StaticData.ask添加volatile修饰,保证多线程可见性;初始化initAsks逻辑添加失败重试,确保ask成功初始化后再启动循环线程。
内容的提问来源于stack exchange,提问作者Tomáš Sýkora
相关产品推荐
相关产品推荐

