第二单元总结:基于synchronize锁的简单多线程设计
单元统一的多线程设计策略
类的设计
电梯
- 每部电梯为一个线程。
- 电梯从调度器接收原子指令,知晓自己的状态(内部的人/服务的人、运行方向、所在楼层)
- 原子指令包括且仅包括:
- 向上走一层 / 向下走一层
- 让哪些人进电梯
- 让哪些人出电梯
- 而电梯不可见其他电梯的状态、不可见调度队列的内容
- 相当于电梯只是一个输出器和状态储存器,大大解耦
输入器
- 输入器为一个线程。
- 输入器直接将输入的指令加入调度队列(在第三次作业时可能会将指令进行拆分后再一一加入)
- 输入器与调度器、电梯均无关,不直接传送数据
调度队列
- 调度队列使用单例模式
- 支持push_back操作
- 支持按某些策略取出元素
- 保证线程安全
调度器
- 调度器只是一个保存方法的类,直接由电梯调用申请一个原子指令返回,自己不开线程。
- 调度器使用单例模式。
- 调度器可知晓各个电梯的状态、可访问调度队列,向申请指令的电梯返回一个原子指令。
线程间逻辑UML图
图中带[T]标记的都是线程,其他的都是被调用方法的普通类。
主线程(Main.main)只负责 构造并start一个ReQuestInput线程和若干个Elevator线程,本身无业务功能,在图中忽略。
注意到,是电梯主动请求Handler分配一个原子请求,执行完再次主动请求;而不是Handler主动向电梯发布任务!
因为电梯是线程,本身存在时序上的先后关系和竞争关系,相互独立;而调度器不是线程,也不知道电梯是否运行完毕。
(笔者认为在非synchronized / Lock的地方使用wait() / notify()是没有必要的且危险的,因为这一对操作的本质意义是对于条件变量工作的)
故选择让电梯执行完一个原子操作后主动管调度器“要”请求(就是普通的函数调用,不涉及多线程通信和同步)
或者说,没有必要使用多线程的时候,就不使用多线程!

多线程通信与控制策略
在尽量少用多线程的设计思想下(也直接导致了线程的共享数据大大减少),被多个线程共享的数据只有两个:
- RequestQueue:被输入线程和电梯线程共享(在三次作业中均存在)。其作用是保存全部的输入请求。
- 由于调度器本身不是线程,所以只要在插入(输入器)和访问(电梯调用的调度器方法)中保证线程安全即可
- 对于某些标记,访问和更改器方法标记为synchronized
- 对于容器,使用Java自带的线程安全类。
- 由需求决定该“请求队列”并不是一个FIFO的队列,而应该是一个支持遍历的ArrayList
- 使用concurent.CopyOnWriteArrayList保证容器的线程安全(能用Java保证好的线程安全支持,就不要自己造轮子)
- ServingMap:被电梯间共享(在第三次作业——多电梯中存在)。
- 其功能是保存各个请求已经被哪个电梯服务(或者没有电梯服务),防止电梯间重复承担请求。
- 于是该对象被多部电梯之间共享,标记为static对象,要保证线程安全。
- 在使用ServingMap时(全部操作,包括get()方法、遍历KeySet等)全部使用synchronized关键字,对方法或对象加锁。
在尽量少用多线程的设计思想下(也直接导致了线程间同步控制大大减少),不存在使用wait()和notify()的场景!
总结来说,笔者坚持少用多线程,从而导致使用多线程特性极少,安全性和鲁棒性显著提升,在测试的时候基本上没有不可复现的多线程bug。
第一次作业——傻瓜调度的单电梯
类图

可以看到,主线程Main只是构造和开启了电梯线程和输入线程而已,并没有其他的操作。在三次作业中均是这样。
输入线程将输入请求加入请求队列,调度器从请求队列中访问请求,构造原子操作发送给电梯。
代码度量分析

可以看到,除了构造和发送原子指令的方法,其他的方法复杂度都极低,而nextAtomicTask()方法也没有飙红字,说明本设计还是很简洁的!

