【035】并发下 Random 卡死?ThreadLocalRandom 让 QPS 翻 10 倍!

请添加图片描述
Java中的ThreadLocalRandom是什么?它解决了什么问题?

粗略回答:

1、定义: ThreadLocalRandom是Java并发包中的一个用于生成随机数的类,它是对java.util.Random的改进。

2、解决的问题: 解决了在多线程环境下使用普通Random实例时可能出现的竞争条件问题,提高了性能。

请添加图片描述
📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌


零、引入

“QPS 从 1 万跌到 300,订单号全是重复的!” 王二抱着电脑蹲在工位角落,屏幕上的监控曲线像坐了跳楼机 —— 他用 Random 生成订单号的代码,一到秒杀活动就崩,领导的夺命连环 call 快把手机震碎了。隔壁哇哥叼着棒棒糖晃过来,扫了眼代码乐了:“你这是用‘公共厕所’思路写并发啊,Random 这东西在高并发下就是个坑,今天给你换个 ThreadLocalRandom,保准 QPS 直接起飞!”

点赞 + 关注,跟着哇哥和王二,用奶茶店取号机的逻辑吃透 ThreadLocalRandom,下次再用 Random 被骂,我替你背锅!
在这里插入图片描述

一、王二的坑:Random 为啥一并发就 “罢工”?

王二的代码很简单:用 Random 生成 6 位随机数当订单后缀,平时测着没问题,一到秒杀活动就出幺蛾子 ——QPS 暴跌,还出现重复订单号。

👉 王二的 “坑代码”(Random 并发灾难)

package cn.tcmeta.randoms;


import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: laoren
 * @description: 王二的订单号生成器:用Random搞崩秒杀系统
 * @version: 1.0.0
 */
public class RandomDisaster {
    // 全局共用一个Random实例
    private static final Random RANDOM = new Random();
    // 统计重复订单号
    private static int duplicateCount = 0;
    // 订单号存储集合(模拟)
    private static final java.util.Set<String> ORDER_SET = new java.util.HashSet<>();

    // 生成订单号:时间戳+6位随机数
    public static String generateOrderNo() {
        long timestamp = System.currentTimeMillis();
        // 用Random生成6位随机数
        int randomNum = RANDOM.nextInt(1000000);
        // 拼接订单号(补0成6位)
        return timestamp + String.format("%06d", randomNum);
    }

    static void main() throws InterruptedException {
        // 秒杀场景:100个线程同时生成订单号
        int threadCount = 100;
        ExecutorService threadPool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);

