第二单元总结
第二单元总结
前言
第二单元的作业主要围绕电梯接人而展开,通过完成第二单元的作业,我逐步理解了多线程存在的意义,本单元的难点在如何解决线程安全问题以及选择什么调度策略上。线程安全与程序正确性相关,调度策略与程序性能相关。在架构中,我采用了生产者-消费者模型,并在后续作业中将其扩展成为了食物链模型,调度策略上,纵向电梯我采用LOOK策略,横向电梯只作顺时针运动,换乘策略我采用了指导书中给出的基准策略。我认为总体的设计是不错的,在编写代码容易的同时,也不容易产生线程安全问题,并且也能得到比较不错的性能分。
第一次作业
作业思路
架构
总体架构如下图所示:
架构采用了食物链设计,共享对象为等待队列(PassengerQueue)类,输入线程与调度器共享一个等待队列,调度器与五个电梯共享分别共享一个等待队列,有了调度器的存在,输入线程便可以无脑向等待队列中加入乘客,再由调度器取出后进行判断最后加入到电梯乘客的等待队列中。通过本次设计,我发现要将电梯设计成多线程的目的首先是要保证输入的随时性,传统输入往往都是一次输入后就结束,而多线程的输入更好地模拟了真实情况,即乘客可以在任意时刻到来。第二个是要保证电梯运动的同时性,传统的单线程程序一次只能运行一个电梯,而多线程程序可以保证5个电梯同时运行。
调度器设计
本次作业的调度器非常简单,就是从输入线程往里添加乘客的等待队列中取走乘客,再根据乘客所在的楼层向电梯乘客等待队列里投放乘客。
同步块的设置与锁的选择
由于本次共享对象的设置只有一个类,即PassengerQueue类,所有锁的设置都围绕它来进行。实际上为什么要对一个东西加锁,就是因为其他线程会同时对一个对象进行读或者写,加锁的目的是为了避免这种同时的情况,保证读的时候不写,写的时候不读。
下面是PassengerQueue中同步块的设置:
public synchronized void setEnd(boolean isEnd)
public synchronized boolean isEnd()
public synchronized boolean isEmpty()
public synchronized void addPassenger(Passenger passenger)
public synchronized ArrayList<Passenger> getPassengers()
public synchronized Passenger getSurePassenger(int id)
public synchronized Passenger getOnePassenger()
public synchronized void sleep()
这些方法的锁保证了PassengerQueue类中的属性不会被其他线程同时读或者写,同时为了避免轮询,每当从队列中获得人时,一旦为空,都要进行wait()。
并且当等待队列与电梯内的乘客队列都为空时,要进行wait(),以避免轮询。
if (passengerQueue.isEmpty() && passengers.isEmpty()) {
passengerQueue.sleep();
continue;
}
策略选择
本次作业我采用的是LOOK策略,性能不错,但在研讨课上我们比对了其他策略,发现不论采用什么策略,总会有数据不适合这个策略,造成性能低下。
LOOK策略总体可以表述为
-
电梯沿一个方向运动时,每次到达的地点满足(以上行为例):
- 当前电梯内乘客目的地的最小值t1。
- 等待队列中请求方向与电梯运行方向相同并且出发楼层大于电梯当前楼层的乘客所在的最低楼层t2。
- 取t1与t2的最小值作为下次到达的楼层。
-
在当前电梯运行方向上没有请求并且电梯中的乘客全部送到后,检索请求方向与当前运行方向相反的乘客,如果其出发楼层大于电梯当前楼层(以上行为例),到达地点为该乘客出发楼层。
-
运行方向取反。
代码结构分析
类图
时序图
Schedule:
InputThread:
Lift:
MainClass:
第二次作业
作业思路
架构
整体架构如图所示
为了能完全复用第一次作业的设计,采用了超长的食物链模型来保证将人员分配到每个电梯独有的等待队列中,而不像自由竞争那样多个电梯共享一个等待队列,也就是说当新增一个电梯时,不仅要创建电梯线程,还要为其创建乘客等待队列。这样的设计虽然有一点麻烦,但线程的安全性很高,因为总体上只用了食物链模型,困难的地方在第一次作业已经写完了,可以全部沿用。但是不断地拿取与分配会损失一些性能,但并不多。
调度器设计
设计了多个调度器,调度器1将电梯请求的横向与纵向分开,调度器2将乘客请求的横向与纵向分开,调度器3、4分别按楼座与楼层将人员分配进该楼座、楼层的乘客共享队列中,再由每层和每座各一个的人员分配调度器将人员分配进每个电梯的共享队列中。同时横向与纵向电梯分配调度器按楼层、楼座将电梯精确增加到横向、纵向的电梯队列中。设计方法沿用第一次作业,只是共享对象有一些改变,别的基本不变。
同步块的设置与锁的选择
PassengerQueue类与作业1相同,需要补充ElevatorRequestQueue的同步块以及将同一座/层的电梯与他们的共享队列集合起来的Vertical与Sideways类的同步块。
同步块的设置如下:
ElevatorRequestQueue:
public synchronized void addElevatorRequest(ElevatorRequest elevatorRequest)
public synchronized ElevatorRequest getOneElevatorRequest()
public synchronized boolean isEmpty()
public synchronized boolean isEnd()
public synchronized void setEnd(boolean isEnd)
可见与PassengerQueue类十分相同。
Vertical与Sideways相同:
public synchronized void addLift(int id)
public synchronized void addPassenger(Passenger passenger)
public synchronized void teEnd()
这里同步块的设置也是为了保证一个既被读又被写的变量不会同时改变。
同时避免轮询的的部分与作业1相同。
策略选择
纵向电梯的策略选择与作业1相同,横向电梯策略选择是一直保持顺时针旋转,能接人就接人,能放人就放人,只有能接人或者能放人时才能开门。同时为了避免轮询,每人的时候一定要停下来,方法与作业1相同。这样设计的性能还不错,大概率是强测数据中纵向的请求要比横向多很多,并且横向电梯走一层只有0.2s,性能主要还是体现在纵向电梯上。
代码结构分析
类图
时序图
新增了横向电梯,其他的相同。
横向电梯:
第三次作业
作业思路
架构
相比于第二次的设计,只需要让横向电梯与纵向电梯支持向最大的乘客等待队列内放回需要换乘的乘客即可。并且为乘客多添加了两个属性,一个是state表示当前的状态,一个是changefloor表示换乘的楼层。并且根据面向对象的思想,外界是不需要读出乘客的changefloor的,乘客向外界展示的只是通过他的state的值给出的出发、目的楼座与楼层,fromfloor,tofloor,frombuilding,tobuilding,changefloor为乘客自身的属性,getFromFloor(),getToFloor(),getFromBuilding(),getToBuilding()为外界获取乘客信息的方法:
state | 0无需换乘 | 1第一阶段(纵向) | 2第二阶段(横向) | 3第三阶段(纵向) |
---|---|---|---|---|
getFromFloor() | fromfloor | fromfloor | changefloor | changefloor |
getToFloor() | tofloor | changefloor | changefloor | tofloor |
getFromBuilding() | frombuilding | frombuilding | frombuilding | tobuilding |
getToBuilding() | tobuilding | frombuilding | tobuilding | tobuilding |
通过对乘客进行这样的设计,我们便可以直接沿用第二次的设计,只需要保证需要换乘的乘客每次下电梯时将state加1即可。
调度器设计
与第二次相同。
同步块的设置与锁的选择
相比于第二次,新增了一个cnt量,为了记录是否全部乘客已经下电梯,避免输入结束就不能再往等待队列添加乘客的情况。所添加的锁也是为了保证cnt量自增与自减都是原子性的。
public synchronized void addCnt()
public synchronized void sunCnt()
public synchronized int getCnt()
同时增加了helper类,来帮助输入线程确定乘客的换乘楼层,对helper的读与写也需要加锁保护。
public synchronized void addMessages(int m)
public synchronized ArrayList<Integer> getMessages()
策略选择
换乘策略选择的是指导书中的策略,实现起来简单,性能也不错。
代码结构分析
类图
时序图
MainClass:
横向电梯/纵向电梯:
调度器(以一个为例):
Bug分析
公测:
三次作业在公测中都没有bug被找出。
互测:
第一次作业:bug有两方面,一是输出线程不安全,在按照讨论区修改后就没有问题了。二是不能逐层输出,这个很好改,改完bug修复就完成了。这两方面bug的特点都是没有仔细看指导书与讨论区所致。
第二次作业:被hack了一个点,在70s加入了一部电梯,导致线程无法结束,至今也没复现......
第三次作业:互测中没有被测出bug
找bug的策略
自己debug的:
在这一单元,我仍然采用分模块测试的方法,先将每个线程独立出来进行测试,再将他们分别连接起来进行测试,比如先将输入线程与调度器连接起来,观察调度器往乘客队列中放入的乘客是否正确,对电梯线程进行测试时,可以先不将其与调度器连接起来,而是提前在等待队列中放一些人,观察其行为是否正确,如果这两部分基本都正确,那么将他们连接起来也大概率正确。
互测中的:
由于前两次互测时比较忙,所以没有进行hack。
最后一次我采用的策略分为两种,一种是在70s时加入大量的乘客试图构造RTLE,另一种是下载下别人的代码,静态观察其中有无逻辑错误,并专门构造数据来进行测试。成功hack了一次。
与第一单元不同之处:
不同之处主要在于不可复现性强,并且机器构造数据进行测试意义变小,需要手动构造一些***钻的数据来测试。
心得体会
- 多线程这一单元线程安全是重点,如果能保证线程安全,那么测试大概率可以通过。
- 线程安全问题主要分为两种,死锁与轮询,轮询时要注意查找有没有循环后会不断触发continue的语句,死锁要看哪里出现了死循环。即弄明白什么时候wait,wait之后什么时候notify。
- 我对层次化设计的理解是将设计分为多个小设计,比如将一个很大的调度器设计成多个小调度器的组合,这样逻辑清晰,写起来也简单。
- 要多查资料,多看书,一开始我对多线程也是一头雾水,但在阅读了《图解多线程设计模式》这本书后,多线程的知识变得清晰起来。
- 同时还要感谢老师和助教在我遇到问题时帮我解答疑惑,感谢同学与我一起讨论,让我理解电梯调度策略具体的实现等等。