面向对象第二单元总结

一、作业设计策略 1 第一次作业 第一次作业是让我们做一个单部多线程傻瓜调度电梯的模拟,我是分成两个线程来处理,一个电梯线程,一个调度器线程。电梯线程并不知道

一、作业设计策略

1.第一次作业

第一次作业是让我们做一个单部多线程傻瓜调度电梯的模拟,我是分成两个线程来处理,一个电梯线程,一个调度器线程。电梯线程并不知道自身所处理的请求,只有移动、开关门等操作,每完成一个动作,都会向调度器发出请求,获取下一个动作,类似于一个状态机。调度器线程就负责保存各个请求,当接收到电梯的请求时,返回下一个动作即可。在这次作业中,电梯使用的是暴力轮询的方法,毕竟没有这方面的要求,而且实现也比较简单。

这两个线程是一个单向的轮询请求,“电梯”线程不断轮询请求“调度器”线程,调度器线程同时负责输入请求的处理。每当电梯完成一个请求后,则将处理新的待处理请求,当待处理请求为空时,则控制电梯停止。当输入中止时,调度器设置flag。电梯线程继续处理尚未完成的请求,当所有请求都已处理完毕,则检查flag并退出。

这个结构比较简单,但是暴力轮询的话,CPU时间会比较长,尽量还是使用notify和wait来实现。

2.第二次作业

第二次作业要求我们的电梯可以进行捎带,如果只是把请求用一个列表存起来,那么对于电梯还是调度器来说,都很难知道当前楼层是否需要开门接人。所以这次我对请求的存储结构进行了一些调整,主要是在电梯里模拟了一栋楼的结构出来,每次有新请求时,则将该请求加入到对应的“楼层”中去,每当一个乘客被接上,那么就在OUT的楼层加入一个请求。这样电梯每到一个楼层就知道自己在该楼层是否有请求,而且还可以知道高楼层和低楼层是否还有可以接受的请求。这样电梯掌握的信息就比较多,可以了解每个楼层的信息,自己决定怎么去处理这些请求。在这种设计策略下,调度器的功能就比较小了,因为只有一部电梯,那么只需要把收到的请求全部送给电梯即可,还有确认当前是否已经终止输入。

这次作业中,因为要避免暴力轮询,所以在“电梯”线程需要wait,“调度器”线程需要去notify“电梯”线程。而且这次由于结构的修改,“调度器”线程还需要向“电梯”线程加入请求。线程内部,“电梯”线程需要自己处理已经接受的请求,并在没有请求的时候wait;“调度器”线程需要检查输入是否停止,当输入停止时,结束该线程。线程协同,“电梯”线程需要在全部请求处理完毕时,检查“调度器”线程是否已经结束;“调度器”线程需要给“电梯”线程发送请求,并且在结束时设置flag,由于“电梯”线程可能处于wait,所以“调度器”线程在执行这两个动作时,需要notify“电梯”线程。

由于只有两个线程,一个“生产者”,一个“消费者”,所以实现还是比较简单的,对性能影响比较大的是电梯的调度问题。我才用的调度和助教的很像,只是对捎带的要求比较低,只要是能够IN和OUT的都处理,不存在要判断方向的问题。

3.第三次作业

 第三次作业是让我们实现多个不同电梯的模拟。在这次作业中,每个电梯的运行速度,载客量和可停靠楼层都不一样,所以我对电梯增加了一些属性。不过设计策略还是沿用第二次作业的策略,每当有一个新请求发出时,直接将请求分配给负担较轻的电梯中去。由于每个电梯的可停靠楼层不一样,所以存在一个请求需要多个电梯实现的问题,所以电梯线程都必须在所有请求都处理完毕的情况下才能结束,所以每个电梯都需要可以在结束的情况,唤醒其他电梯线程的能力。这次调度器还增加了请求的拆分,以及对请求的分配。

这次作业,除了第二次作业中的“电梯”线程和“调度器”线程之间的协同,增加了“电梯”线程和“电梯”线程之间的协同,来解决转乘的问题。当一个电梯完成一个转乘请求后,直接调用对应电梯的add方法,将请求加入到对应电梯中。在输入结束后,“调度器”线程自身设置flag,并notify所有的“电梯”线程。当一个“电梯”线程完成自身请求的时候,“调度器”是否以及退出,以及其他“电梯”线程是否处理完所有请求。因为可能存在转乘请求,所以其它“电梯”还有请求的时候还不能退出。当满足这些请求时,结束线程,因为有可能只调度了一部电梯,其他电梯还处于wait状态,所以需要在结束之前notify其它“电梯”线程。