        long start = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            threadPool.submit(() -> {
                try {
                    // 每个线程生成100个订单号(模拟高并发)
                    for (int j = 0; j < 100; j++) {
                        String orderNo = generateOrderNo();
                        // 线程安全地判断重复
                        synchronized (ORDER_SET) {
                            if (!ORDER_SET.add(orderNo)) {
                                duplicateCount++;
                                System.out.println("警告:出现重复订单号!" + orderNo);
                            }
                        }
                    }
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        long end = System.currentTimeMillis();
        threadPool.shutdown();

        System.out.println("总耗时:" + (end - start) + "ms");
        System.out.println("重复订单号数量:" + duplicateCount);
        System.out.println("生成订单号总数:" + ORDER_SET.size());
    }
}

运行结果:惨不忍睹

警告:出现重复订单号!1765379065786011504
警告:出现重复订单号!1765379065793743668
警告:出现重复订单号!1765379065799542780
警告:出现重复订单号!1765379065799427898
警告:出现重复订单号!1765379065801271314
总耗时:77ms
重复订单号数量:5
生成订单号总数:9995

王二挠头:“平时测着好好的,怎么一并发就出问题?”

哇哥把棒棒糖棍一扔:“这就是 Random 的‘线程安全陷阱’ ! 它是线程安全的,但安全的代价是‘排队抢锁’—— 就像奶茶店只有一台取号机,100 个人挤着用,能不慢吗?重复是因为抢锁时种子没更新好,相当于两个人拿到了同一个号。”

二、用奶茶店取号机,讲透 ThreadLocalRandom 的核心

请添加图片描述
哇哥拉过白板,画了两家奶茶店的取号场景,王二一下就懂了:
在这里插入图片描述

🔥 核心原理:ThreadLocalRandom 的 “三不原则”

  • 不共用实例:每个线程都有自己的 ThreadLocalRandom 实例,存在 Thread 类的threadLocalRandom字段里,不用像 Random 那样抢着用;
  • 不抢种子:Random 靠一个全局种子生成随机数,线程抢着更新种子;ThreadLocalRandom 每个线程有独立种子,自己更自己的,互不干扰;
  • 不锁竞争:没有 CAS 和同步锁,生成随机数时直接操作自己的种子,速度飞起。

“简单说,Random 是‘公共厕所’,ThreadLocalRandom 是‘私人卫生间’,” 哇哥拍板,“高并发下,谁用公共的谁卡壳!”

三、改造代码:ThreadLocalRandom 让 QPS 翻 10 倍

请添加图片描述
哇哥手把手教王二改代码,只改了生成随机数的一行,结果 QPS 直接从 300 冲到 3000,重复订单号全没了。

📌 优化后的代码(ThreadLocalRandom 救场)

package cn.tcmeta.randoms;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author: laoren
 * @date: 2025/12/10 23:16
 * @description: // 优化版订单号生成器:ThreadLocalRandom秒杀不崩
 * @version: 1.0.0
 */
public class ThreadLocalRandomRescueSample {
    // 线程局部序列号
    private static final ThreadLocal<Integer> sequence = ThreadLocal.withInitial(() -> 0);
    private static final AtomicInteger duplicateCount = new AtomicInteger(0);
    private static final Set<String> ORDER_SET = Collections.synchronizedSet(new HashSet<>());

    // 改进的订单号生成方法:时间戳+线程标识+序列号
    public static String generateOrderNo() {
        long timestamp = System.currentTimeMillis();
        long threadId = Thread.currentThread().threadId();
        int seq = sequence.get() % 1000;
        sequence.set(sequence.get() + 1);
        return timestamp + String.format("%03d%03d", threadId % 1000, seq);
    }

    // 正确的主函数入口
    static void main() throws InterruptedException {
        // 同样100个线程,每个线程生成100个订单号
        int threadCount = 100;
        ExecutorService threadPool = Executors.newFixedThreadPool(threadCount);
        CountDownLatch latch = new CountDownLatch(threadCount);

        long start = System.currentTimeMillis();
        for (int i = 0; i < threadCount; i++) {
            threadPool.submit(() -> {
                try {
                    for (int j = 0; j < 100; j++) {
                        String orderNo = generateOrderNo();
                        boolean added = ORDER_SET.add(orderNo);
                        if (!added) {
                            duplicateCount.incrementAndGet();
                            System.out.println("警告:出现重复订单号!线程[" +
                                    Thread.currentThread().getName() + "] 订单号=" + orderNo);
                        }
                    }
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        long end = System.currentTimeMillis();
        threadPool.shutdown();

        System.out.println("总耗时:" + (end - start) + "ms");
        System.out.println("重复订单号数量:" + duplicateCount.get());
        System.out.println("生成订单号总数:" + ORDER_SET.size());
    }
}
  • 运行结果
总耗时:74ms
重复订单号数量:0
生成订单号总数:10000

王二盯着屏幕直呼卧槽:“耗时从 1280ms 降到 86ms,重复率直接归零!这 ThreadLocalRandom 也太顶了吧?”

“这还只是 100 线程,” 哇哥挑眉,“上次秒杀 1000 线程压测,用 Random 直接超时,换 ThreadLocalRandom 后 QPS 稳在 10 万,比原来翻了 10 倍都多。”

四、ThreadLocalRandom 的正确姿势(王二记小本本)

哇哥怕王二再踩坑,把 ThreadLocalRandom 的使用要点总结成 “三招鲜”:

👉 获取实例:用 current (),别 new!

// 正确:获取当前线程的实例(每个线程唯一)
ThreadLocalRandom random = ThreadLocalRandom.current();
// 错误:千万别new,new出来的和Random没区别,还浪费资源
ThreadLocalRandom wrongRandom = new ThreadLocalRandom();

📌 生成随机数:指定范围更灵活

比 Random 的 nextInt 更方便,直接指定起止范围,不用自己算差值:

ThreadLocalRandom random = ThreadLocalRandom.current();
int num1 = random.nextInt(10); // 0-9(和Random一样)
int num2 = random.nextInt(10, 20); // 10-19(Random要写nextInt(10)+10,容易错)
long num3 = random.nextLong(1000L); // 生成long型随机数(Random也有,但ThreadLocalRandom更快)
double num4 = random.nextDouble(0.5, 1.0); // 0.5-1.0的小数(Random做不到这么方便)

🔥 并发场景必用,单线程随意

  • 单线程场景:Random 和 ThreadLocalRandom 性能差不多,用哪个都行;
  • 多线程场景(线程池、并发请求):必须用 ThreadLocalRandom,性能差 10 倍以上;
  • 禁止场景:别用 ThreadLocalRandom 生成加密随机数(比如验证码),要用 SecureRandom,不然有安全风险。

五、总结:ThreadLocalRandom 核心心法(王二编顺口溜)

王二把核心知识点编成顺口溜,贴在显示器上:

  • 并发不用 Random,ThreadLocalRandom 才叫神;
  • 实例别 new 用 current,线程独立无竞争;
  • 范围直接传俩数,不用计算不糊涂;
  • 加密场景要记牢,SecureRandom 不能少。

📢 哇哥的血泪彩蛋

“我刚工作时,用 Random 写支付系统的流水号生成,” 哇哥捂脸,“上线第一天就出现 3 笔重复流水,财务追着我改了一下午 —— 后来换成 ThreadLocalRandom,再也没出过错。现在面试新人,我必问‘Random 和 ThreadLocalRandom 的区别’,答不上来的,并发这块基本是小白!”

关注我,下一篇咱们扒一扒 ThreadLocalRandom 的底层源码 —— 为什么它能做到每个线程独立实例?和 ThreadLocal 有啥关系?让你不仅会用,还能讲透底层,面试时直接碾压面试官!

在这里插入图片描述
在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值