OO第二次博客作业
OO第二次博客作业
因为之前从来没有接触过多线程编程,对于线程安全、同步控制、死锁这些概念都一无所知,所以这一单元学的很吃力,但收获满满。
OO第二单元需要完成的任务为多部多线程可捎带调度电梯的模拟,第一次迭代增加了电梯运行的数目,第二次迭代增加了对电梯的运行速度和停靠楼层的限制,通过这一个月的训练,我不仅增加了对多线程编程的了解,学会了同步控制块,锁机制,wait和notify等多线程编程方法,也掌握了生产者消费者模式、Worker Thread模式、观察者模式等并发程序设计模式。
第一次作业
先放上类图再具体分析:
我的程序有一个输入处理类Input,一个调度器类Manage,Input和Manage构成生产者消费者模式,中间的共享对象是RequestQueue,这个对象对一个ArrayList进行了同步控制。Main类创建了一个RequestQueue,将其传入Input和Manage,并将这两个线程启动。在此我依据课程组下发的Request对象重新设计了Passenger类,主要是为了后续迭代时可能出现的换乘站做准备,在前两次作业中并没有什么作用。
Manage负责创建调度器和电梯之间的共享对象,启动电梯和向电梯分派任务。调度器和电梯再次构成生产者消费者模式,共享对象是ElevatorQueue类,这个类对一个以楼层作为索引的HashMap进行同步控制。因为第一次作业只有一个电梯,这种存在两对生产者消费者关系的设计感觉不是很有必要,Manage和ElevatorQueue这两个类都是多余的,但是这种设计对于增加电梯数目,增加电梯种类,还有增加请求种类等迭代方向代码的改动都很小。
电梯有一个内部队列,也有一个和Manage共享的外部队列,每次内部队列为空时,就会调用外部ElevatorQueue的getDestination方法获取新的目标楼层,这个方法采用的是一个如下所示的循环,floor是当前电梯所在楼层,direction是当前电梯的运行方向,1为上行,-1为下行。查找策略很好理解,就是比如当前电梯在5层上行,则优先查找5到15层上行的,再查找15到1层下行的,最后查找1到4上行的,电梯的运行轨迹应该类似于LOOK算法。
int i = floor;
if (direction == 1) {
for (; i <= Constant.maxFloor; i++) {
if (hasPassenger(1, i)) { return i; }
}
for (i = Constant.maxFloor; i >= Constant.minFloor; i--) {
if (hasPassenger(-1, i)) { return i; }
}
for (i = Constant.minFloor; i < floor; i++) {
if (hasPassenger(1, i)) { return i; }
}
} else {
for (; i >= Constant.minFloor; i--) {
if (hasPassenger(-1, i)) { return i; }
}
for (i = Constant.minFloor; i <= Constant.maxFloor; i++) {
if (hasPassenger(1, i)) { return i; }
}
for (i = Constant.maxFloor; i > floor; i--) {
if (hasPassenger(-1, i)) { return i; }
}
}
电梯的运行策略是每当这一层有下电梯的乘客或是和当前电梯运行方向相同的乘客时,就开门上下人,如果一位上电梯的乘客的目标楼层比电梯当前的目标楼层要远,那么就更新电梯的目标楼层,这里对“远”的定义如下。
if ((passenger.getToFloor() - destination) * direction > 0) {
destination = passenger.getToFloor();
}
程序中的各个线程如何停止是一个让我思考了很久的问题,在第一次作业中我的解决方法是Input在输入为null时就结束,并将RequestQueue的state变量从ture改为false,Manage当RequestQueue为空且RequestQueue的state==false时结束,并将ElevatorQueue的state设置为false,电梯线程当内外队列均为空且ElevatorQueue的state==false时结束。
从方法层面来看,复杂度最高的两个方法分别是电梯外部队列的getDestination方法和电梯线程的run方法,寻找主请求的方法就是上面已经写到的几个循环,这个没有想到好的解决方法,而电梯线程由于只有一个run方法,所以真的是过于臃肿,在第二次作业中就进行了简化。
这次作业强测和互测都没有被发现bug,同屋也没有bug出现,第一次作业比较简单,也是让我们适应多线程编程,所以大家完成的应该都比较好。
第二次作业
这次作业真的可以称得上被OO支配,因为从周二晚到周六晚,除了听课,所有课下时间都在思考或是写OO,也实现了可以定时投放数据的jar包。然而最后强测得了不到60分,扎心......
还是先把类图放出来再进行分析:
和第一次相比改动主要有三点,第一点是去掉了Input类,改由Main类替代,这个是觉得第一次作业中单独开一个Input线程作用不大;第二点是设计了一个Floor类,可以upStair和downStair,这样就把-1到1的跳跃隐藏了起来;第三点是为了适应这次的迭代任务,Manage可以创建任意数量的电梯,这些电梯都共享一个外部队列。
第二次作业我一直在思考的就是应该一个电梯一个外部队列,由Manage向电梯的外部队列中分派任务,还是应该所有电梯共享一个外部队列去抢夺任务。前者比较好实现,也发挥了调度器的功能,但是我觉得第二种更贴切于实际生活,因为所有从某一楼层经过的电梯都是可以开门上乘客的,不存在分配给某一个电梯对于别的电梯就不可见的乘客。考虑到第三次作业可能增加不同类型的电梯,我采用了第二种设计,准备让同一类型的电梯都共享一个外部队列,在具体的代码实现中有一些需要处理的问题,主要有三点。
第一点是一个电梯到达一个楼层时,如何让别的经过的电梯不在这一层停靠。我为每个楼层增加了一个标志位,表示当前楼层有电梯正在接人,其他的电梯就不需要再停靠了。
第二点是当一个电梯内部队列为空时,获取的主请求是否应该放进电梯的内部队列中去,如果不放入,就有可能会出现几部电梯查找到相同的主请求,向同一个楼层运动而只有一部电梯能接上乘客的情况,如果放入,又存在电梯在接主请求的过程中如果超载,就会导致主请求登不上电梯,需要将放入内部队列的乘客退还给外部队列的情况。我最后采用的是直接放入内部队列的方法。
第三点是线程如何结束的问题,因为主请求虽然放入了内部队列,但是不一定能登上电梯,可能会被退还回外部队列,所以应该等所有电梯都结束运行进入wait状态后,ElevatorQueue再结束,并通知电梯线程也应该结束了。
在中测通过后对代码进行优化时,我想起第一次作业中Elevator只有一个run方法非常的臃肿,所以拆分为move,setDirection,getRequestsOff,getRequestsOn等一系列子方法,结果我修改的时候不小心删去电梯在上乘客的时候对是否超载进行判断的代码,也没有做回归测试。犯了这么低级的错误,强测和互测都真的凉凉。
从复杂度来看,和第一单元没有太大区别,依然是Elevator和ElevatorQueue这两个类的复杂度较高。
实验课
第二次作业成绩不太好,丧失了一点写OO的动力,第三次作业没有一放出来就开始动手写。祸不单行,周三的实验课也没通过。这次实验课主要练习的是Worker Thread模式,要完成的任务是购票系统。下图中的ClientThread相当于TicketBuyer,WorkerThread相当于TicketWindow,除此以外还增加了TicketCenter和TicketPool。
我本次实验未通过的原因是因为TicketBuyer向TicketPool查询是否还有余票和向Channel提交请求应该是一个原子操作,我的解决方法是让TicketBuyer向Channel查询是否有余票,TicketWindow从Channel而不是TicketPool中取票。在Channel中设置缓冲区,每次TicketBuyer向Channel查询有余票时,就从TicketPool取出一张票放在缓冲区中,TicketWindow从缓冲区取票交给TicketBuyer。我觉得更好的方法是修改Channel的putRequest方法,如果没有票就返回false,代表放票失败,相应TicketBuyer线程也就直接结束。因为实验课要求不能修改给定函数的返回值,所以这样并不可行,但是给了我第三次作业一点启发。
第三次作业
两次失败激起了我重新写OO的斗志。这次作业沿用前两次的架构,改动也不是很大,Manage创建三个外部队列,每一类电梯共享一个外部队列,Manage依靠换乘策略分配任务给相应类的电梯。每一类电梯的外部队列增加一个新的变量waitNum,代表有乘客将要换乘这一种类的电梯。
类图如下:
换乘策略如下:
第二次作业我思考的时间很长,感觉有很多东西在设计时觉得挺好,写完后感觉意义不是很大或者是实现起来感觉很复杂,在第三次作业中就删去了。第一个是对于主请求的处理,我把第二单元的放入内部队列改成了依然留在外部队列,虽然会导致多个电梯有相同主请求的情况,但是对性能影响并不是很大;第二个是线程结束的问题,因为进入电梯内部队列的乘客不会返还给外部队列,所以线程的结束顺序和第一次作业相同,依然是在输入为null时将RequestQueue的state变量从ture改为false,Manage当RequestQueue为空且RequestQueue的state==false时结束,并将ElevatorQueue的state设置为false,电梯线程当内外队列均为空且ElevatorQueue的state==false且waitNum==0时结束。
方法的复杂度上,三次作业都没啥大区别。
这次作业完成的比较好,强测互测都没有测出来bug,性能分也很高,测试点得分大部分都在99+,同屋同学测出了一个超载的bug,不知道是不是因为三个电梯载客量不同产生的失误。
SOLID原则
单一职责原则(Single Responsibility Principle)一个类应该只有一个发生变化的原因
开闭原则(Open Closed Principle)一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
里氏替换原则(Liskov Substitution Principle)所有引用基类的地方必须能透明地使用其子类的对象
接口隔离原则(Interface Segregation Principle)类间的依赖关系应该建立在最小的接口上。
依赖倒置原则(Dependence Inversion Principle)上层模块不应该依赖底层模块,它们都应该依赖于抽象。
这一单元的作业不涉及继承和接口,所以主要需要满足SRP和OCP原则。程序中主要是Elevator和ElevatorQueue这两个类比较复杂,方法和功能比较多,每次也都是对这两个类进行修改而不是扩展。第一次依据这些设计原则审视自己的代码,感觉实现的不是很好。
心得体会
1、多线程的调试真的很难,也很玄学,很多bug具有随机性无法复现,所以一定要在设计时下功夫,保证线程安全和程序的鲁棒性。
2、注重程序的设计原则很重要,虽然对于简单问题有些设计很复杂或是没有必要,但是对于养成良好的编程习惯大有裨益。
3、对代码的修改一定要慎之又慎,而且一定要进行回归测试,保证修改或是优化后的代码的正确性。