面向对象第二单元总结
面向对象第二单元总结
第二单元三次作业分别为单步多线程可稍带电梯、多部多线程可稍带调度电梯以及增加功能和限制条件的多部多线程可稍带电梯。三次作业关联性很强,有很多需要迭代继承的部分,在开发时也可以针对需求对必要的地方进行重构。
1.多线程的运用与同步控制
-
第一次作业
第一次作业我采用了生产者-消费者模式,将输入类作为生产者,将电梯类作为消费者。在此次作业中我没有单独设立调度器,而是把电梯的调度逻辑放在了电梯的run方法中。对他们的共享变量——存储等待乘客的waitingqueue采取同步控制的方法。输入的时候对waitingqueue上锁,电梯内判断电梯无等待乘客和内部乘客时上锁。其他采用同步方法进行操作。这次的控制机制比较简单,是一对一的多线程同步问题。
-
第二次作业
第二次作业我延续了生产者-消费者模式,但是在输入时不是将所有乘客都放入同一个waitingqueue中,而是通过随机数算法放入不同的waitingqueue中并同时进行上锁,每个电梯只能对自己的waitingqueue进行上锁和处理。所以,表面上第二次作业是一对多的多线程同步问题,但是实质上还是一对一的多线程同步问题。
-
第三次作业
第三次作业我改变了架构,新设立了schedule调度类,并且将电梯线程放入schedule类里面新建和运行。这样方便于对输入进行解析后再放入相应等待队列中,也是为了电梯类位置的一致。我将请求队列分成了三类,分别作为a、b、c类的等待队列。我的方法是对输入进行处理,判断是否需要换乘,并且放入相应的电梯类的队列中,进行上锁操作。
第三次作业中的另一个难点在于结束的判断,一种保险的做法是等到所有乘客均被送达才可以结束。这就需要判断结束的时候综合所有电梯的状态。我的方法是让所有电梯共享一个busy变量,运行时对其加一,结束时对其减一。在结束信号到来后,直到busy信号变为0才可以开始结束操作。
2.性能设计和可拓展性分析
-
电梯策略与运行策略
对于单部电梯,基于对人乘坐电梯的基本认识,我在第一次作业采用了als调度算法,但是发现还没有scan和look算法的性能好,所以在第二第三次作业采用了scan算法。
在第三次作业中,由于楼层的限制,需要对换乘进行规划。我的规划较为死板,按照固定的逻辑判断换乘电梯和楼层,再用随机数算法加入到一个waitingqueue里面。这样做,针对特定数据的性能可能会有不足。但是通过修改分配函数,可以达到更好的性能,如:分析各个电梯的拥挤状况,结合电梯速度等,进行更好的分配。
我认为我的架构对拓展还是比较友好的,无论是增加其他楼层要求的电梯,还是删去电梯等等。schedule类中的函数具有一定的覆盖性,没有特别针对性的算法,但是需要进行相应的抽象。
3.从度量方面看程序结构
- 第一次作业和第二次作业
第一次和第二次作业的逻辑较为简单,各个类和模块之间的关系也较为清楚明白,故只做简要分析。
我认为第一次作业没有必要将电梯运行逻辑单独提出做为一个类。第一是因为代码逻辑较为简单,不需要更复杂的结构就能完成任务,第二是因为如果提出,电梯只剩输入输出功能,名存实亡,反而没有必要设立电梯类。
对第二次作业来说,我认为可以建立一个简单的调度类,主要负责将乘客分配给合适的电梯以及传递结束信号。
- 第三次作业
Schedule.setend() | 1.0 | 5.0 | 6.0 |
---|---|---|---|
Schedule.Schedule() | 1.0 | 1.0 | 1.0 |
Schedule.findc(int) | 3.0 | 1.0 | 3.0 |
Schedule.findb(int) | 3.0 | 1.0 | 3.0 |
Schedule.finda(int) | 3.0 | 1.0 | 3.0 |
Schedule.addtransfer(Person) | 1.0 | 4.0 | 4.0 |
Schedule.addperson(int,int,int) | 1.0 | 22.0 | 31.0 |
Schedule.addelevator(String,String) | 1.0 | 4.0 | 4.0 |
Safeoutput.println(String) | 1.0 | 1.0 | 1.0 |
Personlist.size() | 1.0 | 1.0 | 1.0 |
Personlist.setEnd(boolean) | 1.0 | 1.0 | 1.0 |
Personlist.remove(int) | 1.0 | 1.0 | 1.0 |
Personlist.Personlist() | 1.0 | 1.0 | 1.0 |
Personlist.getEnd() | 1.0 | 1.0 | 1.0 |
Personlist.get(int) | 1.0 | 1.0 | 1.0 |
Personlist.add(Person) | 1.0 | 1.0 | 1.0 |
Person.Person(int,int,int,boolean,int,int) | 1.0 | 1.0 | 1.0 |
Person.getTransfertotype() | 1.0 | 1.0 | 1.0 |
Person.getTransferfloor() | 1.0 | 1.0 | 1.0 |
Person.getTofloor() | 1.0 | 1.0 | 1.0 |
Person.getNeedtransfer() | 1.0 | 1.0 | 1.0 |
Person.getId() | 1.0 | 1.0 | 1.0 |
Person.getFromfloor() | 1.0 | 1.0 | 1.0 |
Mainclass.main(String[]) | 1.0 | 1.0 | 1.0 |
Forminput.run() | 3.0 | 6.0 | 6.0 |
Forminput.Forminput(Schedule) | 1.0 | 1.0 | 1.0 |
Elevator.run() | 6.0 | 14.0 | 16.0 |
Elevator.open() | 1.0 | 1.0 | 2.0 |
Elevator.move() | 1.0 | 1.0 | 8.0 |
Elevator.getout() | 3.0 | 6.0 | 6.0 |
Elevator.find() | 3.0 | 1.0 | 3.0 |
Elevator.Elevator(Personlist,String,int,Schedule,ArrayList) | 1.0 | 1.0 | 4.0 |
Elevator.close() | 1.0 | 1.0 | 2.0 |
Elevator.carrywith() | 4.0 | 9.0 | 9.0 |
Elevator.canopen() | 7.0 | 7.0 | 9.0 |
Elevator.cangetout() | 5.0 | 3.0 | 5.0 |
Elevator.caldir(int,int) | 3.0 | 1.0 | 3.0 |
Elevator.arrive() | 1.0 | 1.0 | 1.0 |
Total | 70.0 | 108.0 | 146.0 |
Average | 1.8421052631578947 | 2.8421052631578947 | 3.8421052631578947 |
总体来看,第三次作业复杂度还可以接受,但是由于电梯类中的run方法包括了单部电梯的运行逻辑,所以复杂度略高。对输入的personrequest的处理也较为繁琐,引起了复杂度的增高。第三次作业中,schedule类主要负责分配输入以及对电梯发出终止信号。
关于bug
- 自己的
前两次作业比较简单,我基本没有处理过实质性的bug,只有一些简单的结构问题和细节问题,故不再赘述。
第三次作业中,我的程序产生了一个有趣的bug,可以与大家分享一下,希望大家引以为戒。由于我的设计,在每个电梯进入阻塞状态前,会先修改busy变量并且notify。而schedule类中的setend方法被唤醒继续执行。在我的设计中,setend方法可能先于最后一个电梯进入阻塞状态之前进行notify,就会使这个线程无法结束,造成问题。针对这个问题,我将setend方法中的查询busy操作设为每0.5秒执行一次,既不会耗费太长的cpu时间,又可以在所有电梯都进入阻塞状态后再执行。
- 别人的
这几次作业,我没有采用特殊的策略去hack别人的bug,而是随机生成了一些数据,并结合我的经验,手动编写一些可能会出现问题的数据进行测试,比如针对换乘和同步结束等等。
心得体会
这一单元,我们学习了多线程编程。其中,多线程的同步是重中之重。对于锁的理解和使用决定了多线程编程的质量。生产者-消费者模式、工厂模式的运用等等是多线程编程的重要工具和手段,在编程时可以进行有效利用。在互联网编程的很多地方,都有多线程编程和同步的思想——不管是app中消息的实时刷新,还是游戏中各个玩家的实时数据交互。学了这一单元后,也让我对这些方面有了更深的理解与思考。