我在 Go 的 for 循环里写 select 处理 channel、为了让它别卡住顺手加了个 default 分支,结果服务一启动某个 CPU 核心就直接飙到百分之百风扇狂转,我盯着那段逻辑明明没干什么重活百思不得其解最后才反应过来那个 default 让 select 永远不阻塞整个循环变成了疯狂空转的忙轮询的深度复盘

我有个后台 goroutine 在 for 循环里用 select 监听几个 channel(收任务、收停止信号),担心万一所有 channel 都没消息 select 会卡住、就给它加了个 default 分支,想着没消息时走 default 不卡住挺稳妥,功能也正常。可一上线就发现:服务明明很闲没什么任务、某个 CPU 核心却常年 100% 风扇狂转、整体性能还下降了。top 看是我的进程吃满 CPU,pprof 抓火焰图热点全堆在那个 for-select 循环上。我很纳闷这循环没干重活怎么这么烧 CPU,直到盯着 default 才明白:select 如果有 case 就绪就执行那个 case,如果没有 case 就绪且没有 default 它就阻塞等待(让出 CPU、什么都不做地等到有 channel 就绪),但一旦有了 default、没 case 就绪时 select 就立刻执行 default 不再阻塞;而我把它放在 for 循环里,于是所有 channel 没消息时 select 立刻走 default→立刻回 for 顶部→再 select→还是没消息→又立刻 default→无限疯狂空转,把一个 CPU 核占满却什么有用的事都没做。我加 default 本意是别卡住,实际造成的是一刻不停地空忙。根因是 Go 的 select 有两种等待模式:无 default 时所有 case 未就绪则阻塞、goroutine 挂起让出 CPU、有 channel 就绪时被唤醒、几乎不耗 CPU;有 default 时没 case 就绪就立即执行 default 根本不等待;把有 default 的 select 放进 for 且 channel 大多空闲就形成忙轮询空耗一个核。正解是分清要哪种等:想等消息没消息就安静等着就去掉 default 让 select 阻塞(最省 CPU 最常用)、要周期性做事用 time.Ticker 当 case、真要非阻塞探测 default 要有明确退出别放紧凑 for、退出用 ctx.Done()。这篇复盘从故障现场讲到阻塞等待与忙轮询是两种不同的等、对照表、怎么诊断,再到阻塞 select 加 ticker 加 ctx 退出的完整正解与骨架,以及轮询代替阻塞读/极短 sleep 轮询/自旋锁长临界区/前端高频轮询等同类坑,和等待资源与忙闲、让无事可做的资源安静歇着而非空转的认知。

我在 Go 的 for 循环里写 select 处理 channel、为了让它别卡住顺手加了个 default 分支,结果服务一启动某个 CPU 核心就直接飙到百分之百、风扇狂转,我盯着那段逻辑明明没干什么重活百思不得其解,最后才反应过来那个 default 让 select 永远不阻塞、整个循环变成了疯狂空转的忙轮询

这是一次让我把 Go 里"select 的 default 分支"这件事,从"加上它就不会卡住、挺好",重新理解成"它把'阻塞等待'变成了'疯狂空转'"的事故。我在 for 循环里写 select 处理 channel,为了让它别卡住,顺手加了个 default 分支。结果服务一启动,某个 CPU 核心就直接飙到 100%、风扇狂转。我盯着那段逻辑——明明没干什么重活——百思不得其解,最后才反应过来:那个 default 让 select 永远不阻塞,整个循环变成了疯狂空转的忙轮询。这篇就把这次"加了 default、CPU 烧满"的事故,从头到尾复盘一遍。

故障现场:没干重活,一个 CPU 核却被烧满

我有个后台 goroutine,在一个 for 循环里用 select 监听几个 channel(收任务、收停止信号)。我担心"万一所有 channel 都没消息,select 会不会卡住",于是给 select 加了一个 default 分支,想着"没消息时走 default、不卡住,挺稳妥"。代码跑起来功能是正常的。