从类复杂度也可以看出,整个设计复杂度低,十分合理。
bug分析与总结
在中测中,笔者曾经把评测机搞坏过一次,那可以说是“第零个版本”,出现了一些乱写的错误,
比如当时对线程的理解太过sb,没有选择让一个线程没事干的时候进行等待,而是当它完事之后自动死去,再开启新的线程重新run的设计。
众所周知线程的用法是让必须在多条线同时进行的工作并发,而不停的“断线”再“接线”显然不是线程的正确用法——还不如一直保证“线不断”,只不过在中间Thread.sleep()掉。
创建和销毁线程都是危险的操作,必须做到程序员可控,不应该将其放在while等循环中,否则一旦程序有bug,将不断地创建新线程,吃掉大量资源的同时造成麻烦。
在使用了新的设计模式之后,笔者在公测和互测中均未被发现bug。
由于第一次作业太过简单,在互测中,未发现其他同学的bug。
第二次作业——优化调度方案的单电梯
算法
由于第一次作业的电梯(输出器和状态保存器)和调度器分离的良好设计,在第二次作业中只需改变调度器之中的nextAtomicTask()方法即可了!
调度策略如下:
- 如果能捎带(nowFloor == xRequest.fromFloor),则上电梯,无论是不是同方向
- 针对某个请求,有电梯目标楼层定义:
- 如果不在电梯内则去from层接他
- 如果在电梯内则去to层送他
- 电梯运行方向由整个队列的目标楼层决定,取max和min操作可以得到最高目标楼层maxTarget和最低目标楼层minTarget
- 如果当前的运行方向向上,则定义电梯的本原子操作目标楼层Target为maxTarget
- 如果当前的运行方向向下,则定义电梯的本原子操作目标楼层Target为minTarget
- 由Target和当前楼层nowFloor的相对位置决定运行方向
可以看到,由于要知晓电梯内部的人(某人在不在、有哪些人在),还要知晓当前的全部请求情况,调度器的复杂度将会显著上升。
类图

可以看到,此次作业的架构和上一次完全一致,只在电梯内部增加了一个保存“哪些人在电梯内”的列表,
还增加了一个主请求对象(类似指导书中的主请求),其他的完全不变,也体现了本架构对新功能的支持性良好。
代码度量分析

(大部分复杂度均为1的方法被省去)
毫无疑问的,取原子操作的nextAtomicTask()方法复杂度最高。仔细研究代码后发现该方法的主要任务可以分成三块,而笔者为了写起来方便(懒),将它们写到了一个方法内。
其实将某些功能移出是更优的写法,因为显然“通过最高目标和最低目标确定目标楼层,从而确定运行上下方向”的任务应该是一个独立的逻辑模块。而事实上笔者在第三次作业中意识到了这个问题,将其移出了。
而电梯的doOneAtomic()方法中存在大量if语句,导致复杂度稍高。研究代码后发现部分if其实可以简化,而笔者为了保存代码的可读性没有进行简化,这里红字的判断有待商榷。

可以看出,调度器类由于要经常遍历电梯内列表、调度队列列表,存在大量的分支和循环操作,所以操作复杂度稍高。
bug分析与总结
由于第一次作业中不存在显然的bug,而本次作业又直接继承自第一次作业,那么定位和排查可能的bug的范围就很小了,便于测试和迭代,这也是保证可拓展架构的附加好处之一。
也由此笔者在修改第一次作业到第二次时思路更加清晰:只需管新增和修改的函数即可,其他的全部当作一个黑箱,不修改也不关心。这进一步加强了其正确性,以至于笔者在写程序时没有碰到过完整的bug。
笔者在公测和互测中均未被发现bug。在互测中,也未发现其他同学的bug。
第三次作业——优化调度的多电梯
设计
总体来说进一步沿用第二次作业中的架构,对优化算法进行了一点小的调整。
电梯的构造函数变成传入参数(由于名称、可达列表、速度等属性并不相同)。
值得一提的是,为了保证电梯内的修改尽量少,笔者使用了离散化实际楼层的小技巧:
- 设立实际楼层->虚拟楼层的映射,在给电梯发送信息时使用虚拟楼层号
- 设立虚拟楼层->实际楼层的映射,在电梯输出、sleep()和调度器判断条件时使用实际楼层号
这样,电梯本身的代码几乎没有改变,只有在输出“ARRIVE-”和sleep()时需要得到真实的实际楼层号。这样在第二次作业中的“从1楼到3楼”可能在第三次作业中实际上是“从1楼到7楼”,
但是电梯内部的改动极少,而且以前代码的正确性会被继续保证(当作黑箱)。
调度器方面,增加了一个HashMap<Request, ElevatorName>来维护哪些请求被哪个电梯服务,从而解决电梯的冲突问题,也巧妙地规避了多线程的其他问题,只需对HashMap对象加锁即可。
而上述一切的保障都来自于调度队列的修改——本设计下,任何一条请求都应该是能被一部电梯独立完成的,所以我们需要将不能被一部电梯完成的请求合理拆分成两个请求。
这个部分在Queue.pushBackRequest()中进行,下文将会进一步阐述。
类图

可以看到,架构设计基本不变,只是现在为了便于管理,电梯成为了RequestHandler的内部对象,有着聚合的关系。
代码度量分析

此次调度器逻辑变多,为了更好的优化从而导致了代码复杂度的增加,是可以预见的。
在电梯的doOneAtomic()方法中,部分输出模块可以单独变成一个方法,从而减少每个方法的循环和分支复杂度。
RequestQueue中的peekFront()方法同样因为更好的优化从而导致了代码复杂度的增加。
真正值得修改的是RequestQueue中的pushBackRequest(Request)方法。
本方法由于设计上需要判断请求的一部电梯可达性、拆分不一部可达的请求,故该方法的逻辑变得复杂了许多。
但是仍然写丑了,如果让我进行修改将是这样的:
- 判断一部可达性,单独变成一个Boolean函数,从本方法中提取出去(判断任务如果没有附带着顺便更新值的功能,尽量单独摘出);
- 求中间换乘楼层,单独变成一个int函数,从本方法中提取出去(求得一个值,而对当前环境不产生改变影响,尽量单独摘出)。
- 这样pushBackRequest方法就回归了其本质:
一部电梯可达吗?可达的话,插入请求!
不可达的话,求得中间楼层,构造插入两个请求!

从类复杂度上看,由于电梯内增加了虚拟楼层<->实际楼层转换,而且输出时需要一层实际楼层、一层实际楼层地输出,增加了循环逻辑,导致其复杂度上升。
而奇怪的时RequestHandler的复杂度居然较第二次降低了,不是很理解。
bug分析与总结
本次对优化算法的改动较大,而且增加了离散化的虚拟楼层,故发生bug的几率大大增加。
我的测试策略如下:
- 首先针对离散化后虚拟楼层和双向映射、以及换乘方案转换的代码进行测试:
总共只有[-3,0) (0,20]共23层,于是我构建了共计23*22 = 506个测试用例,
一开始在没有测评机时手动剔除相同类型的数据(如16、17、18、19、20层可以视作等价),再喂给程序进行逐一测试,保证楼层相关的正确性。 - 其次是对多电梯协作和电梯容量等进行测试,这里主要是人工识别易错点,之后手动构建针对性数据进行测试,
包括乘客是否可能分身、容量限制是否成功等。 - 之后有了评测机后进行全面的自动测试。
- 由于架构基本没有变化,只增加了一个由调度器维护的HashMap,故线程安全性非常好保障(归功于继承自安全的架构和新设计的简单性),不需进行测试。
笔者在公测和互测中均未被发现bug。在互测中,也未发现其他同学的bug。
总结
- 尽量在一开始就选择相对好的设计架构,不但可以让拓展性得到保障,还可以在拓展时的思维复杂度大大降低,同时出bug的可能大大降低。
- 尽量让方法回归其本质功能,不该放进去的不放进去。一些特定模式的功能也应该拎出来。
- 尽量少用多线程,不得不用时才使用。慎重使用没有必要的sync / Lock搭配的wait() / notify(),它们是用来实现条件变量的相关操作的,不是让你真的去等待和唤醒的。
- 不要经常重复创建新线程和让线程早死掉,即使使用join()方法也不好。线程的生命周期应该被程序员牢牢把握。
第二单元总结:基于synchronize锁的简单多线程设计的更多相关文章
- 第二单元电梯调度作业 By Wazaki
figure:first-child { margin-top: -20px; } #write ol, #write ul { position: relative; } img { max-wid ...
- OO第二次博客作业--第二单元总结
第一次作业 1. 设计策略 第一次作业,一共三个线程,主线程.输入线程和电梯线程,有一个共享对象--调度器(队列). 调度的策略大多集中到了电梯里,调度器反而只剩下一个队列. 2. 基于度量的分析 类 ...
- OO第二单元总结(多线程的电梯调度)
经过第一单元作业的训练,在做第二单元的作业的时候,要更加的有条理.但是第二次作业多线程的运行,带来了更多的运行的不确定性.呈现出来就是程序会出现由于线程安全问题带来的不可复现的bug.本单元的作业也让 ...
- 【BUAA-OO】第二单元作业总结
第二单元作业总结 ——电梯恐惧症患者的极限自救 一. 第一次作业程序分析 1. 设计策略简略分析 线程:主线程.输入线程和电梯线程,另有一个持有请求队列的调度器,一个对输入进行处理的Req ...
- OO第二单元作业总结【自我反思与审视】
第二单元作业的完成史,就是一部心酸的血泪史…… 多线程的出现为我(们)打开一片广阔的天地,我也在这方天地摸爬滚打,不断成长!如果说第一单元之前还对Java语法有所了解的话,那么这单元学习多线程则完全是 ...
- BUAA_OO第二单元作业总结——多线程
OO第二单元作业总结——多线程 单元任务 本单元主要的内容是通过模拟电梯的运行来熟悉多线程的实现,从简单的单部FAFS电梯开始,ALS电梯,到最后的多部ALS电梯. 一.设计策略分析总结 1.1 多线 ...
- OO第二单元总结之线程大冒险第一关
第二个单元的三次作业均为多线程电梯的设计,旨在让我们能够理解多线程在面向对象设计时的重要意义,并熟练掌握在保证线程安全和性能高效情况下的多线程协同的设计模式——在本次作业中主要体现在生产者-消费者模式 ...
- 2019OO第二单元作业总结
OO第二单元的作业主题是模拟电梯. ---------------------------------------------------------------------------------- ...
- 抢人就完事了——OO第二单元总结
总结性博客作业 (1)从多线程的协同和同步控制方面,分析和总结自己三次作业的设计策略. (2)基于度量来分析自己的程序结构度量类的属性个数.方法个数.每个方法规模.每个方法的控制分支数目.类总代码规模 ...
随机推荐
- USN日志
转载:https://www.iteye.com/blog/univasity-805234 https://blog.51cto.com/velika/1440105 源码:https://f ...
- 使用 TestFight 构建 Beta 测试版本
---恢复内容开始--- Beta测试属于软件开发周期中的一环,测试的重点就是让一些活生生的人去使用你的App,不断测试然后反馈.你需要让你的测试成员发现尽可能多的bug,以便你在公开发布之前将其修复 ...
- matlab学习 — 实现简单的爬虫
这里复杂的情况暂时不考虑..测试网址为pixiv的每日排行榜 = = url = 'https://www.pixiv.net/ranking.php?mode=daily' text = webre ...
- luogu P2774 方格取数问题
有限制的问题,显然考虑全选再根据限制去掉的想法较优,我们发现一个点四周的点受限,其x或者y差一,也就是说奇偶性不同,那我们可以将其分成白点和黑点,就变成了最小割的问题,将每个白点向受限制的黑点连边,c ...
- CSS - icon图标(icon font)
1. 概念 这个小红点是图标,图标在CSS中实际上是字体. 2. 为什么出现本质是字体的图标? 2.1 图片增加了总文件的大小. 2.2 图片增加了额外的http请求,大大降低网页的性能. 2.3 图 ...
- TCP/IP,三次握手四次挥手,TCP/UDP , HTTP/HTTPS
internet:通用名词,由多个计算机网络组成的网络,网络间的通信协议是任意的 Internet:专用名词,当前全球最大的开放计算机网络,采用TCP/IP协议族作为通信的规则.www万维网是广泛应用 ...
- C# Connection:连接数据库---转载
C# 语言中 Connection 类是 ADO.NET 组件连接数据库时第一个要使用的类,也是通过编程访问数据库的第一步. 接下来我们来了解一下 Connection 类中的常用属性和方法,以及如何 ...
- SAP BO WebI 如何连接webi server folder下面的EXCEL文件作为数据源
昨天做Webi Report,需要连接一个在Webi Server Folder下面的EXCEL文件作为数据源,然后再去生成相应的报表,找了半天才找到可以连接Webi Server Folder的EX ...
- Qt中QListWidget的verticalScrollMode选项设置为ScrollPerPixel无效果的原因
设置为ScrollPerPixel无效果,根据Qt手册的描述,需要在设置一次setSingleStep()的值,才会生效
- ROS学习笔记INF-重要操作列表
该笔记将重要操作的步骤进行列表,以便查询: 添加消息 在包中的msg文件夹中创建msg文件 确保package.xml中的如下代码段被启用: <build_depend>message_g ...