OO-2021-buaa 第二单元
OO第二单元博客总结
引言
第二单元的作业以Java多线程设计为重点,通过电梯调度的设计,来掌握多线程的设计与处理,以及理解生产者—消费者模式、多个线程的交互、线程安全等方面的知识。
这一次的作业,对我来说是难度逐步递减的,第一次因为对多线程不熟悉,写的很慢,同时线程安全性也存在问题,导致最终到了ddl还没有debug完成;第二次作业进行了重构,艰难完成了中测,但使用的look调度算法有问题,强测有多条rtle;第三次作业因为没有设计换程的原因,相对简单,同时修改了第二次作业存在的调度上的bug后,强测也是顺利通过。总的来说,这一单元的作业,难度主要在于线程的处理,以及课下测试的方法,需要自己构造些有效的样例,进行课下调试。
第一次作业
第一次作业的目标是模拟单部多线程电梯的运行。
1.架构设计
-
类的分析:
本次作业只有一部电梯,需要生产者-消费者模式相对了解。在电梯运行时,存在两个线程:Input和Elevator。通过一个Queue用来存储等待队列。Main用来启动线程。这一次的作业我没有设计调度器。 -
线程同步和锁:
本次作业中多线程交互主要通过Personrequest,当Input为生产者,得到一个有效输入时,通过Putrequest加入等待队列,当输入结束后,Putrequest传入结束信息break。
在Queue中,所有方法加上synchronized关键字来保证线程安全。
Elevator为消费者,在运行时,需要访问等待队列并获取锁,若存在请求则运行,当没有请求且输入未结束,则进入wait状态,等待生产者的唤醒。当确认到结束信息break且电梯结束所有请求后,终止程序。 -
电梯调度算法
本次电梯使用LOOK算法,其主要内容为:- 正常模式下电梯以1楼和20楼为转交,进行上下移动;
- 如果电梯没人,查看等待队列,有请求则按请求方法行动。如果电梯有人,则判断开门楼层,向开门楼层运动。
- 总的来说就是电梯移动到请求的最外道就回转,反方向查找服务。
在模式选择上,我选择将所有模式都当做
Random来处理,这样处理下Night和Random的性能差不多,而Morning的性能会差一些。
2.性能分析
- 类图