可一上线我就发现不对:服务明明很闲、没什么任务,某个 CPU 核心却常年 100%、机器风扇狂转、整体性能还下降了。我用 top 看是我这个进程吃满了 CPU,用 pprof 抓火焰图,发现热点全堆在那个 for-select 循环上。我很纳闷:这循环没干什么重活啊,怎么这么烧 CPU? 直到我盯着那个 default 分支看,才彻底明白根因——select 的行为是:如果有 case 的 channel 就绪就执行那个 case;如果没有 case 就绪,且没有 default,它就阻塞等待(让出 CPU、什么都不做地等到有 channel 就绪);但一旦有了 default,当没有 case 就绪时,select 就立刻执行 default、不再阻塞。而我把这个 select 放在了 for 循环里:于是当所有 channel 都没消息时,select 立刻走 default → 循环立刻回到顶部 → 再 select → 还是没消息 → 又立刻走 default →…… 整个循环以 CPU 能跑多快就多快的速度疯狂空转,什么有用的事都没做,却把一个 CPU 核占得满满的。我加 default 的本意是"别卡住",可它实际造成的是"一刻不停地空忙"。

// 我的写法: for-select 里加了 default, 想"别卡住"
for {
    select {
    case task := <-taskCh:
        handle(task)
    case <-stopCh:
        return
    default:
        // 我以为: 没消息时走这, 不卡住
        // 实际: 没消息时【立刻】走这 → 立刻回 for 顶部 → 立刻再 select →
        //       又没消息 → 又立刻 default → 无限疯狂空转, 烧满一个 CPU 核
    }
}
// 现象: 服务很闲, 一个 CPU 核却常年 100%, 风扇狂转
// 根因: default 让 select 永不阻塞, for-select 退化成忙轮询(busy loop)

问题被钉死在这个认知错位上:我以为"加 default 让 select 不卡住"是好事,但 select 在 for 循环里、所有 channel 都空闲时,正确的行为恰恰应该是"阻塞等待"——也就是让出 CPU、安安静静地睡着,直到某个 channel 来消息才被唤醒。这种"阻塞等待"几乎不耗 CPU。而我加的 default,把这个"该睡就睡"的等待,变成了"一遍遍醒来看一眼、发现没事又立刻接着看"的疯狂轮询——它确实"不卡住",但代价是一个 CPU 核被无意义的空转占满。我混淆了两种完全不同的"":一种是"阻塞地等"(让出 CPU、被动地等唤醒,几乎零开销);一种是"忙等"(占着 CPU、主动地一遍遍查,烧满核)。我以为我避免了卡住,其实我只是把"静静地等"换成了"原地疯狂打转地等"。我怕它睡死过去,结果让它一刻不停地空跑,跑得满头大汗却什么也没做成。

第一件事:想明白"阻塞等待"和"忙轮询"是两种不同的等

把这次事故彻底想清楚,关键是理解Go 的 select 有两种截然不同的等待模式:没有 default 时,若所有 case 都未就绪,select 阻塞——goroutine 被挂起、让出 CPU,由 runtime 在某个 channel 就绪时再唤醒它,这期间几乎不消耗 CPU;有 default 时,若没有 case 就绪,select 立即执行 default 并返回,根本不等待。把"有 default 的 select"放进 for 循环、且 channel 大多数时候空闲,就会让循环以最高速度反复"查一下→没有→走 default→再查一下",形成忙轮询(busy-wait / busy loop),空耗一个 CPU 核。

所以关键是分清你到底想要哪种"":如果你想"等到有消息再处理、没消息就安静地等着"——那就不要 default,让 select 阻塞,这是最省 CPU、也最符合直觉的写法;default 只用在"我确实想'看一眼,有就拿、没有就立刻去干别的'(非阻塞探测)"的场景,而且绝不该把它放在一个会立刻转回来的紧凑 for 循环里"阻塞等待"不是"卡死"——它是 goroutine 该有的、廉价的休眠;真正该怕的不是 select 阻塞(那正是它的本职),而是用 default 把阻塞变成空转。关键认知是:"等待"有两种实现——让出资源被动等唤醒(阻塞),和占着资源主动反复查(轮询);绝大多数"等一件事发生"的场景,要的都是前者;用轮询去实现本该阻塞的等待,是在用 100% 的 CPU 空转,换一个你以为的"不卡住"。