总的来说,这个作业的线程协作实现还是比较简单,主要麻烦的地方在于调度,需要转乘的请求如何分解这些问题上面。

二、程序分析

一些缩写的说明:

LOC:代码行数

CC:圈复杂度

PC:方法参数个数

NOF:类的属性个数

NOPF:类的public属性个数

NOM:类的方法个数

NOPM:类的public方法个数

ev(G):essential cyclomatic complexity,重要圈复杂度.

iv(G):设计复杂度

v(G):cyclomatic complexity,圈复杂度

第一次作业

 

第二次作业


 

 第三次作业

 

 优缺点

 第一次作业的电梯功能较少,结构和逻辑比较简单,但是调度器的逻辑有些复杂,而且如果加入优化,那么调度器需要掌控电梯的很多信息,设计相对复杂;第二次、第三次作业则将请求队列加入到电梯中,并修改了存储方式,代码逻辑比较清楚,各个部分都比较简单,同时减少了调度器的复杂程度,相比于简单调度有了些许的提升,但是很难继续提升性能。

SOLID原则分析

Single Responsibility

这三次作业中,电梯类的职责从单一动作变成了可以处理多个请求,把处理过程交给电梯,设计相对简单,而调度器的职责就只有请求的接收和分发。这两个类的功能较为单一,耦合度也有一定的下降。

Open / Closed

第一次和第二次作业之间有着设计模式上的修改,第二次和第三次之间则可以使用继承来进行拓展,但是我是用了增加属性的方法来拓展功能,对一些方法进行了一些修改, 增加了一些分支,确实是违背了这个原则。

Liskov Substitution、Dependency Inversion

这三次作业场景比较简单, 没有用到继承和抽象。

Interface Segregation

这三次作业,由于各个类的功能比较单一,所以只留了一些必要的接口,主要是这次作业就被我分成两个类来处理,都是两个类之间的交互,问题可能不是很大,但是如果加入更多的类的话,可能需要在设计阶段就要考虑这些事情。

 三、程序bug

第一次作业如果做的比较简单的话,调度上面使用简单的先来先服务调度的话,很少有逻辑上的bug。比较有可能出现的bug地方就是可能没有判断当前是否停止终止或者还有请求没完成的情况下,就结束了电梯线程。

第二次作业,由于不能暴力轮询,需要使用 notify和wait方法,这个需要考虑线程之间的协作问题,避免出现notify比wait先执行的现象,导致线程无法notify起来。还有就是对公共的数据进行加锁,以免出现bug。

第三次作业,增加了两部电梯,但实际上逻辑是差不多的,需要考虑的情况会多一些,因为是三个电梯线程,所以需要想办法让三个电梯一起结束。

四、发现bug策略

发现别人的bug一般要先清楚对方的电梯运行逻辑。着重观察程序对于公共数据的处理,以及怎么结束线程。不过由于没有互测,只能对自己的程序找找bug,我一般先写个测试程序用来产生相应的输出,再配合管道运行。一般出现的bug就是电梯线程没有办法正常结束,问题大部分都是条件设置的问题,比较容易解决,但是不太好发现,需要使用printf大法。

由于第二单元是多线程,相对于第一单元,bug更加难以发现和复现,往往需要经过很多次测试才出现一个bug现象,而且printf也会影响bug的出现,所以调试是相当困难的,最好在设计阶段就已经设计好线程间的协同和同步。

五、心得体会

这三次作业让我了解了Java的多线程,对于多个线程之间的协同和公共数据的保护也有基本的了解和实践。

在线程安全方面,我们可以通过加锁等操作使操作原子化,避免出现读到脏数据或者写丢失等问题,也保证了部分代码逻辑的正确性。还有先notify再wait的问题,我们可以通过加锁等操作,使得check-wait原子化,当先notify的时候,不会进入wait;当先wait的时候,可以成功notify。而且这次我的wait和notify都是对函数加锁,如果使用一个公共的对象,类似于producerMonitor之类的,可以更加灵活。如果要加两个以上的锁,那么要让每个线程的加锁顺序一致,以免出现死锁的问题。

在设计原则上面,了解了一些程序设计应该遵循的原则——SOLID原则。在设计阶段,遵循这些原则可以让我们的代码逻辑更加简单,功能拓展更加方便,同时减少很多潜在的bug。不遵循这些设计原则,也可以很快完成这些功能,但是很难拓展,比如第一次到第二次作业,基本上是重写,由此带来的工作量有点大。