本文目录导读:

- 场景设定
- 第一步:错误的实现(比分会被意外“改写”/丢失)
- 第二步:正确实现(比分不会错误改写,且实时可见)
- 第三步:为什么“比分还会改写”?—— 深入技术解析
- 第四个实际生产要点:如何实现“实时性”?
- 总结回答你的问题
这是一个非常经典且具有深度的Java并发编程问题,为了给你一个“综合性实时”的答案,我们不仅要看代码逻辑,还要看业务场景和数据一致性要求。
直接回答:比分是否会改写,完全取决于你如何编写同步逻辑。 在错误的实现下,比分不仅会改写,还会出现严重的数据错乱(丢失更新)。
下面我用一个完整的实时足球比赛计分系统案例,从错误示范到正确方案,并融入实时推送(模拟WebSocket) 的场景来综合解答。
场景设定
我们有一个MatchScore(比赛比分)对象,多个线程(代表不同数据源:如主裁判哨声、边裁补时、技术台修正)同时更新比分。
第一步:错误的实现(比分会被意外“改写”/丢失)
import java.util.concurrent.*;
/**
* 场景:模拟实时比赛计分
* 问题:使用非原子操作,导致“比分丢失更新”(Lost Update)
*/
class BadMatchScore {
// 主队进球数,客队进球数
private int homeScore = 0;
private int awayScore = 0;
// 非同步的更新方法
public void homeGoal() {
// 这里不是原子操作:读取->修改->写回
int current = homeScore;
// 模拟网络延迟或CPU时间片切换
try { Thread.sleep(10); } catch (InterruptedException e) {}
homeScore = current + 1;
}
public void awayGoal() {
int current = awayScore;
try { Thread.sleep(10); } catch (InterruptedException e) {}
awayScore = current + 1;
}
public String getScore() {
return homeScore + " : " + awayScore;
}
}
public class LostUpdateDemo {
public static void main(String[] args) throws InterruptedException {
BadMatchScore score = new BadMatchScore();
int THREADS = 10;
// 模拟10个并发数据源,每个都尝试进一个球(主队或客队)
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
for (int i = 0; i < THREADS; i++) {
final int source = i;
executor.submit(() -> {
// 偶数线程提交主队进球,奇数线程提交客队进球(模拟并发)
if (source % 2 == 0) {
score.homeGoal();
} else {
score.awayGoal();
}
});
}
executor.shutdown();
executor.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("错误版本的最终比分:" + score.getScore());
// 预期结果是 5 : 5,但由于并发问题,实际可能远小于这个值,
// 2 : 3,部分进球被“覆盖”了(比分被改写成错误的值)。
}
}
运行结果(大概率):
错误版本的最终比分:1 : 3 (或者任何非5:5的数值)
比分被“改写”了,丢失了大量进球。
第二步:正确实现(比分不会错误改写,且实时可见)
我们采用原子变量(AtomicInteger) 或显式锁,并结合发布/订阅模式实现实时推送。
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 场景:正确的实时比分更新
* 核心:使用 CAS(Compare-And-Swap)保证原子性 + 回调接口实现实时推送
*/
interface ScoreChangeListener {
void onScoreChanged(int home, int away); // 实时回调
}
class LiveMatchScore {
// 使用原子类保证读改写操作的原子性,杜绝丢失更新
private final AtomicInteger homeScore = new AtomicInteger(0);
private final AtomicInteger awayScore = new AtomicInteger(0);
private final ScoreChangeListener listener;
public LiveMatchScore(ScoreChangeListener listener) {
this.listener = listener;
}
public void homeGoal() {
int newValue = homeScore.incrementAndGet(); // 原子自增,不会被覆盖
listener.onScoreChanged(newValue, awayScore.get()); // 推送最新比分
}
public void awayGoal() {
int newValue = awayScore.incrementAndGet();
listener.onScoreChanged(homeScore.get(), newValue);
}
// 如果存在“修正比分”需求(例如裁判改判),使用CAS来安全更新
public void correctScore(int expectedHome, int expectedAway, int updateHome, int updateAway) {
// 比较并交换:只有当前值等于期望值时才更新,否则丢弃或重试
boolean homeOk = homeScore.compareAndSet(expectedHome, updateHome);
boolean awayOk = awayScore.compareAndSet(expectedAway, updateAway);
if (homeOk && awayOk) {
listener.onScoreChanged(updateHome, updateAway);
System.out.println("[系统] 裁判改判,比分修正为 " + updateHome + " : " + updateAway);
} else {
System.out.println("[系统] 改判失败,最新比分已有变化,请重新确认");
}
}
}
public class CorrectDemo {
public static void main(String[] args) throws InterruptedException {
// 模拟WebSocket/实时UI推送(打印在控制台)
LiveMatchScore score = new LiveMatchScore((h, a) -> {
System.out.println("[实时推送] 当前比分: " + h + " : " + a);
});
int THREADS = 100; // 模拟更激烈的并发(50个主队球,50个客队球)
ExecutorService executor = Executors.newFixedThreadPool(16);
CountDownLatch latch = new CountDownLatch(THREADS);
for (int i = 0; i < THREADS; i++) {
final int source = i;
executor.submit(() -> {
try {
if (source % 2 == 0) {
score.homeGoal();
} else {
score.awayGoal();
}
} finally {
latch.countDown();
}
});
}
latch.await(); // 等待所有线程执行完毕
executor.shutdown();
System.out.println("\n===== 最终确认比分 =====");
// 这里为了演示,手动获取一次最终值
listener.onScoreChanged(直接打印最终值)— 实际上我们应该获取字段。
// 简化:直接调用getScore
// 因为Demo里没有getScore,我们打印最终可以通过监听器最后一条,但最好加一个查询方法。
// 为了展示,我重新写输出:
// 注意:真实项目中这里应该通过接口查询。
}
}
补充一个最终的查询方法:
为了准确打印最终比分,我们可以在LiveMatchScore中加入printFinalScore()方法:
public void printFinalScore() {
System.out.println("最终比分: " + homeScore.get() + " : " + awayScore.get());
}
在你main方法最后调用 score.printFinalScore(); 你会发现无论线程有多少,最终比分一定是精确的50:50,且比分会实时、有序地推送到终端。
第三步:为什么“比分还会改写”?—— 深入技术解析
如果不加锁/原子类:
homeScore++ 在JVM底层对应三条指令:LOAD, ADD, STORE。
线程A和线程B同时执行,都读取到主队比分是 10,然后A计算成11,B也计算成11,最后A写入11,B写入11。主队比分明明应该进2个球变成12,结果只有11 —— 这就是典型的丢失更新(Lost Update),表现就是“比分被改写了”。
如果加了synchronized或ReentrantLock:
比分确实不会写错,但锁是阻塞式的,在高并发实时数据源下(比如每秒几百次事件),性能差,且有可能造成线程阻塞等待,无法实时推送。
如果用了AtomicInteger(CAS):
底层使用了CPU的硬件同步原语(如cmpxchg),无锁、无阻塞、高效,当线程尝试incrementAndGet()时,如果比分已被其他线程改过,CAS会自旋重试,最终一定会把进球数加上去,所以比分永远不会被“吞掉”。
第四个实际生产要点:如何实现“实时性”?
在真实系统中(如体育直播App),除了保证比分正确,还需要毫秒级推送,这里推荐的是Actor模型或响应式流(Reactive Streams):
- Java 8+ 真实案例:使用
CompletableFuture+ConcurrentHashMap管理订阅者。 - 框架案例:
Netty(TCP长连接)+Protobuf(数据结构)+Jetty/WebSocket(HTTP长连接)。
模拟实时推送的伪代码片段:
private final ConcurrentHashMap<String, BroadcastSession> sessions = new ConcurrentHashMap<>();
public void broadcast(int home, int away) {
sessions.forEach((id, session) -> {
session.sendMessage(JsonFormat(ScoreEvent.builder()...)); // 异步IO非阻塞
});
}
配合上面的 AtomicInteger,保证比分算得准,推得快。
总结回答你的问题
- 比分还会改写吗?
- 如果你用非线程安全的代码(如裸的
int+),一定会改写,且数据会错乱。 - 如果你用原子类或锁,永远不会被错误改写,最终比分一定是所有数据源的精确总和。
- 如果你用非线程安全的代码(如裸的
- 如何做?
- 数据正确性:
AtomicInteger/LongAdder/synchronized。 - 实时推送:基于事件的观察者模式,在比分变化后立刻发布新比分。
- 在生产环境中,配合
Redis分布式锁(数据在服务端)或ZooKeeper来实现集群下的比分一致性。
- 数据正确性:
在专业的实时Java架构中,比分不仅不会被改写,而且每一次进球都会在几十毫秒内精准地推送到千万观众屏幕上。