// 正解1: 想"等消息、没消息就安静等着" → 去掉 default, 让 select 阻塞
for {
    select {
    case task := <-taskCh:
        handle(task)
    case <-stopCh:
        return
    // 没有 default! 没消息时 select 阻塞、让出 CPU, 几乎零开销
    }
}

// 正解2: 真要"定期做点事"而非死等 → 用 ticker, 而不是 default 空转
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
    select {
    case task := <-taskCh:
        handle(task)
    case <-ticker.C:
        doPeriodicWork()      // 每秒一次, 不是每纳秒一次
    case <-stopCh:
        return
    }
}

// 正解3: 真的需要非阻塞探测(default 的正当用途) —— 但别放在紧凑 for 里
select {
case msg := <-ch:
    handle(msg)
default:
    // 没消息就立刻去干别的(这次探测一下, 不是循环里疯狂探)
    doSomethingElse()
}

想通这一层,我才明白自己错在哪:我把"select 阻塞"当成了"卡死"这种坏事,急着用 default 去"避免"它,却没意识到阻塞正是"等消息"这个场景该有的、廉价的等待方式。我用 default 把一个本该让出 CPU 安静休眠的 goroutine,逼成了一个一刻不停疯狂查 channel 的空转机器。根治之道简单到只是删掉那个 default,让 select 回归它的本职——阻塞地等;真要周期性做事就用 ticker,真要非阻塞探测就别把它塞进紧凑循环。不是怕等待就用轮询去填满每一刻,而是分清哪种等待该让出 CPU、让该睡的安静睡去。

第二件事:正解——想等就让 select 阻塞,要周期做事用 ticker,探测别进紧凑循环

找到根因,正解就清晰了:分清你要的""是哪种——想"等消息、没消息就安静等着"就去掉 default 让 select 阻塞(几乎零 CPU);想"定期做点事"就用 time.Ticker 当一个 case(到点才触发),而不是用 default 空转;真要"非阻塞探测一下"(default 的正当用途)就别把它放进会立刻转回来的紧凑 for 循环里。

// 错误: for-select + default → 忙轮询, 烧满 CPU
for {
    select {
    case t := <-taskCh:
        handle(t)
    default:                  // ✗ 没消息时疯狂空转
    }
}

// 正解1: 去掉 default, 阻塞等待(最常用、最省 CPU)
for {
    select {
    case t := <-taskCh:
        handle(t)
    case <-ctx.Done():        // 用 context 优雅退出
        return
    }
}

// 正解2: 既要等消息、又要周期性做事 → ticker 当 case
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
    select {
    case t := <-taskCh:
        handle(t)
    case <-ticker.C:
        flushMetrics()        // 每 5 秒一次, 不空转
    case <-ctx.Done():
        return
    }
}

// 正解3: 批量非阻塞地"把 channel 里现有的都取出来"(default 的正当用法之一)
func drain(ch chan int) []int {
    var out []int
    for {
        select {
        case v := <-ch:
            out = append(out, v)   // 有就取
        default:
            return out             // 取空了就退出, 不是无限空转
        }
    }
}

这套做法的精髓,是让"等待"回归"阻塞"这种廉价、被动、让出 CPU 的形式,只在真正需要"不等、立刻看一眼"时才用 default,且确保它有明确的退出、不会在循环里无限空转。去掉 default 让 select 阻塞,是"等消息"场景的标准答案;周期性任务交给 ticker(到点唤醒一次);非阻塞探测则用在"排空 channel""试一下拿不到就走"这类有明确终点的地方。核心是:别用"一刻不停地查"去实现"安静地等"不是怕 select 阻塞就拿 default 堵上,而是认清阻塞就是这里该有的等待,把 CPU 还给该睡的 goroutine。

【用 for-select, 我现在认死的几条】

1. select 无 default: 没 case 就绪时【阻塞】, 让出 CPU, 几乎零开销

2. select 有 default: 没 case 就绪时【立即】走 default, 不等待

3. for-select + default + channel 常空 = 忙轮询, 烧满一个 CPU 核

4. 想"等消息、没消息安静等" → 去掉 default, 让 select 阻塞(最常用)

5. 要周期性做事 → 用 time.Ticker 当 case, 别用 default 空转

6. 真要非阻塞探测 → default 要有明确退出, 别放进紧凑 for 无限空转

