oo第二单元总结——多线程电梯
前言
第二单元是我们学习oo以来第一次接触多线程。这一单元的三次作业和以前一样,采用了难度递进的方式,而且前两次作业的设计思路在第三次作业都多多少少有些体现(或者说是在其基础上做出的改进)。所以这次博客将以第三次作业为主,对这一单元的作业进行分析。
设计策略
第一次作业是傻瓜电梯的设计,我就严格按照,先来先服务,一次只有一个人在电梯里来设计。第二次增加了捎带请求,也就是说,电梯不再只能有一个人,而是有一个主乘客(主请求),和多个从属乘客(从属请求)。这里的主请求和第一次作业里的只有一个请求时候的那一个唯一的请求其实起到的是相同的作用,就是说实际操控电梯的实际上是他,而电梯中的其余从属请求能做的只是在他要进入或者要出去的时候,命令电梯开关门供他进出。当然,从属请求也有可能成为控制电梯实际运行的主请求,那就是在主请求已经到达目的地,而从属请求还没有达到的时候。这两次作业其实大致上没多大区别,只是第二次作业在调度方法上做了改进,但总体上看,都是两个线程(主线程,在我这儿也就是输入线程,电梯线程)的并发。他们之间的关系与生产者消费者很相似,输入线程往请求队列里投放请求,电梯线程从请求队列里拿出请求,这个请求队列就是两个线程都会访问的临界资源,需要使用synchronized将其锁住,防止两个进程同时对其进行读写的情况发生。输入线程向请求队列输入请求是在任何时候都可以进行的,至于电梯线程什么时候从请求队列取出请求,那就是调度器的职责了。
接下来具体讲一下第三次作业的设计策略。与前两次作业不同,这次作业在以前的基础上由一部电梯改成了三部电梯,而且由以前的电梯每一层都可以到达,变成了不同电梯可以到达特定楼层。
这样就会出现这种情况,有一些请求无法在一部电梯里完成,而是需要多部电梯配合完成。比如一个从-1楼到3楼的请求,电梯A可以到-1楼和1楼但是到不了3楼,电梯C可以到1楼和3楼但是到不了-1楼,这是单独的A、C都完成不了任务,但是让A、C合作,A把乘客从-1运到1,再让把乘客从1运到3,就完成了这个请求。所以一个最容易想到,也最容易实现的方法,就是A、B、C三部电梯各自有自己的请求队列,在输入一个请求时,由调度器决定这个请求应该去往哪个队列。要是能用一部电梯就解决的请求,那就只用一部电梯,比如-1楼到1楼就用A电梯。如果是一部电梯无法解决,比如先前提到的-1到3楼的例子,那就把这个请求拆分成两个请求,一个是-1到1楼的请求,一个是1到3楼的请求,然后这两个请求分别进入A、C两部电梯的等待队列。这里还有一个问题,就是1到3楼必须在-1到1楼的请求执行完成之后执行,我的解决方法是每部电梯设置两个等待队列,一个是就绪队列,一个未就绪队列。就绪队列和我们之前的队列没有分别。而未就绪队列中的请求都是从一个主线程输入的请求拆分成两个请求中的一个,而与它对应的另一个请求在就绪队列中,只有等就绪队列中的这个请求已经执行完成后,这个未就绪队列中的请求才能进入就绪队列,等待调度器调度给电梯。还是用-1到3楼的例子来解释,在这个例子中,我让1到3楼的请求先到C电梯的未就绪队列,-1到1楼的请求到A电梯的就绪队列,这时,调度器可以再合适的时机把-1到1楼的请求给A电梯,但是1到3楼的请求被分配到C电梯的等待队列,只有等-1到1楼的请求执行完了也就是说乘客已经从电梯出来,处于1楼了,它才能进入就绪队列,才有从1楼又进入C电梯的资格。
这种算法容易实现,但是只考虑了正确性的问题,它的性能是很差的。比如设想这样一种情况:一个从1楼到15楼的请求,调度器把这个请求分给了A电梯,在乘客进入A电梯,电梯关门后,又有一个1到15楼的请求到达请求队列,调度器又会把这个请求分配给A电梯,那么A只能把第一个乘客送到15楼,再返回1楼,把第二个乘客送到15楼。1到15楼的请求原本是A、B电梯都可以单独完成的,但是由于调度器的调度方式,是只根据请求自身而不顾电梯的状态来拆分请求。因此,虽然B电梯也可以完成这个任务,但由于调度方式的原因,明明B没人用,A处于忙碌状态,但是调度器认定了A电梯,就一根筋的只把任务分配给A,导致了电梯资源的浪费。
度量程序结构
1.代码统计
method ev(G) iv(G) v(G)
Dispatcher.Dispatcher() | 1.0 | 1.0 | 1.0 |
Dispatcher.geta() | 1.0 | 4.0 | 8.0 |
Dispatcher.getb() | 1.0 | 4.0 | 8.0 |
Dispatcher.getc() | 1.0 | 4.0 | 8.0 |
Dispatcher.getRequestsA() | 1.0 | 1.0 | 1.0 |
Dispatcher.getRequestsB() | 1.0 | 1.0 | 1.0 |
Dispatcher.getRequestsC() | 1.0 | 1.0 | 1.0 |
Dispatcher.getWaita() | 1.0 | 1.0 | 1.0 |
Dispatcher.getWaitb() | 1.0 | 1.0 | 1.0 |
Dispatcher.getWaitc() | 1.0 | 1.0 | 1.0 |
Dispatcher.put(PersonRequest) | 1.0 | 4.0 | 4.0 |
Dispatcher.requestsadc() | 1.0 | 2.0 | 3.0 |
Dispatcher.setExit() | 1.0 | 1.0 | 1.0 |
Dispatcher.sort1(PersonRequest) | 1.0 | 23.0 | 23.0 |
Dispatcher.sort2(PersonRequest) | 1.0 | 9.0 | 9.0 |
Dispatcher.sort3(PersonRequest) | 1.0 | 6.0 | 7.0 |
Dispatcher.waitsadc() | 1.0 | 3.0 | 3.0 |
Elevator.arrive() | 1.0 | 19.0 | 20.0 |
Elevator.call() | 1.0 | 5.0 | 5.0 |
Elevator.close() | 1.0 | 14.0 | 15.0 |
Elevator.downfloor() | 1.0 | 1.0 | 2.0 |
Elevator.Elevator(Dispatcher,int,int) | 1.0 | 1.0 | 1.0 |
Elevator.getFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getThread() | 1.0 | 1.0 | 1.0 |
Elevator.in(int) | 1.0 | 1.0 | 1.0 |
Elevator.move() | 1.0 | 5.0 | 5.0 |
Elevator.open() | 1.0 | 2.0 | 2.0 |
Elevator.out(int) | 1.0 | 7.0 | 7.0 |
Elevator.run() | 1.0 | 6.0 | 6.0 |
Elevator.sleep() | 1.0 | 2.0 | 2.0 |
Elevator.start() | 1.0 | 2.0 | 2.0 |
Elevator.threadsleep(int) | 1.0 | 2.0 | 2.0 |
Elevator.upfloor() | 1.0 | 1.0 | 2.0 |
Main.main(String[]) | 3.0 | 3.0 | 3.0 |
Total | 36.0 | 140.0 | 158.0 |
Average | 1.0588235294117647 | 4.117647058823529 | 4.647058823529412 |
2.类图
运行Main类的初始进程即是三部电梯线程的父进程(创建三个电梯进程),同时扮演着输入线程的角色。他不断向请求队列(请求队列位于调度器中)输入请求。再有调度器处理这个请求,将处理后的1~2个请求加入到A、B、C的就绪队列和未就绪队列中。最后电梯线程执行到适当的时机,调度器会根据情况把这部电梯的就绪队列中的请求给到相应电梯来执行,或者将未就绪队列中的请求移动到就绪队列中。这种结构好处在于简单明了,只有三个类,分别负责请求输入,电梯运行和电梯调度,分工明确,思路简单。但是这种写法带来的弊端就是调度器类过于冗长,在写调度器的时候容易顾此失彼。而且由于调度器全部都写在一个类中,修改时要考虑的东西很多,改debug和优化带来了不小的难度。
心得体会
多线程与单线程最大的不同在于线程安全问题。单线程你只需要想着你在这一个线程中所要实现的功能,如果要上难度,也只会是在语法上上难度。而多线程不同,你必须考虑总体架构,必须考虑线程间的通讯,考虑不同线程的同步与互斥。所以在设计时,我们要考虑设计的原则问题:单一责任原则、开放封闭原则、里氏替换原则、接口分离原则、依赖倒置原则。
bug分析
- 发现了以下几种bug,对应解决方案
- 程序不退出:我最开始的关于程序退出的设计,是在输入进程的输入结束时,向调度器发出一个信号end,调度器会在收到了end信号、请求队列的已经为空,且电梯中的任务也已经执行完毕在等待新的任务的时候,由电梯进程调用调度器来退出程序。在第一次作业中,当电梯进程由于等待输请求而wait的时候,输入进程向调度器发出end信号,但此时电梯进程还在睡眠,等待输入进程输入请求来唤醒,但此时输入进程已经结束,没有进程来唤醒电梯进程,也就没有进程可以调用已经接收到end信号的调度器来结束程序,这样电梯进程就永远处于睡眠状态,无法退出。解决方案是让电梯进程在睡眠的时候,隔一段时间自己醒一次,但是这种唤醒不是因为继续执行的条件满足而唤醒的,是一种虚假唤醒,会接着进入睡眠状态(一般的wait都用在while语句中,这里不详做说明了),但是再又一次进入睡眠之前,可以在这个while中加入判断是否退出程序的语句段,这样就不存在前面说的问题了。第三次作业中要加入count来计数有几个电梯正在等待请求,只有在三个电梯都执行完所有任务并且在等待新任务时,才能退出。
- 调度方法问题:这个主要出现在第三次作业,由于调度放方法不当,电梯在不该停留的楼层执行了开关门操作。