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