- 度量分析
| class | OCavg | OCmax | WMC |
|---|---|---|---|
| Elevator | 3.2 | 7.0 | 32.0 |
| Input | 1.67 | 3.0 | 10.0 |
| Main | 1.0 | 1.0 | 1.0 |
| Queue | 2.4 | 5.0 | 12.0 |
| Total | 55.0 | ||
| Average | 2.5 | 4.0 | 13.75 |
从表中可以看出,我的逻辑复杂度主要集中在电梯类上,因为我将电梯运行与状态都放在Elevator中。
- 方法复杂度分析
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Elevator.Elevator(Queue) | 0.0 | 1.0 | 1.0 | 1.0 |
| Elevator.close() | 1.0 | 1.0 | 2.0 | 2.0 |
| Elevator.move() | 3.0 | 1.0 | 2.0 | 3.0 |
| Elevator.open() | 1.0 | 1.0 | 2.0 | 2.0 |
| Elevator.peopleout() | 3.0 | 1.0 | 3.0 | 3.0 |
| Input.Input(Queue) | 0.0 | 1.0 | 1.0 | 1.0 |
| Input.getfrom(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Input.getid(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Input.getto(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Main.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
| Queue.getRequests() | 0.0 | 1.0 | 1.0 | 1.0 |
| Queue.putway(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Queue.getRequest(int,int) | 5.0 | 2.0 | 5.0 | 6.0 |
| Queue.putRequest(String) | 1.0 | 2.0 | 2.0 | 2.0 |
| Elevator.peoplein() | 3.0 | 3.0 | 3.0 | 3.0 |
| Elevator.run() | 11.0 | 3.0 | 7.0 | 8.0 |
| Input.get(String,int) | 3.0 | 3.0 | 3.0 | 3.0 |
| Input.run() | 5.0 | 3.0 | 4.0 | 4.0 |
| Elevator.isUpordown() | 4.0 | 4.0 | 1.0 | 4.0 |
| Elevator.needstop() | 7.0 | 4.0 | 3.0 | 4.0 |
| Queue.validask(int) | 6.0 | 5.0 | 3.0 | 6.0 |
| Elevator.isturn() | 15.0 | 7.0 | 6.0 | 9.0 |
| Total | 68.0 | 48.0 | 54.0 | 67.0 |
| Average | 3.09 | 2.18 | 2.45 | 3.05 |
从表中看,复杂度主要在isturn上,这是look算法的判断方法,而本电梯上下逻辑主要在look上,因此相对复杂。
3.bug分析
本次作业因为对多线程的不熟悉,导致线程安全出现了一些问题,使得电梯无法正常停止,直到ddl也没有完成debug,在课后的自我分析中,我发现,当我把电梯wait后,判断电梯需要停止时,没有重新notifyall,这会使得电梯没法停止,同时,电梯的look算法也有一些问题,这个问题将会在作业二和作业三中明显的反映出来。
第二次作业
第二次作业的目标是模拟多部多线程电梯的运行,并且可以动态增加电梯。
1.架构设计
-
类的分析:
这一次的电梯是多部电梯,因此我设置了Schedule作为调度,增加了Elechoice用来存储相应电梯的等待队列,将Input放入Main中。 -
线程同步和锁:
本次电梯在Main中获取输入后,将Personrequest加入到Schedule中,在Schedule中对其进行调度,为其分配一个电梯,放入此电梯的等待队列。在Schedule中所有方法仅为对等待队列的分享,且使用synchronize锁住,保证线程安全。而在Elevator则与第一次类似。最后将输出加锁以保证输出的安全。private static final Object OUT_LOCK = new Object(); private void printf(String s) { synchronized (OUT_LOCK) { TimableOutput.println(s); } } -
电梯调度算法
本次电梯刚开始仍使用LOOK算法,但后来发现写的有一些问题,改为了更为暴力的Scan方法。
2.性能分析
-
类图

-
协作图

-
度量分析
| class | OCavg | OCmax | WMC |
|---|---|---|---|
| Elechoice | 1.0 | 1.0 | 5.0 |
| Elevator | 3.67 | 8.0 | 33.0 |
| Main | 2.0 | 6.0 | 10.0 |
| Schedule | 1.33 | 2.0 | 8.0 |
| Total | 58.0 | ||
| Average | 2.15 | 3.6 | 11.6 |
从表中可以看出,我的逻辑复杂度仍主要集中在Elevator中。
- 方法复杂度分析
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Elechoice.Elechoice(PersonRequest,int) | 0.0 | 1.0 | 1.0 | 1.0 |
| Elechoice.getRequest() | 0.0 | 1.0 | 1.0 | 1.0 |
| Elechoice.setChoose(int) | 0.0 | 1.0 | 1.0 | 1.0 |
| Elechoice.setRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
| Elevator.Elevator(Schedule,int,int,int,String,int) | 0.0 | 1.0 | 1.0 | 1.0 |
| Elevator.move() | 3.0 | 1.0 | 2.0 | 3.0 |
| Elevator.printf(String) | 0.0 | 1.0 | 1.0 | 1.0 |
| Elevator.someoneout() | 3.0 | 1.0 | 3.0 | 3.0 |
| Main.getNumofele() | 0.0 | 1.0 | 1.0 | 1.0 |
| Main.isRunning() | 0.0 | 1.0 | 1.0 | 1.0 |
| Main.setNumofele(int) | 0.0 | 1.0 | 1.0 | 1.0 |
| Main.setRunning(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
| Schedule.Schedule() | 1.0 | 1.0 | 2.0 | 2.0 |
| Schedule.getEleq() | 0.0 | 1.0 | 1.0 | 1.0 |
| Schedule.getElequeue() | 0.0 | 1.0 | 1.0 | 1.0 |
| Schedule.getPersonreq() | 0.0 | 1.0 | 1.0 | 1.0 |
| Schedule.needtowait() | 3.0 | 1.0 | 3.0 | 3.0 |
| Schedule.put(PersonRequest,int) | 0.0 | 1.0 | 1.0 | 1.0 |
| Elevator.run() | 21.0 | 3.0 | 9.0 | 10.0 |
| Elevator.someonein() | 6.0 | 3.0 | 4.0 | 5.0 |
| Main.main(String[]) | 10.0 | 3.0 | 7.0 | 7.0 |
| Elevator.getStatus() | 11.0 | 4.0 | 6.0 | 9.0 |
| Elevator.in() | 5.0 | 4.0 | 4.0 | 5.0 |
| Elevator.out() | 4.0 | 4.0 | 2.0 | 4.0 |
| Total | 67.0 | 42.0 | 59.0 | 68.0 |
| Average | 2.48 | 1.56 | 2.19 | 2.52 |
从图中可以看到复杂度主要在Elevator.run()上,这是对电梯开关门以及上下状态的判断,虽然已将判断封装为类似in()和out()的方法中,但还是避免不了过于复杂。
bug分析
这一次作业的bug主要在look算法和输出安全上。
我的look算法最初的设计是当楼层在1时,就上楼;在20时,下楼;之后在判断请求的情况,用请求的判断结果覆盖之前的判断。这存在一个问题,那就是当不在1层或20层的时候是什么样的状态。我在作业中设定了一个status,其中
- 1:上楼
- 2:下楼
- 3:停止并结束
而我对上下楼的判断中,将0也作为下楼,这会导致电梯会在1-2之间无线徘徊,导致电梯tle,但这种情况不是每一次都会出现的,因此直到强测我才发现,同时也比较难以复现。为修改这个bug,我将look算法直接简化为scan算法,并将1层和20层对up进行取反来模拟反向掉头的情况。
输出安全是体现在输出时是否考虑到线程安全,通过同学和讨论区的一些提醒,我很快修改了这个bug。
第三次作业
本次作业要求模拟多部不同型号电梯的运行。型号不同,则开关门速度,移动速度,限载人数,可停靠楼层不同。
1.架构设计
-
类的分析:
这一次的电梯与第二次作业基本相同,只是增加了一下不同型号电梯的处理。 -
线程同步和锁:
本次电梯并未采用换乘的模式,只是将请求简单的分为A,B,C三类,分别由A,B,C电梯运送 -
电梯调度算法
本次电梯刚开始仍使用LOOK算法,但后来发现写的有一些问题,改为了Scan方法。
2.性能分析
- 类图

-
协作图

因为没有加入换乘的缘故,第三次作业的时序图和第二次基本相同。
-
度量分析
class OCavg OCmax WMC Elechoice 1.0 1.0 5.0 Elevator 3.89 8.0 35.0 Main 1.8 5.0 9.0 Schedule 1.67 3.0 1.0 Total 61.0 Average 2.26 3.6 12.2 这一次的与上一次的类复杂度差不多,主要增加了点对不同型号电梯的处理。
-
方法复杂度分析
method CogC ev(G) iv(G) v(G) Elechoice.Elechoice(PersonRequest,int) 0.0 1.0 1.0 1 Elechoice.getChoose() 0.0 1.0 1.0 1 Elechoice.getRequest() 0.0 1.0 1.0 1 Elechoice.setChoose(int) 0.0 1.0 1.0 1 Elechoice.setRequest(PersonRequest) 0.0 1.0 1.0 1 Elevator.Elevator(Schedule,int,int,int,String,String) 3.0 1.0 2.0 3 Elevator.move() 3.0 1.0 2.0 3 Elevator.printf(String) 0.0 1.0 1.0 1 Elevator.someoneout() 3.0 1.0 3.0 3 Main.getNumofele() 0.0 1.0 1.0 1 Main.isRunning() 0.0 1.0 1.0 1 Main.setNumofele(int) 0.0 1.0 1.0 1 Main.setRunning(boolean) 0.0 1.0 1.0 1 Schedule.Schedule() 1.0 1.0 2.0 2 Schedule.getEleq() 0.0 1.0 1.0 1 Schedule.getElequeue() 0.0 1.0 1.0 1 Schedule.getPersonreq() 0.0 1.0 1.0 1 Schedule.needtowait() 3.0 1.0 3.0 3 Schedule.put(PersonRequest,int) 5.0 1.0 2.0 5 Elevator.run() 21.0 3.0 9.0 10 Elevator.someonein() 6.0 3.0 5.0 6 Main.main(String[]) 9.0 3.0 6.0 6 Elevator.getStatus() 11.0 4.0 6.0 9 Elevator.in() 5.0 4.0 5.0 5 Elevator.out() 4.0 4.0 2.0 4 Total 74.0 42.0 62.0 74 Average 2.74 1.56 2.30 2.74 从表中得到,在
Elevator.run和Schedule.put(PersonRequest,int)比上次复杂一点。
3.bug分析
第三次作业的bug与第二次作业是一样的,主要是这次的错误是在中测就复现出来的,而第二次作业是在强测复现的,因此两个作业是一起修改的。
hack方法
第一次没过,没资格hack,后面的hack也是通过阅读代码来观察分析,并且自己构造一些样例,手动输入(痛苦),效率极低且没啥用。
重构经历
第一次作业没过,因此第二次作业进行了重构。我重构的方面主要在于等待队列与电梯线程的交互中,第一次作业中,我在等待队列进行了一些不太安全的交互,导致后面的debug过程过于艰难,在第二次的重构中,我吸取教训,在调度中进行线程间的交互,且将交互对象明确,避免线程间的不安全。
心得体会
- 第二单元相对第一单元来说,是一个难度逐渐降低的过程,在一次次的作业中,会对多线程更加理解,使得后面的作业逐渐变得没有那么困难;而第一单元是到了后面会更加复杂。
- 这一次的作业只经历过一次重构,第三次作业也是在第二次作业的基础上完成的,相比第一次的次次重构,更加轻松,这说明在一开始就构造一个合适的结构是非常重要的,在做作业之前应该仔细思考再动手。
- 这次作业也是我理解到多线程的复杂,以及维持线程安全的重要性。

浙公网安备 33010602011771号