
用 Random 做加权随机,别直接调 nextDouble() 累加比较
权重抽奖本质是「按比例切分 [0,1) 区间」,但手写累加容易出浮点误差或边界越界。比如奖品 A 权重 30、B 权重 50、C 权重 20,总和 100 —— 正确做法是先算前缀和:[30, 80, 100],再用 nextInt(100) 生成整数,二分查找落在哪个区间。
- 用
nextInt(totalWeight)(整数)比nextDouble()更稳,避免0.999999999踩不中最后一个区间 - 权重建议全转为
int,别混用小数(如 0.3/0.5/0.2),否则总和难精确到 1.0 - 如果权重来自配置文件,读入后立刻做归一化校验:
Math.abs(sum - 100) > 1e-6就该报错,别默默修正
Java 里用 TreeMap.floorEntry() 快速查权重区间
前缀和数组配二分查找(Arrays.binarySearch)可行,但更简洁的是用 TreeMap 存「累积权重 → 奖品」映射,靠 floorEntry() 一步定位。它天然支持插入有序、查找上界,且不用手动维护数组下标。
- 键必须是累积值,不是原始权重;例如 A:30 → 存
map.put(30, "A"),B:50 → 存map.put(80, "B") - 抽中时用
map.floorEntry(random.nextInt(total)),返回的Map.Entry的 value 就是中奖项 - 注意:如果总权重是 0,
nextInt(0)会抛IllegalArgumentException,得提前 guard
并发场景下别让多个线程共用一个 Random 实例
Random 不是完全线程安全的——它的 next() 方法用 AtomicLong 更新种子,但多线程高并发时仍可能因 CAS 失败重试而轻微拖慢,更关键的是:如果你在抽奖逻辑里用了 setSeed()(比如为了可重现测试),那共享实例会导致行为串扰。
- 生产环境统一用
ThreadLocalRandom.current(),它每个线程独享实例,且省去同步开销 - 别在循环里反复 new
Random()(尤其是用系统时间做 seed),同一毫秒内创建多个实例会导致重复序列 - 单元测试需要固定结果?用
new Random(123L)没问题,但只限测试代码,别塞进业务逻辑
权重动态更新时,别现场重建 TreeMap 或前缀数组
如果运营后台能实时改奖品权重,每次修改都重新计算前缀和、清空再 put 进 TreeMap,看似干净,实则在高 QPS 下可能引发短时锁竞争(TreeMap 写操作非无锁)或 GC 压力。
立即学习“Java免费学习笔记(深入)”;
- 更轻量的做法是把权重存在
ConcurrentHashMap,抽前用computeIfAbsent缓存当前有效前缀和结构,带版本戳防脏读 - 或者直接换方案:用蓄水池算法(Reservoir Sampling)的变种,适合流式权重更新,但实现复杂度上升,小项目没必要
- 最常被忽略的一点:权重变更后,旧请求可能还在用老缓存,要确保「生效时间」对齐(比如用
AtomicReference<map>></map>+ CAS 替换)
事情说清了就结束。权重算法本身不难,难的是浮点精度、并发安全、动态更新这三处细节,漏掉任何一处,上线后都可能中奖率飘移或偶发空指针。










