讲师中心 微信公众号
AI工具推荐 视频效率加速

Java并发编程:构建部门级线程安全的排队取号系统

夜伟姑娘_2348

夜伟姑娘_2348

发布时间:2025-11-20 17:31:33

|

475人浏览过

|

来源于php中文网

原创

Java并发编程:构建部门级线程安全的排队取号系统

本文深入探讨如何在java中构建一个高效且线程安全的排队取号系统。针对多部门并发取号的需求,文章提出了一种基于`concurrenthashmap`的部门级同步方案,避免了全局锁带来的性能瓶颈,确保了同一部门内的顺序性,同时允许不同部门间并行操作,从而优化系统并发性能。文章还强调了数据库层面并发控制的重要性,以提供端到端的数据一致性。

引言:并发取号系统的挑战

在许多业务场景中,例如银行、医院或服务中心,都需要一个排队取号系统来管理客户的顺序。当多个用户(或线程)同时请求获取下一个号码时,系统必须确保每个号码只能被分配一次,避免重复分配或跳号。更复杂的场景是,系统可能服务于多个部门,而不同部门的取号流程可以并行进行,但同一部门内的取号操作必须是严格顺序的。直接对整个取号函数进行同步(例如使用synchronized关键字修饰方法)虽然能保证线程安全,但会严重限制系统的并发能力,导致不同部门之间的操作也相互阻塞,从而降低整体性能。

传统同步方案的局限性

考虑以下简化版的取号函数伪代码:

private DailyTurns callTurnLocal(int userId) {
    try {
        DailyTurns turn = null;
        // 1. 从数据库获取下一个可用号码
        turn = getNextTurnForUser(userId); // 假设这里也需要部门ID

        if (turn != null) {
            // 2. 更新号码状态为“已叫号”,并分配给用户
            turn.setTurnStatusId(TURN_STATUS_CALLED);
            turn.setEventDate(new Date());
            turn.setUserId(userId);

            // 3. 保存更新到数据库
            turn = save(turn);
        }
        return turn;
    } catch (Exception e) {
        // 异常处理
        return null;
    }
}

如果直接将callTurnLocal方法声明为synchronized,那么在任何时刻,只有一个线程能够进入该方法。这意味着即使来自不同部门的两个用户同时请求取号,它们也必须排队等待,这显然不符合“不同部门可并行”的需求。为了实现部门级的并发,我们需要一种更细粒度的同步机制。

基于ConcurrentHashMap的部门级锁策略

为了解决上述问题,我们可以利用ConcurrentHashMap来为每个部门维护一个独立的锁对象。当一个线程需要为某个部门取号时,它会尝试获取该部门对应的锁。这样,同一部门的请求会因为争抢同一个锁而串行执行,而不同部门的请求则可以同时获取各自的锁,从而实现并行处理。

立即学习“Java免费学习笔记(深入)”;

Alibabacloud Sdk Client Initialization For Java
Alibabacloud Sdk Client Initialization For Java

在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。

下载

核心思想:

  1. 创建一个ConcurrentHashMap<Integer, Object>,其中键是部门ID(departmentId),值是用于该部门同步的任意Object实例。
  2. 当需要为某个部门执行取号操作时,首先从ConcurrentHashMap中获取或创建一个与该部门ID关联的锁对象。
  3. 使用synchronized关键字配合这个部门特定的锁对象,来包裹取号的核心逻辑。

实现细节与示例代码

首先,定义一个简单的DailyTurns实体类和TurnStatus常量,用于模拟业务数据:

import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 模拟排队号码实体
class DailyTurns {
    private int turnId;
    private int departmentId;
    private int userId;
    private int turnStatusId;
    private Date eventDate;

    public DailyTurns(int turnId, int departmentId) {
        this.turnId = turnId;
        this.departmentId = departmentId;
    }