7. 退出用 ctx.Done()/stopCh; "阻塞等待"不是卡死, 是廉价的休眠

第三件事:其他"用忙等代替阻塞等、空耗资源"的同类坑

顺着"本该让出资源被动等,却用占着资源主动反复查来实现"这条线,我把同类的坑都排查了一遍:

第一个,轮询代替阻塞读/事件通知for { if hasData() {...} } 不停查有没有数据,而不用阻塞 IO/条件变量/channel,CPU 空转。

第二个,sleep(0) 或极短 sleep 的轮询循环。用 for { check(); time.Sleep(time.Millisecond) } 高频轮询,间隔太短近似忙等,既费 CPU 又有延迟。

第三个,自旋锁用在长临界区。自旋锁(spin lock)只适合极短的临界区;锁要持有很久时还自旋,就是 CPU 空转,该用会让出 CPU 的互斥锁。

第四个,前端用 setInterval 高频轮询接口。每隔很短时间就请求一次问"有更新没",而不用 WebSocket/SSE/长轮询,既费资源又不实时。

第四件事:阻塞等 vs 忙轮询——一张对照表

我把 for-select 加不加 default 的两种行为摆在一起对比,核心看"没消息时怎么等、耗不耗 CPU":

维度 无 default(阻塞等待) 有 default(忙轮询)
没 case 就绪时 阻塞挂起, 让出 CPU 立刻走 default, 不等
放进 for 循环 睡到有消息才醒 疯狂空转, 烧满一个核
CPU 占用(空闲时) 几乎为 0 100%(一个核)
响应消息的延迟 极低(被唤醒) 极低但代价是空转
适用场景 等消息/事件(最常用) 非阻塞探测(别放紧凑循环)
周期性任务 加 ticker case 误用 default 空转(错)

看清这张表,选择就有谱了:"等消息"就别加 default、让 select 阻塞(几乎零 CPU);"周期做事"用 ticker;default 只留给"非阻塞探测且有明确退出"的场景,绝不放进会立刻转回来的紧凑 for。我这次踩坑,正是把 default 当成了"防卡住"的万金油塞进 for-select,结果把廉价的阻塞等待变成了烧 CPU 的忙轮询。阻塞,才是""的正确姿势。

第五件事:我曾经对 select/default 想当然的几个误区

这次事故也把我对 select 的一堆"想当然"照了个底朝天:

我以为 实际上
select 阻塞是坏事, 会卡死 阻塞是廉价的休眠, 正是"等消息"该有的样子
加 default 让它不卡住, 更稳妥 在 for 里反而退化成烧 CPU 的忙轮询
循环没干重活就不会费 CPU 空转的 for-select 能把一个核占满
default 是 select 的标配 它只用于非阻塞探测, 多数场景不该有
要周期做事就在 default 里做 那是每纳秒做一次的空转, 该用 ticker

这些误区的根子是同一个:我把"阻塞"和"卡死"混为一谈,以为让程序"停下来等"是危险的,于是用 default 让它"一刻不停地动",却没意识到这种""是毫无意义的空转、是用满负荷的 CPU 换一个虚假的"没卡住"。真正高效的"",恰恰是该停时就停、让出资源、被唤醒再动;而忙轮询是用最大的能耗,做着最少的有用功。把"让出资源去等"误当成"危险的卡死"、转而用"占着资源空转"去填满每一刻,是这类 CPU 空耗的共同根源。

第六件事:写 for-select、排查"CPU 莫名烧满"时,我现在的自检习惯

现在每当我写 for-select、或排查"服务很闲但某个 CPU 核常年 100%",我都会先按这张图问自己:

