我们用一个贴近生活的 "年会红包雨" 案例,手把手带你实现多线程抢红包逻辑 ------ 这个案例涵盖线程创建、线程安全、同步代码块等核心知识点。
目录
[步骤 1:准备红包池](#步骤 1:准备红包池)
[步骤 2:定义员工抢红包线程类](#步骤 2:定义员工抢红包线程类)
[步骤 3:启动 100 个员工线程](#步骤 3:启动 100 个员工线程)
一、案例需求:年会红包雨规则
某公司年会搞红包雨活动,规则对应多线程核心场景:
- 参与对象:100 名员工(对应 100 个独立线程);
- 红包总数:200 个;
- 红包金额规则:
- 小红包(1-30 元):占比 80%(共 160 个);
- 大红包(31-100 元):占比 20%(共 40 个);
- 核心逻辑:100 个线程同时抢 200 个红包,保证每个红包只能被抢一次,抢完即止。
二、案例核心知识点
| 知识点 | 作用 | 新手提示 |
|---|---|---|
| 继承 Thread 类创建线程 | 每个员工对应一个独立线程,实现并发抢红包 | 启动线程必须调用start(),不能直接调run() |
| 同步代码块(synchronized) | 解决多线程抢红包的 "数据错乱" 问题 | 锁对象必须是多个线程共享的对象 |
| ArrayList 集合 | 存储红包列表,通过remove()实现 "抢红包" |
ArrayList 本身非线程安全,需加锁保护 |
| Thread.sleep() | 模拟抢红包的延迟,让执行过程更易观察 | 休眠时不会释放锁,仅暂停线程执行 |
三、代码分步拆解:实现红包雨
我们把案例拆成 3 个核心步骤,逐行讲解,确保新手能看懂每一步的逻辑~
步骤 1:准备红包池
首先需要生成 200 个红包,按 80%/20% 的比例分配小红包和大红包,存入 List 作为 "红包池"。
java
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Test {
public static void main(String[] args) {
// 1. 生成200个红包,存入列表(红包池)
List<Integer> redPacketList = getRedPackets();
// 3. 创建100个员工线程,启动抢红包(步骤2先定义线程类)
for (int i = 1; i <= 100; i++) {
new PeopleGetRedPacket(redPacketList,"员工"+ i).start();
}
}
// 核心方法:生成200个红包
private static List<Integer> getRedPackets() {
Random random = new Random();
List<Integer> redPacketList = new ArrayList<>();
// 小红包:1-30元,160个(80%)
for (int i = 0; i < 160; i++) {
// random.nextInt(30)生成0-29,+1后是1-30
redPacketList.add(random.nextInt(30) + 1);
}
// 大红包:31-100元,40个(20%)
for (int i = 0; i < 40; i++) {
// random.nextInt(70)生成0-69,+31后是31-100
redPacketList.add(random.nextInt(70) + 31);
}
return redPacketList;
}
}
关键解释:
- Random.nextInt(n):生成
0 ~ n-1的随机整数,通过+1调整金额范围; - ArrayList 是 "红包池" 的载体,支持随机索引获取和删除元素,契合 "抢红包" 的逻辑。
步骤 2:定义员工抢红包线程类
每个员工对应一个线程,需要继承 Thread 类,重写**run()**方法实现抢红包逻辑 ------核心是解决线程安全问题(100 个线程同时操作同一个红包池,不加锁会出现 "重复抢红包""红包数量负数" 等问题)。
java
import java.util.List;
// 员工抢红包线程类:继承Thread
public class PeopleGetRedPacket extends Thread{
// 共享资源:所有线程共用同一个红包池
private List<Integer> redPackets;
// 构造器:传入红包池 + 设置线程名称(员工编号)
public PeopleGetRedPacket(List<Integer> redPackets, String name){
super(name); // 调用父类构造器,设置线程名
this.redPackets = redPackets;
}
@Override
public void run() {
String userName = Thread.currentThread().getName(); // 获取当前员工名称
while (true) {
// 同步代码块:加锁保护共享资源(红包池)
// 锁对象必须是所有线程共享的(这里用红包池),否则锁无效
synchronized (redPackets) {
// 终止条件:红包池为空,结束抢红包
if (redPackets.size() == 0) {
break;
}
// 随机获取一个红包索引(模拟"抢"的随机性)
int index = (int)(Math.random() * redPackets.size());
Integer money = redPackets.remove(index); // 删除红包=抢到红包
System.out.println(userName + "抢到红包:" + money + "元");
// 最后一个红包抢完,提示活动结束
if (redPackets.size() == 0) {
System.out.println("\n🎉 活动结束!所有红包已抢完~");
break;
}
}
// 模拟抢红包的延迟(比如点击、网络延迟),便于观察结果
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
核心重点(线程安全):
- synchronized (redPackets):给红包池加锁,确保同一时间只有 1 个线程能执行抢红包代码,避免 "多个线程同时删同一个红包";
- redPackets.remove(index):删除对应索引的红包,代表 "抢到"------ 这是操作共享资源的核心代码,必须放在同步代码块内;
- Thread.sleep(10):让线程休眠 10 毫秒,模拟真实抢红包的延迟,否则线程执行太快,结果一闪而过。
步骤 3:启动 100 个员工线程
回到 Test 类的 main 方法,循环创建 100 个员工线程(命名为 "员工 1" 到 "员工 100"),调用**start()**启动线程,抢红包逻辑就会并发执行。
四、完整源码
包结构
java
ThreadTest/
├─ Test.java // 主线程:生成红包+启动抢红包线程
└─ PeopleGetRedPacket.java // 员工抢红包线程类
Test.java
java
package ThreadTest;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Test {
public static void main(String[] args) {
// 1. 生成200个红包,存入红包池
List<Integer> redPacketList = getRedPackets();
// 2. 创建100个员工线程,启动抢红包
for (int i = 1; i <= 100; i++) {
new PeopleGetRedPacket(redPacketList,"员工"+ i).start();
}
}
// 生成200个红包:160个小红包(1-30) + 40个大红包(31-100)
private static List<Integer> getRedPackets() {
Random random = new Random();
List<Integer> redPacketList = new ArrayList<>();
// 小红包:80%占比
for (int i = 0; i < 160; i++) {
redPacketList.add(random.nextInt(30) + 1);
}
// 大红包:20%占比
for (int i = 0; i < 40; i++) {
redPacketList.add(random.nextInt(70) + 31);
}
return redPacketList;
}
}
PeopleGetRedPacket.java
java
package ThreadTest;
import java.util.List;
public class PeopleGetRedPacket extends Thread{
private List<Integer> redPackets;
public PeopleGetRedPacket(List<Integer> redPackets, String name){
super(name);
this.redPackets = redPackets;
}
@Override
public void run() {
String userName = Thread.currentThread().getName();
while (true){
// 同步代码块:保护共享资源,解决线程安全问题
synchronized (redPackets) {
if (redPackets.size() == 0){
break;
}
// 随机抢一个红包
int index = (int)(Math.random()*redPackets.size());
Integer money = redPackets.remove(index);
System.out.println(userName + "抢到红包:" + money + "元");
if (redPackets.size() == 0) {
System.out.println("\n🎉 活动结束!所有红包已抢完~");
break;
}
}
// 模拟延迟,让抢红包过程更真实
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
五、运行结果演示
运行 Test 类后,控制台会输出类似内容(多线程执行顺序随机,每次结果不同):
java
员工1抢到红包:15元
员工3抢到红包:88元
员工2抢到红包:22元
员工5抢到红包:5元
员工4抢到红包:99元
...
员工99抢到红包:28元
员工100抢到红包:76元
🎉 活动结束!所有红包已抢完~
六、核心知识点总结
- 多线程创建 :继承 Thread 类创建线程,每个线程对应一个员工,调用**start()启动(直接调run()**只是普通方法,不会并发执行);
- 线程安全 :多线程操作共享资源(红包池)时,必须用synchronized加锁,锁对象要保证所有线程共用(本例用红包池 List);
- 共享资源操作 :ArrayList 非线程安全,其**remove()**等修改操作必须放在同步代码块内,避免并发修改异常和数据错乱。