    // Getters and Setters (省略部分代码,实际应用中需完整)
    public int getTurnId() { return turnId; }
    public void setTurnId(int turnId) { this.turnId = turnId; }
    public int getDepartmentId() { return departmentId; }
    public void setDepartmentId(int departmentId) { this.departmentId = departmentId; }
    public int getUserId() { return userId; }
    public void setUserId(int userId) { this.userId = userId; }
    public int getTurnStatusId() { return turnStatusId; }
    public void setTurnStatusId(int turnStatusId) { this.turnStatusId = turnStatusId; }
    public Date getEventDate() { return eventDate; }
    public void setEventDate(Date eventDate) { this.eventDate = eventDate; }

    @Override
    public String toString() {
        return "DailyTurns{" +
               "turnId=" + turnId +
               ", departmentId=" + departmentId +
               ", userId=" + userId +
               ", turnStatusId=" + turnStatusId +
               ", eventDate=" + eventDate +
               '}';
    }
}

// 模拟号码状态常量
class TurnStatus {
    public static final int TURN_STATUS_PENDING = 1; // 待叫号
    public static final int TURN_STATUS_CALLED = 2;  // 已叫号
    // ... 其他状态
}

接下来是包含部门级同步逻辑的取号服务类:

public class TurnService {
    private static final Logger logger = LoggerFactory.getLogger(TurnService.class);

    // 使用ConcurrentHashMap存储每个部门的锁对象
    // 键是部门ID,值是用于同步的任意Object实例
    private final ConcurrentHashMap<Integer, Object> departmentLocks = new ConcurrentHashMap<>();

    // 模拟从数据库获取下一个可用号码
    // **重要提示:此方法内部仍需数据库层面的并发控制,详见下文**
    private DailyTurns getNextTurnForDepartment(int departmentId) {
        // 模拟数据库查询耗时
        try {
            Thread.sleep((long) (Math.random() * 50)); 
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.warn("Thread interrupted while simulating DB latency.", e);
        }

        // 实际场景中,这里会查询数据库,获取该部门下一个未被叫号的号码
        // 并可能需要使用 SELECT ... FOR UPDATE 等数据库锁机制来预留号码
        // 这里简化为生成一个模拟的号码
        int nextTurnId = generateNextDummyTurnId(departmentId); // 模拟生成一个号码
        if (nextTurnId > 0) {
            return new DailyTurns(nextTurnId, departmentId);
        }
        return null;
    }

    // 模拟生成下一个号码ID(非线程安全,仅用于演示应用层逻辑)
    private static final ConcurrentHashMap<Integer, Integer> departmentTurnCounters = new ConcurrentHashMap<>();
    private int generateNextDummyTurnId(int departmentId) {
        return departmentTurnCounters.compute(departmentId, (k, v) -> (v == null ? 1 : v + 1));
    }

    // 模拟将更新后的号码信息保存到数据库
    private DailyTurns save(DailyTurns turn) {
        // 实际中会将更新后的DailyTurns对象保存到数据库
        logger.info("保存叫号信息: " + turn);
        return turn;
    }

    /**
     * 为指定用户和部门获取下一个号码。
     * 该方法实现了部门级的线程安全。
     *
     * @param userId 用户ID
     * @param departmentId 部门ID
     * @return 获取到的号码信息,如果失败则返回null
     */
    public DailyTurns callTurnLocal(int userId, int departmentId) {
        DailyTurns resultTurn = null;

        // 获取或创建该部门的锁对象。
        // computeIfAbsent 是线程安全的,确保每个 departmentId 只有一个锁对象被创建。
        // 如果 departmentId 对应的锁不存在,则会创建一个新的 Object 实例作为锁。
        Object departmentLock = departmentLocks.computeIfAbsent(departmentId, k -> new Object());

        // 对该部门的锁对象进行同步
        synchronized (departmentLock) {
            try {
                DailyTurns fetchedTurn = null;

                // 1. 获取下一个号码。
                // 这一步在应用层已经由 departmentLock 保证了同一部门的串行访问。
                // 但仍需注意数据库层面的并发控制。
                fetchedTurn = getNextTurnForDepartment(departmentId);

                if (fetchedTurn != null) {
                    resultTurn = fetchedTurn;

                    // 2. 更新号码状态和用户信息
                    resultTurn.setTurnStatusId(TurnStatus.TURN_STATUS_CALLED);
                    resultTurn.setEventDate(new Date());
                    resultTurn.setUserId(userId);

                    // 3. 保存更新到数据库
                    resultTurn = save(resultTurn);
                }

                return resultTurn;
            } catch (Exception e) {
                logger.error("为部门 " + departmentId + " 取号时发生异常: " + e.getMessage(), e);
                return null;
            }
        }
    }

