BUAA OO Unit2 Summary
BUAA OO Unit2 Summary
零、写在前面
https://www.cnblogs.com/GrapeLemonade/p/14702876.html
初听不知曲中意,再听已是曲中人。
一、同步块与锁
锁的选择
在本单元作业中,我全部采用了JVM层面的synchronized
关键字对同步块进行加锁。虽然在后期的理论课和上机实验中也引入了lock
读写锁——在资源竞争很激烈的情况下性能吊打synchronized
,但是出于对正确性的稳妥考虑并没有使用。(结果最后正确性都没保证😄
同步块设置
在三次作业中,我主要采用的是类似 exp3 的设计然后基本没有大的改动:实现了一个 “线程安全类” RequestQueue
作为共享对象,对其中所有方法进行加锁,保证相应的添加、删除和查找等操作具有原子性。同时,需要采用wait-notifyAll
的形式编程以避免轮询。在我的理解中,当输入尚未结束但没有新请求进入、或者调度尚未结束但没有投喂给电梯的时候,电梯线程拿不到请求,就要停下来释放锁,即wait()
;与之对应,当我们输入结束或添加请求时,就要使用notifyAll()
来唤醒等待池中的线程进入RUNNABLE
状态。这里需要注意,当我们访问RequestQueue
中的信息(也就是读操作)的时候,是不需要notifyAll()
的。否则,当一部电梯在查询RequestQueue
结束之后,就会唤醒同一栋\同一层所有在等待的电梯,然后拿到锁的那部电梯大概率是没事干的,发现没事可做之后又进入WAITING
状态。这样频繁无意义的线程唤醒会占用大量CPU资源,甚至会阻塞其他线程(比如输入线程),导致程序都无法运行。
此外,在第三次作业中,为了结束线程,参考 exp4-2 增加了一个RequestCounter
类,其单例由输入及电梯线程共享,所以也需要进行同步控制。最后一个不太明显的同步块是针对TimableOutput
设计的:为了保证输出的安全性(也就是输出的时间戳必须非递减)需要对其加锁,具体学习了往届学长的博客,不多赘述。
二、调度器设计
在前两次作业中,我并没有设置调度器,一方面是出于设计的简洁另一方面也感觉也没有必要。由于采用了自由竞争的策略,遇到新请求就直接扔到对应building/floor的RequestQueue
中,让挂在请求队列上的电梯自己抢。仔细一想,这样的设计有何 “调度” 可言?遂放弃。
然后到了第三次作业,需要处理换乘的问题,一开始还是懒得加调度器,而是考虑赋每栋的电梯访问所有floor队列的权限、每层的电梯访问所有building队列的权限,来实现请求的重新加入。这么一想,电梯似乎不再那么纯了,且 ”调度“ 的意味貌似就在其中了。最后增设了一个总的等待队列以及调度器线程:新来的和重入的请求都放入等待队列,通过调度器分派到应该去的楼层or楼座中。
三、线程协同的架构设计
在本单元的设计中,我主要采用了生产者—消费者模型。到了第三次作业,我参考了上机实验,借鉴了一点流水线模型的思想,新增加了RequestCounter
类用于结束线程、 Scheduler
类用于实现新请求及换乘的调度,同时形成了:
InputHandler - <waitQueue> -Scheduler - <buildingQueues/floorQueues> - Elevators
的两级生产者—消费者模型。
以下是我第三次作业的时序图 (sequence diagram)。
电梯策略
电梯的运行逻辑如下:
public void run() {
while (true) {
if (Queue.isEnd() && Queue.isEmpty() && noPassengersInside) {
return;
}
putOffPassengers();
PickupList = look();
getOnPassengers();
if (doorOpen) {
close();
}
move();
}
}
如图,对于纵向电梯我采用的是look
策略,关于算法本身就不多说了,简单来说就是”捎带同向,同向无请求则反向“,没请求就停着空等。而横向电梯类似,魔改了一个look
,要注意转向的逻辑需要修改,防止死循环一直绕圈的现象;也可以采取无条件捎带、或者一直顺/拟时针,在这种最远距离不过3栋的情况下性能应该差别不大。
关于电梯的运行,虽然参看了学长的博客,但我并没有实现所谓的”量子电梯“优化,私以为只是一种“合理利用规则”的行为,但在现实中毫无意义。
分配策略
如上所述,采用的是自由竞争的策略,毕竟没有全局最优,哪为什么不简单点呢(
在我的设计中,每个电梯在每层都会根据策略生成一个捎带列表,然后根据这个列表去接客,只有当真正接客的时候电梯才会从请求队列中取走请求,这样就保证不会出现一个乘客上多部电梯的情况。这种“建议式”的设计自然地就适应了自由竞争的策略。
换乘策略
仅有一瞬间想过最短路,但感觉考虑的因素很多而以我的编程能力估计很难实现,遂采用基准思路。(然而甚至连基准思路都实现不好
-
根据请求特点,静态拆分为1~3个阶段,一般情况为纵向—横向—纵向。
-
从原来继承了一个
MyPersonRequest
,增加了中转层midFloor
以及当前阶段stage
标记,重写了四个getxxx()
方法。目的是最大程度保证Elevator类的复用,仅需要小修一下putOffPassengers()
:- 是最后阶段:正常下课,标记完成了一个请求
- 不是最后阶段:进入下一阶段,扔回
waitQueue
即可
以下是三次作业的UML类图。
HW5
HW6
HW7
综合看三次作业,第六次增加了横向电梯,第七次增加了计数器、调度器以及修改的请求类,整体上看是迭代的增量开发,基本符合开闭原则。
而对于可扩展性,在我看来,目前这个版本而言是很差的,主要是因为我比较懒()。如果实现以下修改可以提高可扩展性:
- 增加
Elevator
类,两种电梯或者新品种的电梯都可继承自父类,采用工厂模式产生新电梯 - 增加
Strategy
接口,将电梯本身的运行与其指导策略解耦,然后将实现接口的特定策略作为电梯的属性,相当于电梯自身的“调度器”。
四、bug分析与hack策略
自己的bug
出现在第三次作业,都是功能性的bug……
- 对
MyPersonRequest
类重写的getxxx()
方法有错误,没有设置默认的midFloor
- 采用基准思路寻找中转层的时候没有考虑横向电梯的可达性
他人的bug
- 第一次作业:直接无效作业(
- 第二次作业:随机看了两个人的、感觉写得还行,然后用自己出过问题的样例交了两发,没有成功
- 第三次作业:一早醒来就被hack了,难蚌。然后用同学的测评机跑,一下就出问题了,交上去hack了三个
个人对互测的兴趣不高,也怠于看别人的代码,所以一般是用自己错过的数据以及dalao的测评机生成随机数据来hack。
多线程debug
与第一单元相比,第二单元的debug除了要检查功能的正确性,还要关注线程之间的交互:防止出现死锁、轮询等问题。同时,由于多线程问题很难复现,所以需要一组数据跑多次来调试。总之,
跑一百次没问题,不代表没有问题
跑一次有问题,那一定有问题
对于多线程,可以更多地进行白盒测试,通过阅读代码保证逻辑上的正确性。同时,也可以利用jps、jstack、jconsole等JVM监控工具以及JProfiler(神器)辅助调试。
五、心得体会
线程安全
线程安全可以说是多线程编程最终要的问题。我选择对共享对象类集中加锁,便于管理也便于定位错误。这样一来,只要是对共享对象的处理就能保证一定是读写一致的。同时,为了更优的性能,synchronized
不应该简单地作为方法的修饰词,应该把临界区设计得尽可能小;notifyAll()
不能无脑加,一般情况只读共享对象时是不需要的。而这些,都要求我们对多线程理论知识有充分的了解。
层次化设计
在经历本单元作业之后,我也对层次化设计有了更深的理解。主线程,输入线程,调度器线程,电梯线程,信息(Request)在其中是逐级传递的:输入获取新请求传给调度器,调度器安排给电梯,电梯再返还给调度器……合理的划分层次应该让各级只承担一种职责,而不是像我的设计中让InputHandler
大包大揽。荣文戈老师上课几次讲到了“分而治之”的思想,对于功能、对于优化都是如此:每一层干完自己的就可以扔给下一层。大道至简。
多线程对我来说,是新知识,而且难。接触新知识的过程总是漫长且痛苦的,也让我送出了第一次的无效作业;但当你初探究竟之后,似乎又信手拈来,就像第二周顺带把第一周作业做完的我当时盲目自信的样子;最后一次作业就是给我浇的冷水,让我知道多线程,路还很长。