这张图的精髓,是"CPU 空耗先查 for-select 有没有 default 造成忙轮询;等消息就删 default 让它阻塞, 周期做事用 ticker"设计就等消息别加 default 让 select 阻塞、周期任务用 ticker、default 只留给有明确退出的非阻塞探测、排查就pprof 看热点是不是 for-select、那个 select 有没有 default 空转这套习惯,让我从"怕卡住就加 default"变成了"先分清要哪种等"——核心始终是:Go 的 select 有两种截然不同的等待模式:没有 default 时若所有 case 都未就绪 select 阻塞——goroutine 被挂起让出 CPU、由 runtime 在某个 channel 就绪时再唤醒它、这期间几乎不消耗 CPU;有 default 时若没有 case 就绪 select 立即执行 default 并返回根本不等待;把有 default 的 select 放进 for 循环且 channel 大多数时候空闲就会让循环以最高速度反复查一下没有走 default 再查一下、形成忙轮询 busy loop 空耗一个 CPU 核;所以关键是分清你到底想要哪种等:想等到有消息再处理没消息就安静等着就不要 default 让 select 阻塞这是最省 CPU 也最符合直觉的写法、default 只用在确实想看一眼有就拿没有就立刻去干别的的非阻塞探测场景而且绝不该把它放在一个会立刻转回来的紧凑 for 循环里;阻塞等待不是卡死它是 goroutine 该有的廉价的休眠、真正该怕的不是 select 阻塞那正是它的本职而是用 default 把阻塞变成空转;一句话等待有两种实现——让出资源被动等唤醒(阻塞)和占着资源主动反复查(轮询)、绝大多数等一件事发生的场景要的都是前者、用轮询去实现本该阻塞的等待是在用 100% 的 CPU 空转换一个你以为的不卡住;要周期性做事用 time.Ticker 当 case 到点才触发、要非阻塞探测要有明确退出别无限空转、退出用 ctx.Done()。

我立下的几条规矩

这场"加了 default、CPU 烧满"的事故,换来了我写 for-select 时,刻进骨子里的几条铁律:

  1. select 无 default:没 case 就绪时阻塞、让出 CPU、几乎零开销。
  2. select 有 default:没 case 就绪时立即走 default、不等待。
  3. for-select + default + channel 常空 = 忙轮询、烧满一个 CPU 核。
  4. 想"等消息、没消息安静等" → 去掉 default,让 select 阻塞(最常用)。
  5. 要周期性做事 → 用 time.Ticker 当 case,别用 default 空转。
  6. 真要非阻塞探测 → default 要有明确退出,别放进紧凑 for 无限空转。
  7. "阻塞等待"不是卡死,是廉价的休眠;退出用 ctx.Done()/stopCh。

附:我现在写后台 goroutine 的"阻塞 select + ticker + ctx 退出"骨架

这是我现在写后台 goroutine 循环固定套的骨架——把这次踩坑的教训(等消息让 select 阻塞、周期任务用 ticker、ctx 退出、绝不 default 空转)固化成一套结构,让"忙轮询烧 CPU"那种坑再不会埋进代码:

// 标准后台 worker: 阻塞 select 等消息 + ticker 周期任务 + ctx 优雅退出
func worker(ctx context.Context, taskCh <-chan Task) {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()
    for {
        select {
        case task := <-taskCh:
            handle(task)              // 来任务就处理
        case <-ticker.C:
            flushMetrics()            // 每 5 秒做一次周期工作
        case <-ctx.Done():
            cleanup()                 // 收到取消, 优雅退出
            return
        }
        // 注意: 没有 default! 三个 case 都没就绪时 select 阻塞、让出 CPU。
        // 空闲时这个 goroutine 几乎不耗 CPU, 有事才被唤醒。
    }
}

// 非阻塞探测的【正当】用法: 排空 channel(有明确终点, 不是无限空转)
func drainNonBlocking(ch <-chan int) []int {
    var out []int
    for {
        select {
        case v := <-ch:
            out = append(out, v)
        default:
            return out                // 取空就返回, default 在这里是对的
        }
    }
}

// 自检: 启动 worker 后服务空闲时, 观察该 goroutine 的 CPU 占用应接近 0;
//   若某核常年 100%, 用 pprof 看是不是哪个 for-select 误加了 default 空转。

这套骨架把我这次的教训钉死在了结构里:等消息让 select 阻塞(三个 case 都没就绪就让出 CPU、空闲时几乎零占用)、周期任务交给 time.Ticker(到点唤醒一次而非每纳秒空转一次)、退出用 ctx.Done() 优雅收尾、绝不在紧凑 for 里加 default;default 只留给排空 channel 这类有明确终点的非阻塞探测;并用空闲时 CPU 占用应接近 0 作自检。这样,后台 goroutine 该睡就睡、有事才醒,而不再是当初那个"服务很闲、却把一个 CPU 核空转烧满"的局面。把"分清阻塞等待与忙轮询、让无事可做的资源安静地歇着"这个道理,沉淀成写后台循环的固定骨架,这是我对这次"风扇狂转的空闲服务"最实在的交代——毕竟,真正的高效不是一刻不停地空忙,而是该歇时歇、有事才动。