    // 示例主方法,模拟多线程并发取号
    public static void main(String[] args) throws InterruptedException {
        TurnService service = new TurnService();
        // 模拟15个用户,分别来自部门1、2、3
        for (int i = 0; i < 15; i++) {
            final int user = i + 1;
            final int dept = (i % 3) + 1; // 部门ID在1, 2, 3之间循环
            new Thread(() -> {
                DailyTurns turn = service.callTurnLocal(user, dept);
                if (turn != null) {
                    System.out.println("用户 " + user + " (部门 " + dept + ") 成功获取号码: " + turn.getTurnId());
                } else {
                    System.out.println("用户 " + user + " (部门 " + dept + ") 未能获取号码。");
                }
            }).start();
        }
        // 等待所有线程完成,以便观察输出
        Thread.sleep(2000); 
    }
}

在上述代码中,departmentLocks.computeIfAbsent(departmentId, k -> new Object()) 是关键。它会原子性地检查ConcurrentHashMap中是否存在departmentId对应的锁对象。如果存在,则返回该对象;如果不存在,则创建一个新的Object实例并放入map中,然后返回。这样,对于同一个departmentId,所有线程都会获取到并同步在同一个Object实例上,从而保证了该部门取号操作的串行性。

数据库层面的并发控制

尽管应用层面的ConcurrentHashMap锁机制可以保证同一部门内的取号逻辑串行执行,但它并不能完全替代数据库层面的并发控制。尤其是在getNextTurnForDepartment方法中,从数据库查询并“锁定”下一个可用号码的操作,仍然可能面临并发问题,例如:

  1. SELECT 和 UPDATE 非原子性: 如果getNextTurnForDepartment仅执行SELECT查询

相关文章

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
切问学术

切问学术是一款AI论文写作工具,复旦大学NLP团队推出的AI学术智能体。

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

PixTV
PixTV Hot

PixTV是一款面向AIGC内容创作的AI视频生成工具。

讯飞绘文

讯飞绘文是一款由科大讯飞推出的一站式 AIGC 内容运营平台。

Laper
Laper Hot

Laper是专为编剧、导演和制片人推出的 AI 原生剧本创作工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

Atoms
Atoms Hot

Atoms是一款AI智能体工具,第一支自动构建真实业务的 AI 团队。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

相关专题

更多
java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

5864

2023.10.24

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

3698

2023.08.10

golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

470

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

323

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

493

2025.11.17

java判断map相关教程
java判断map相关教程

本专题整合了java判断map相关教程,阅读专题下面的文章了解更多详细内容。

263

2025.11.27

数据库三范式
数据库三范式

数据库三范式是一种设计规范,用于规范化关系型数据库中的数据结构,它通过消除冗余数据、提高数据库性能和数据一致性,提供了一种有效的数据库设计方法。本专题提供数据库三范式相关的文章、下载和课程。

2405

2023.06.29

如何删除数据库
如何删除数据库

删除数据库是指在MySQL中完全移除一个数据库及其所包含的所有数据和结构,作用包括:1、释放存储空间;2、确保数据的安全性;3、提高数据库的整体性能,加速查询和操作的执行速度。尽管删除数据库具有一些好处,但在执行任何删除操作之前,务必谨慎操作,并备份重要的数据。删除数据库将永久性地删除所有相关数据和结构,无法回滚。

3701

2023.08.14

LLVM自定义Pass怎么写
LLVM自定义Pass怎么写

本专题聚焦LLVM自定义Pass开发,整理Pass类结构、run()方法、PreservedAnalyses、CMake构建、插件注册、-load-pass-plugin加载和测试用例编写流程。

20

2026.09.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
dev.java 官方:Learn Java
dev.java 官方:Learn Java

共0课时 | 0人学习

Java JDBC数据库连接官方教程
Java JDBC数据库连接官方教程

共0课时 | 0人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn