第二单元博客作业
(1)总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系
同步块的设置是基于架构建立的。我三次作业的架构较为一致:用三元组<电梯楼层,运动状态,门的状态>来表示电梯的当前状态。在run方法的循环中,每次算出其下一状态以及状态转移所需时间,然后sleep相应时间。如果在求算下一状态时发现乘客请求不够了,就wait,直到被主线程唤醒。如果电梯线程发现乘客任务结束则会退出。

getNextState()以及它调用的一系列方法都位于同步块里。所以尽管里面的代码未必都会读写共享数据,但都被锁保护了起来。
这种做法扩大了同步块,会降低并发执行的效率。然而无伤大雅。因为电梯并不是运算密集型的程序,在程序全部运行时间里,绝大部分时间都在调用sleep()方法,CPU运算时间与sleep的时间相比,有着数量级的差距。所以,尽管压缩同步块是加锁的基本原则,但是在本单元的作业中,臃肿的同步块也没有带来太大危害。
然而这种做法的好处是明显的:简化了代码逻辑,让架构变得简单清晰。我没有绞尽脑汁地压缩同步块的大小,也不需要在代码里四处加锁,那么对线程安全的形式化证明工作自然变得异常简单。程序的线程安全基本可以用这一句话来进行说明:一个电梯在进行状态判断(涉及读写共享变量)时,其他电梯要等待锁。正因为我的架构非常简单,在做第三次作业的时候,我才能游刃有余的编写若干种不同算法的电梯(只需改getNextState()),然后用自己的测评机选出最好的交上去,从而获得了第一的成绩。
一直以来,我都有一个关于性能的观点(不知道对错,请指正):算法优化大于语法优化。我认为对于这一单元的作业来说,编写更优秀的调度算法是算法优化,而压缩同步块则是语法优化。如果放着调度算法不去思考,却不惜牺牲架构的简洁性而绞尽脑汁去节省CPU的那一点点运算时间,就是丢了西瓜捡芝麻。
下面分析锁与同步块中处理语句直接的关系:
①我用容器ArrayList()来储存乘客信息,它不是线程安全的,不能被主线程或各个电梯线程同时访问,不同线程对同一个ArrayList()的读写需要被锁保护(主线程向电梯的侯乘表里添加乘客请求,电梯线程消费)。
②ArrayList()里面存的乘客信息不应当被多部电梯同时访问,否则可能出现一名乘客被拉扯进两部电梯的荒谬情况。另外,在第三次作业中,我还在主线程里分析ArrayList()来决定电梯线程是否可以退出,这个操作也应当被锁保护。
③TimableOutput()不是线程安全类,通过它进行输出时,应当在锁的保护下。
(2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
我的程序一共有两类线程:调度器作为主线程,若干电梯作为工作线程。三次作业的算法基本一致:单个电梯LOOK,多部电梯之间自由竞争。对于第三次作业,每个电梯倾向于在能力范围内,把乘客拉到距离终点最近的楼层(A电梯直达;B电梯把乘客拉到离终点最近的奇数层;C电梯把乘客送到{1-3}U{18-20}范围内距离终点最近的楼层)。对于早上的情况,空闲的电梯在一楼开门等人。
电梯自由竞争的好处是能够尽量调动电梯的积极性,让电梯忙起来。我曾经思考过若干种“优化”方法,但经过测评机检测,发现都是负优化,所以最终还是维持了初始版本。
在细节上,调度器轮询获取请求,并按照乘客请求、添加电梯请求、null请求分别处理。如果是乘客请求,则把它加入到电梯的侯乘表中;如果是添加电梯请求,则按要求新增电梯;如果是null请求,则提示每个电梯准备结束,如果电梯自己原来的任务也执行完毕,则电梯线程退出。
第一次作业:调度器只是把乘客信息放到电梯的侯乘表里。这通过调用电梯的addTask()方法来实现,需要用锁保护。当乘客信息结束时,调度器改变各个电梯里的interrupt标志,提示电梯未来不再有任务。
第二次作业:与第一次作业相比,电梯变多了。尽管我主要采用自由竞争策略,但为了兼容自由竞争算法和主观调度算法,我给每个电梯设置了单独的侯乘表,只不过调度器分发乘客请求的时候把每个乘客请求都加入到所有电梯的侯乘表里。另外, 我用MyPersonRequest包装原来的乘客请求,添加了一个布尔类型isTaken作为标记,表示这个乘客是否已经被某一个电梯拉走了。
第三次作业:在这次作业中,我坚定了自由竞争的决心,所以调度器和所有电梯共享同一个ArrayList()作为侯乘表。调度器只需把乘客请求加入到这个总的ArrayList()里。与第二次电梯相比,引入换乘策略。相应的,我给MyPersonRequest添加了“当前楼层”这个属性,在初始化乘客请求的时候初始化它的值,每当乘客出电梯门,也维护这个值。如果当前楼层和目标楼层一致,说明乘客已经到站,所有电梯都不再理会他。电梯执行完自己的任务后,询问主线程是否可以退出。主线程根据两个方面的条件予以回复:①是否读到了null②是否所有乘客都已经到站。为了满足潜在的换乘需求,只要有一个乘客没到终点站,所有电梯就都不能退出。

我尝试过若干种“优化”方法,但是经过自己测评,发现其中表现最佳的竟然还是初始版本,最终提交后,强测结果也不差。
(3)从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性
个人认为我的架构有一定的可扩展性。三次作业没有重构,只有修改,总代码行数从第一次作业的536行到第三次作业的584行(没有增大太多是因为删去了第一次作业中的一些针对单部电梯的优化算法)。在第三次作业中,我着实的体会到了可扩展性带来的便利。我先是完成了一个电梯-调度器的主体架构,没有填充状态转移的判断算法(它有较高的可扩展性),然后把自己想到的若干种可能有效的算法分别填充进去,作为不同分支,最后用测评机选出最优的交上去。
从功能设计与性能设计的平衡方面来看,我的程序在单部电梯的调度算法方面和多部电梯分配方面都有一定的可扩展性。我的架构主要分为 调度器 - 电梯 两部分。其中电梯部分的架构是以run方法里对状态转移的循环判断为基础,目前我采用的调度算法是LOOK,如果想改成别的算法,只需改变状态转移的函数。在多部电梯分配方面,尽管我采用了共享侯乘表的方法让电梯自由竞争,但如果想兼容分配策略,所需的改动也很简单:只需给每个电梯设置不同的侯乘表(我第二次作业就是这样做的),并且在调度器里先进行分配算法的计算,然后把乘客请求分给部分电梯即可。或者给MyPersonRequest新增一个“可见电梯组”属性,乘客对于不在这个范围内的电梯透明。
本次作业功能设计与性能设计不矛盾。功能方面需要让电梯在时间限额内把乘客拉到站,这当然需要良好的性能。而且我的架构比较简单,也是我能够优选不同算法的重要条件。
如果想添加或修改分派乘客指令的逻辑,只需改动Scheduler部分的代码
如果想改变电梯的调度算法,需要着眼于改动getNextState方法
如果想添加新的到达模式,只需扩展状态转移函数
如果想添加更细微的算法设计,只需植入新的辅助函数。
UML类图

UML时序图

由于我的架构比较简单,所以类图和时序图也不复杂。
(4)分析自己程序的bug
三次作业强测和互测都没有发现bug。
在自己测评的时候发现了一个bug:程序无法正常退出。原因是在第二次作业改到第三次作业时,我把所有synchronized锁都换成了ReentrantLock,然而在改代码的时候遗漏了一句notify()(调度器读到null请求时唤醒所有正在wait的电梯),添加condition.signalAll()即可修复。
有一个线程安全问题,是受了讨论区的启发才意识到的:TimableOutput()不是线程安全类,通过它进行输出时,应当在锁的保护下。
由于我加锁比较少,逻辑比较简单,所以没有遇到死锁问题,也没有死锁风险。
(5)分析自己发现别人程序bug所采用的策略
列出自己所采取的测试策略及有效性
&分析自己采用了什么策略来发现线程安全相关的问题
策略是测评机测试和理论分析。①在测评机的帮助下,利用随机测试点和自己构造的一些测试点对别人的程序进行测试,选出稳定报错的来hack。在遵守测评数据限制的条件下,自己构造的测评样例尽量有一定强度,比如乘客集中在后几十秒出现。②读代码,观察它是否用锁保护了每个共享变量。
令人遗憾的是,有一个同学的代码加锁不够严谨,对共享的侯乘表的保护有漏洞,可能出现bug。而且有一个测试点在我的电脑上跑了六次都可以稳定复现,提交上去却没有hack成功,我觉得可能是操作系统的差异导致的,知识已经到手,不必纠结分数,所以我没有继续穷追猛打。
分析本单元的测试策略与第一单元测试策略的差异之处
hack别人时,由于本单元的测试是多线程测试,报错可能不稳定,需要选取较为稳定的报错来hack,而第一单元的报错可以稳定复现。自己测评时,需要通过多次测评,才能较为肯定的说明自己的程序是正确的,第一单元只需一次正确即可判定正确。
(6) 心得体会
-
从线程安全和层次化设计两个方面来梳理自己在本单元三次作业中获得的心得体会
线程安全方面:
线程安全隐患往往是由资源共享带来的,而资源共享的做法又是为了满足功能或性能的需要,所以无法回避。加锁是解决数据竞争的有效方式,然而又会引入死锁风险。我觉得防范线程安全问题的有效方式不是漫无目的的测试,而是进行形式化验证。用并行思维来思考并发问题,对程序执行过程的不同情况进行逻辑分析,做到胸有成竹,才能严谨的保证线程安全。
层次化设计:
逻辑层次与代码层次较为一致,有助于简化架构。层次化设计有助于把复杂的问题转化成更简单的若干个小问题,降低思维难度和编程难度。对于我的架构,个人感觉层次还是比较分明的。详情如图所示。

如果想添加或修改分派乘客指令的逻辑,只需改动Scheduler部分的代码
如果想改变电梯的调度算法,需要着眼于改动getNextState方法
如果想添加新的到达模式,只需扩展状态转移函数
如果想添加更细微的算法设计,只需植入新的辅助函数。

浙公网安备 33010602011771号