导读:本期聚焦于小伙伴创作的《如何用Java并发编程构建一个部门级线程安全的排队取号系统?》,敬请观看详情。在行政或客服部门现场,多人同时取号常出现重号或跳号。根本原因在于共享计数器未受同步控制。采用AtomicLong做原子自增可彻底消除竞态,再配合ConcurrentHashMap维护业务窗口状态,系统能在高并发下保持序号连续与状态一致。本文给出完整实现,说明为何volatile单独使用不够,以及如何用锁分段降低争用,帮助你用标准库组件搭出稳定可用的取号服务。

部门级排队取号系统看似简单,实则对并发正确性要求很高。多个终端或自助机同时点击取号,后台必须保证每次返回的号码严格递增且不重复,同时能查询各业务窗口的当前叫号状态。如果只用普通int变量加一,在多线程下必然出现丢失更新。下面从原子类、并发容器到整体结构,完整说明如何用Java标准并发工具构建这样一个系统。

如何用Java并发编程构建一个部门级线程安全的排队取号系统?

一、为什么普通计数器不行

最直观的想法是维护一个静态int字段,每次取号时number++后返回。但在多核CPU上,自增不是原子操作,它包含读取、加一、写回三步。线程A读到第10号,线程B也读到第10号,各自加一写回,结果两个客户都拿到11号,而12号被跳过。这种竞态条件在并发量稍高时就必然发生。

有人尝试用volatile修饰计数器,认为可见性可以保证安全。其实volatile只保证线程看到最新值,不保证读改写过程的原子性。如下代码仍然错误:

public class WrongTicket {
    private volatile int num = 0;
    public int next() {
        // 非原子操作,并发下会重号
        return ++num;
    }
}

因此必须使用具备原子性的机制,例如synchronized方法,或性能更好的原子变量类。

二、用AtomicLong实现安全发号

java.util.concurrent.atomic包下的AtomicLong提供了compareAndSet为核心的原子自增方法。incrementAndGet内部使用CPU级CAS指令,多线程同时调用也能保证全局序号连续。下面的发号器只做一件事:永远返回唯一递增的长整型号码。

import java.util.concurrent.atomic.AtomicLong;

public class TicketDispenser {
    private final AtomicLong seq = new AtomicLong(0);

    // 返回从1开始的排队号
    public long takeNumber() {
        return seq.incrementAndGet();
    }

    public long current() {
        return seq.get();
    }
}

上述代码在每秒数千次取号压力下也不会重号。相比synchronized,AtomicLong避免了线程挂起与唤醒开销,适合单纯计数场景。如果部门业务还包含根据号码反查信息,就需要并发容器配合。

要注意AtomicLong的初值与业务对齐。比如上班前已发出过纸质号,可从指定值初始化:new AtomicLong(lastPaperNum)。这样线上线下号码空间不冲突。

三、用ConcurrentHashMap管理窗口状态

部门通常有多个窗口,如“开户”“咨询”“投诉”。每个窗口当前服务的号码需要被前台和取号机实时读取。ConcurrentHashMap支持并发读写且不锁全表,非常适合保存窗口名到当前号的映射。

import java.util.concurrent.ConcurrentHashMap;

public class WindowBoard {
    private final ConcurrentHashMap<String, Long> windows = new ConcurrentHashMap<>();

    public void callNext(String window, long num) {
        windows.put(window, num);
    }

    public Long nowServing(String window) {
        return windows.get(window);
    }

    public java.util.Set<String> windowNames() {
        return windows.keySet();
    }
}

由于ConcurrentHashMap的put和get都是线程安全且高效的,多个终端更新不同窗口时几乎无争用。若某窗口暂未叫号,get返回null,前端可显示“等待中”。

如果业务要求“仅当窗口空闲才允许叫号”,可借助putIfAbsent或compute原子方法,避免先查后改的间隙出错。这体现了并发容器相较手动同步的优势。

四、整合为部门级取号服务

将发号器与窗口板组合,就得到完整服务。取号机调用takeNumber拿号,前台调用callNext推进窗口。以下示例展示一个简化但可直接运行的后台组件。

public class DepartmentQueue {
    private final TicketDispenser dispenser = new TicketDispenser();
    private final WindowBoard board = new WindowBoard();

    public long takeTicket() {
        return dispenser.takeNumber();
    }

    public void serve(String window, long ticket) {
        board.callNext(window, ticket);
    }

    public Long currentTicketOf(String window) {
        return board.nowServing(window);
    }

    public long lastIssued() {
        return dispenser.current();
    }
}

该结构无锁竞争热点:发号走AtomicLong,窗口状态走分段哈希,两者皆为高并发设计。实际部署时,DepartmentQueue可做成单例,由Web接口或Swing终端共用。

若部门规模扩大至跨楼层,可把AtomicLong换成分布式序号服务,ConcurrentHashMap换成Redis哈希,但本地并发模型思想一致:原子发号加并发安全状态表。

五、常见误区与优化建议

误区一是用Random生成号企图避冲突,结果出现跳号且难排查。误区二是给整个取号方法加synchronized,虽然正确但所有终端串行取号,高峰期排队的不是客户而是内部线程。用原子类能将热点缩小到一条指令。

优化上,如果号码需附带时间或部门前缀,可用String.format拼接,拼接本身在取号线程做,不影响原子发号。日志建议异步落盘,避免IO拖慢CAS循环。经过上述设计,一个普通Java应用即可稳稳支撑部门级排队取号。

Java并发编程线程安全排队取号系统修改时间:2026-08-02 12:30:36

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。