写在最后

回头看,这场由"select 加 default"引发的"CPU 烧满"事故,真正教给我的,远不止"删掉那个 default"这一个技巧。它让我对"'等待'这件看似消极的事,其实有两种截然不同的姿态:一种是'安静地停下来,让出资源,把'什么时候该继续'交给一个唤醒信号'(阻塞);另一种是'一刻不停地、主动地、反复地去查'是不是该继续了''(轮询);后者表面上'更积极、更不会错过、更不会卡住',实际上却是在用尽全力做着大量无用功,把本可以歇着的资源,耗在了无意义的空转上",有了一次刻骨的体会。我栽跟头,是因为我把"安静地阻塞等待"误解成了"危险的卡死",于是用"一刻不停地轮询"去"避免"它——我心里对"停下来等"有种本能的不安,觉得程序"不动了"就是出了问题、就是卡住了;于是我加了 default,让它"永远在动",以为这样就稳妥了;我没意识到,真正高效的等待恰恰是"该停就停"——让出 CPU、安静地睡着、等有事了被唤醒;而我用 default 换来的"永远在动",不是勤奋,是一个 CPU 核在原地疯狂空转、满负荷地做着零产出的事这让我领悟到一个关于"等待、资源与忙闲"的深刻认知:"看起来在忙"和"真的在做有用的事"是两回事;一个一刻不停、CPU 拉满的循环,可能正在做的是最彻底的无用功——它用最大的能耗,产出着零价值;而"看起来停着不动"(阻塞等待)也绝不等于"出了问题、卡死了":让出资源、安静等待被唤醒,恰恰是处理"等一件事发生"这类场景最高效、最节能、最正确的姿态;真正成熟的做法,是分清"该主动忙的时候忙(有事做就全力做)"和"该安静等的时候等(无事可做就让出资源歇着)",而不是出于对"停下来"的不安,用无意义的空转去填满每一个本该休息的间隙——那不是积极,是浪费这给了我一种看待"一切'等待某件事发生'之事"时的清醒:每当我要"等一件事发生"时,要追问"我是在'安静地让出资源、等它来唤醒我',还是在'占着资源、一遍遍主动去查它发生没'?如果没事可做,我有没有让该歇的资源真正歇下来,还是让它在空转"——用'阻塞/被唤醒'这种让出资源的方式去等,而不是用'不停轮询'这种占着资源空转的方式;"分清阻塞等待与忙轮询、让无事可做的资源安静地歇着而非空转",是用对 Go 的 select、也是写出节能高效系统的关键认清 select 无 default 阻塞让出 CPU、有 default 在 for 里会忙轮询烧 CPU、等消息就该让它阻塞——这,是我用一次"服务很闲 CPU 却烧满"的事故,换来的、关于 Go、也关于如何正确地等待的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次在 for-select 里顺手想加 default"防卡住"时,先停一秒问"我是想等消息吗?那就让它阻塞、别加 default",那我对着那个"服务很闲、风扇却狂转"的 CPU 曲线排查的大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我在 JavaScript 里用 reduce 求和写得简洁又顺手、测试也全过,上线后却突然抛出 Reduce of empty array with no initial value 把页面整个搞崩,我盯着那行用了无数次的 reduce 百思不得其解最后才明白只要传进来的是个空数组而我又没给它一个初始值 reduce 就会因为根本没有起点可用而直接报错的深度复盘

2026-6-3 9:08:44

技术教程

我在 Java 里用 subList 截了一段子列表想单独拿去处理、结果对子列表的改动竟然莫名其妙影响到了原列表,后来又遇到取完子列表往原列表里加了个元素再用那个子列表时直接抛 ConcurrentModificationException,折腾很久才搞懂 subList 返回的根本不是拷贝而是原列表的一个视图的深度复盘

2026-6-3 9:23:27

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索