综合实时java案例,比分还会改写吗?

wen java案例 2

本文目录导读:

综合实时java案例,比分还会改写吗?

  1. 场景设定
  2. 第一步:错误的实现(比分会被意外“改写”/丢失)
  3. 第二步:正确实现(比分不会错误改写,且实时可见)
  4. 第三步:为什么“比分还会改写”?—— 深入技术解析
  5. 第四个实际生产要点:如何实现“实时性”?
  6. 总结回答你的问题

这是一个非常经典且具有深度的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),表现就是“比分被改写了”。

如果加了synchronizedReentrantLock 比分确实不会写错,但锁是阻塞式的,在高并发实时数据源下(比如每秒几百次事件),性能差,且有可能造成线程阻塞等待,无法实时推送。

如果用了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架构中,比分不仅不会被改写,而且每一次进球都会在几十毫秒内精准地推送到千万观众屏幕上。

抱歉,评论功能暂时关闭!