资讯动态

面向对象与多线程综合实验:档案管理系统并发与持久化实战

发布时间:2026/10/13 16:26:10 来源:尧图企业网站定制
简介这份资源是武汉理工大学面向对象与多线程综合实验的档案管理系统完整项目面向计算机相关专业学生及需要巩固OOP与并发编程的开发者。项目围绕系统管理员、档案管理员和普通用户三种角色展开涵盖用户注册登录、权限控制、档案增删改查、条件搜索、日志记录、多线程性能优化与异常处理等模块并以七次迭代方式逐步完善适合作为课程设计参考或Java进阶练手案例。压缩包共432个文件以294个class字节码、96个java源码、28个xml配置、7个iml工程文件及少量jar依赖为主整体约3.21MB源码与编译产物齐全便于直接导入IDE运行调试。目前已有2213人学习下载。读者可从中获取分层清晰的类设计思路、继承与聚合关系的落地写法、synchronized同步控制与多线程并发处理方案以及完整的需求分析到测试维护流程对理解面向对象建模和线程安全实践具有较高参考价值。1. 从一份“档案管理系统”实验拆起它到底练什么如果你正在搜“面向对象与多线程综合实验”大概率不是想听人复述课本里“封装继承多态”的定义而是手里压着一个必须交的课程设计或者想找一个能跑通、能改、能讲清楚原理的参考实现。这份档案管理系统就是典型的综合实验产物它把面向对象的类设计、文件持久化、多线程并发访问、以及一个能操作的命令行或图形界面揉在一个项目里。适合谁适合刚学完 Java 或 C 语法、但对“怎么把零散知识拼成一个完整系统”没概念的人也适合已经工作、想回头补并发编程基本功的从业者。它解决的不是“档案管理”这个业务本身而是让你亲手处理对象建模、线程安全、资源竞争这些真实开发里绕不开的问题。我拆过不少这类实验包最深的感受是能跑通只是及格线能说清每个锁为什么加、每个类为什么这样拆才算真正吃透。2. 面向对象建模档案、用户、操作日志怎么拆类2.1 为什么先定实体类再谈功能很多人拿到题目就急着写菜单和增删改查结果写到一半发现字段对不上、权限判断散落各处。常见做法是先把系统里“名词”拎出来档案Archive、用户User、操作日志LogEntry。档案是核心实体包含编号、名称、类别、密级、创建时间、状态等字段用户分管理员和普通操作员决定能看什么、能改什么操作日志记录谁在什么时候动了哪条档案方便追溯。这三个类的关系是用户产生日志日志关联档案。把这三者定清楚后面的增删改查和并发控制才有落脚点。2.2 实体类与权限枚举的代码骨架下面用 Java 写一个最小可用的实体层字段和枚举都按实验常见要求来。注意Archive里没有直接放User对象而是放creatorId避免对象图过深导致序列化麻烦。// 档案实体实现 Serializable 方便后续文件持久化 public class Archive implements Serializable { private static final long serialVersionUID 1L; private String id; // 档案编号唯一 private String name; // 档案名称 private String category; // 类别人事、财务、技术等 private int secrecyLevel; // 密级1-公开 2-内部 3-机密 private long createTime; // 创建时间戳 private String creatorId; // 创建者用户ID private boolean deleted; // 软删除标记 // 构造、getter、setter 省略实际实验里必须补全 } // 用户角色枚举权限判断靠它而不是散落的 if-else public enum Role { ADMIN, OPERATOR, GUEST } // 操作日志只追加不修改 public class LogEntry implements Serializable { private String userId; private String archiveId; private String action; // CREATE / UPDATE / DELETE / QUERY private long timestamp; }逻辑说明Archive实现Serializable是为了后面用对象流直接写文件省去手写 CSV 的字段对齐麻烦。secrecyLevel用 int 而不是枚举是因为实验里常要求按数值比较大小做过滤。deleted做软删除避免物理删除后日志找不到关联档案。参数上serialVersionUID必须显式声明否则你改一次类结构之前存的文件就读不回来了这是血泪经验。2.3 服务层怎么组织才不乱实体定完别把所有逻辑塞进一个Main类。常见做法是拆出ArchiveService、UserService、LogService每个服务只操作自己的实体跨实体的动作比如删除档案同时写日志放在一个Facade或控制器里协调。这样多线程加锁时锁的粒度也好控制——锁服务对象而不是锁整个系统。我一般会先画一张类图标出哪些方法是读、哪些是写写方法才需要同步读方法尽量无锁否则并发性能会很难看。3. 多线程并发读写锁、线程池与文件持久化的配合3.1 为什么档案系统天然适合练并发档案管理有个特点查多写少。普通操作员频繁检索档案管理员偶尔新增或修改。如果直接用synchronized把整个服务方法锁死查询也会被阻塞并发吞吐量直线下降。所以这个实验真正想让你体会的是读写锁ReentrantReadWriteLock的适用场景读读不互斥读写互斥写写互斥。另一个练点是文件持久化——多个线程同时写同一个文件必然乱套必须有一个单线程的写队列或者加锁的写入方法。3.2 用读写锁保护档案集合下面这段代码演示服务层如何用读写锁包裹核心的Map并配合线程池处理并发请求。import java.util.concurrent.*; import java.util.concurrent.locks.*; import java.util.*; public class ArchiveService { private final MapString, Archive store new HashMap(); private final ReadWriteLock lock new ReentrantReadWriteLock(); private final Lock readLock lock.readLock(); private final Lock writeLock lock.writeLock(); // 查询加读锁允许多个线程同时进入 public Archive getById(String id) { readLock.lock(); try { Archive a store.get(id); return (a ! null !a.isDeleted()) ? a : null; } finally { readLock.unlock(); // 必须放 finally否则异常时锁泄漏 } } // 新增加写锁独占 public boolean add(Archive a) { writeLock.lock(); try { if (store.containsKey(a.getId())) return false; store.put(a.getId(), a); return true; } finally { writeLock.unlock(); } } // 按密级过滤读操作 public ListArchive listBySecrecy(int maxLevel) { readLock.lock(); try { ListArchive result new ArrayList(); for (Archive a : store.values()) { if (!a.isDeleted() a.getSecrecyLevel() maxLevel) { result.add(a); } } return result; } finally { readLock.unlock(); } } }逻辑说明readLock和writeLock来自同一个ReentrantReadWriteLock这是关键——它们互斥关系由同一个锁对象维护。getById和listBySecrecy只读加读锁add写加写锁。finally里解锁是铁律我见过太多人把unlock写在try块末尾一旦中间抛异常锁永远不释放程序直接假死。参数上maxLevel用来做密级过滤普通操作员传 2管理员传 3这样权限控制就落在查询条件里而不是在界面层做判断。3.3 线程池与异步日志写入日志写入是典型的“慢操作”如果每次增删改都同步写文件主线程会被磁盘 IO 拖住。常见做法是搞一个单线程的ExecutorService专门消费日志队列。public class LogService { private final BlockingQueueLogEntry queue new LinkedBlockingQueue(1000); private final ExecutorService writer Executors.newSingleThreadExecutor(); public LogService() { writer.submit(() - { while (!Thread.currentThread().isInterrupted()) { try { LogEntry entry queue.take(); // 队列空时阻塞不占 CPU appendToFile(entry); // 实际写文件 } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); } public void log(String userId, String archiveId, String action) { LogEntry e new LogEntry(userId, archiveId, action, System.currentTimeMillis()); if (!queue.offer(e)) { // 队列满时降级为同步写防止丢日志 appendToFile(e); } } private void appendToFile(LogEntry e) { // 用 BufferedWriter 追加写具体实现省略 } }逻辑说明LinkedBlockingQueue容量设 1000是防止内存无限增长。writer是单线程池保证写文件顺序和线程安全。queue.take()在队列空时阻塞不会空转浪费 CPU。offer返回 false 说明队列满了这时候降级为同步写宁可慢一点也不能丢日志——这是踩过坑之后的习惯。参数上队列容量和线程数要根据实验规模调一般课程设计 1000 足够生产环境得看日志量。4. 文件持久化与序列化对象怎么落盘、怎么读回来4.1 序列化方案的选择理由档案数据要持久化常见三种做法纯文本 CSV、JSON、Java 原生序列化。CSV 可读性好但字段一多就容易错位JSON 通用但需要引入第三方库有些实验环境不让用Java 序列化最省事直接ObjectOutputStream写对象但缺点是文件是二进制、跨语言读不了、类结构一改就废。这个实验里我一般推荐序列化因为重点在并发和面向对象不是数据交换格式。但必须注意所有要落盘的类都得实现Serializable且serialVersionUID要写死。4.2 带版本校验的读写工具类import java.io.*; public class StoreUtil { private static final String DATA_FILE archives.dat; // 保存整个 Map 到文件调用方需保证没有并发写 public static void save(MapString, Archive store) throws IOException { try (ObjectOutputStream oos new ObjectOutputStream(new BufferedOutputStream( new FileOutputStream(DATA_FILE)))) { oos.writeObject(store); oos.flush(); } } SuppressWarnings(unchecked) public static MapString, Archive load() throws IOException, ClassNotFoundException { File f new File(DATA_FILE); if (!f.exists()) return new java.util.HashMap(); try (ObjectInputStream ois new ObjectInputStream(new BufferedInputStream( new FileInputStream(f)))) { return (MapString, Archive) ois.readObject(); } } }逻辑说明BufferedOutputStream包在FileOutputStream外面减少磁盘 IO 次数。try-with-resources保证流一定关闭。load里先判断文件是否存在不存在返回空 Map避免第一次启动就抛异常。参数上DATA_FILE路径建议放在项目根目录别用绝对路径否则换台机器就找不到文件。注意save方法本身不加锁调用方必须在写锁保护下调用否则多线程同时保存会写出损坏文件。4.3 启动时加载与关闭时保存的时机系统启动时调一次load把数据灌进ArchiveService的store关闭时调一次save。但如果是图形界面用户可能直接点窗口关闭按钮所以要在窗口监听器里触发保存。命令行版本则用Runtime.getRuntime().addShutdownHook注册钩子。我见过有人每次增删改都全量保存数据量一大就卡死正确做法是定期保存或关闭时保存中间靠内存维护。5. 避坑与排查并发和持久化最容易翻车的几个点5.1 现象程序跑一会儿就卡死CPU 不高但没响应原因锁泄漏。某个线程在try块里抛了异常unlock没执行后续线程全部阻塞。解决所有lock()后面紧跟tryunlock()必须放finally。用jstack看线程状态如果大量线程停在WAITING且持有锁的线程已经死了基本就是这个问题。5.2 现象读出来的档案全是 null 或者抛 ClassNotFoundException原因序列化时类的包名或字段变了或者serialVersionUID不一致。解决固定serialVersionUID改类结构时要么兼容旧字段要么删掉旧数据文件重新生成。别指望二进制序列化能自动兼容它没这个能力。5.3 现象日志文件里偶尔出现半行乱码原因多个线程同时写同一个文件或者写的时候没 flush。解决日志写入必须单线程化用第 3 章那个队列方案。如果坚持多线程写至少每个线程持有独立的BufferedWriter并加文件锁但那样性能更差。5.4 现象查询结果里出现了已删除的档案原因软删除标记判断漏了或者读锁没加导致读到写了一半的状态。解决所有对外返回档案的方法都要检查deleted字段写操作加写锁读操作加读锁别偷懒用ConcurrentHashMap就以为万事大吉——它只能保证单个操作原子复合判断还是会出问题。5.5 现象程序正常关闭后数据没保存原因关闭钩子没注册或者图形界面直接System.exit跳过了保存逻辑。解决统一在finally或 shutdown hook 里调save并且保存前先停止所有写线程等队列排空。6. 进阶技巧用压力测试验证你的锁到底有没有用写完并发代码怎么知道锁加对了别靠感觉写个小压力测试。下面这段代码模拟 20 个读线程和 5 个写线程同时跑统计耗时和结果一致性。import java.util.concurrent.*; import java.util.concurrent.atomic.*; public class StressTest { public static void main(String[] args) throws Exception { ArchiveService service new ArchiveService(); // 预置 100 条档案 for (int i 0; i 100; i) { Archive a new Archive(); a.setId(A i); a.setSecrecyLevel(i % 3 1); service.add(a); } int readers 20, writers 5, loops 1000; ExecutorService pool Executors.newFixedThreadPool(readers writers); CountDownLatch latch new CountDownLatch(readers writers); AtomicInteger errors new AtomicInteger(); // 读线程反复查询检查返回对象不为 null 且密级合法 for (int i 0; i readers; i) { pool.submit(() - { try { for (int j 0; j loops; j) { Archive a service.getById(A (j % 100)); if (a ! null a.getSecrecyLevel() 1) errors.incrementAndGet(); } } finally { latch.countDown(); } }); } // 写线程反复新增和软删除 for (int i 0; i writers; i) { final int id i; pool.submit(() - { try { for (int j 0; j loops; j) { Archive a new Archive(); a.setId(W id _ j); a.setSecrecyLevel(1); service.add(a); } } finally { latch.countDown(); } }); } long start System.currentTimeMillis(); latch.await(); pool.shutdown(); System.out.println(耗时: (System.currentTimeMillis() - start) ms); System.out.println(异常数: errors.get()); } }逻辑说明CountDownLatch让主线程等所有任务跑完。读线程检查返回对象的密级是否合法写线程不断新增。如果锁有问题通常会看到errors大于 0 或者程序直接卡死。参数上loops设 1000 是为了在几秒内跑完机器慢就降到 500。跑完之后对比一下把读写锁换成synchronized全锁耗时通常会增加 30% 到 50%这就是读写锁的价值。另一个技巧是给ArchiveService加一个volatile的版本号每次写操作递增读线程先读版本号再读数据如果版本变了就重试。这适合读多写少且对一致性要求极高的场景但实现复杂度高课程设计里用读写锁就够了。从那以后我每次写完并发代码都强制跑一遍这个压力测试不看到errors为 0 不提交。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