OO第二单元总结

一、总结三次作业的设计策略

从多线程的协同和同步控制方面,分析和总结自己三次作业的设计策略。总的来说,我经过三次作业对多线程程序的设计,认为简单可扩展的架构是最好的架构,这种架构不容易出多线程线程安全的bug,思路简单清晰是最好的。

1.第5次作业(FCFS傻瓜电梯)

在我的第5次作业,我本着简单而可扩展的原则,设计了4个类,Elevator.java,Main.java,RequestQueue.java,SingleRequest.java。会有两个线程产生。主线程和电梯线程在Main.java中创建,主线程完成输入读取,并将请求存放于RequestQueue.java,RequestQueue.java中请求的读取和写入都运用了synchronized关键字进行线程安全的保护,保证电梯线程和主线程对RequestQueue的访问不会冲突。由于采用傻瓜式调度,电梯线程每次从RequestQueue中抓取第一条请求执行即可。

 

在线程结束控制方面,主线程在输入结束时结束,并在结束前给电梯线程发信号。电梯线程结束的条件是,主线程结束并且RequestQueue为空。当电梯线程也结束,程序中两个线程都结束程序退出。

 

回顾起来,当时我还写了一个调度器线程,其实完全没必要,调度器可以和队列类写在一起而且不需要新开线程,FCFS傻瓜电梯调度工作很简单,主要做好队列的同步访问和线程结束条件的判断就可以。

 

 

2.第6次作业(ALS可捎带电梯)

在我的第6次作业中,我本着简单而可扩展的原则,设计了两个类,Elevator.java,Requests.java。会有两个线程产生主线程和电梯线程。

 

线程安全方面,由于主线程和电梯线程都会访问Requests请求队列,我对Requests.java类中的所有读取和写入的方法都加了synchronized关键字进行保护,主线程和电梯线程共享一个Requests对象,这样在两个线程对Requests对象进行访问时就能做到同步访问。

 

捎带策略方面,我采取的是同方向捎带,即每次请求与电梯运行方向相同就捎带,直至同方向没有请求,在从请求队列里抓取第一条请求,将去这条请求fromFloor的方向作为新方向,如果请求队列为空没有请求就等待。在我的策略中不突出“主请求”的概念,而是突出方向相同的“捎带请求集”,每次有同方向的请求就加入捎带请求集,捎带请求集为空则从请求队列里抓取并重新定向,如若请求队列也为空则等待。

 

线程结束控制方面,主线程在读取结束后结束,并发信号给电梯线程,电梯线程结束的条件是在请求队列中抓取时发现请求队列为空并且主线程结束。因为请求队列中抓取时前提保证了捎带请求集为空,然后请求队列为空且主线程结束说明不会有新的请求再来了,那么电梯线程就可以结束了。当两个线程都结束,程序退出。

 

 

3.第7次作业(SS三部电梯)

在我的第7次作业中,我本着简单可扩展的原则,设计了两个类,Elevator.java,Schedule.java,会有四个线程产生主线程和三个电梯线程。

 

线程安全方面,我的设计是,主线程和三个电梯线程共享一个调度器Schedule对象,Schedule.java里面按单例模式的写法,保证永远只有一个Schedule对象被所有线程共享,而三个电梯各自待处理的请求队列也在Schedule对象的成员中,在对这些成员访问的方法都用了synchronized关键字进行保护,这样就能确保四个线程的访问不会冲突,解决线程安全问题。

 

多部电梯调度方面,我将请求分为直达和非直达,直达即fromFloor和toFloor都在电梯的覆盖范围之内。对于直达请求,在能直达的电梯中选择任务数最少的电梯进行分配。对于非直达请求,找一个能到达其fromFloor的电梯先将其运送,待其成为直达请求再在能直达的电梯中选择任务数最少的电梯进行分配。

 

线程结束控制方面,主线程在输入结束后结束,电梯线程是在三部电梯的等待队列都为空且主线程结束时退出,也就是说当一部电梯自己的队列为空且主线程退出时并不能马上退出,因为可能其他电梯会再递给他直达的请求。

 

 

二、度量分析

基于度量来分析自己的程序结构

1.第5次作业

类规模表1

度量表1

从表中可见

Elevator类有9个方法,类规模为107行,

RequestQueue类有5个方法,类规模为31行

Main类有1个方法,类规模为26行

SingleRequest类有2个方法,类规模为12行

各个方法的时间复杂度都在5以下,可见方法的分工很明确,总体满足高内聚低耦合的设计需求。

类图1

从图中可见RequestQueue是Elevator和Main共享的,类似于生产者消费者模式中的托盘。

时序图1

从时序图中可以看到主线程创建电梯线程Elevator和RequestQueue类,主线程读取输入放入RequestQueue中,唤醒电梯来取,当主线程结束后给电梯线程发信号,电梯线程接受主线程结束的信号并且无法再从RequestQueue中取到请求整个程序就可以退出了。

 

按照 SOLID 原则检查自己的程序,列出问题:

  • SRP单一责任原则:每个类都只有明确的职责
  • OCP开放封闭原则:需求改动,不要对类进行任何修改,不存在问题
  • LSP里氏替换原则:没有子类,不存在问题
  • DIP依赖倒置原则:高层次模块依赖于低层次模块的抽象,不存在问题
  • ISP接口分离原则:实现了所有接口,不存在问题

 

2.第6次作业

类规模表2

度量表2

从表中可见

Elevator类有15个方法,类规模为223行,

RequestQueue类有6个方法,类规模为74行

各个方法的时间复杂度都在5以下,Elevator类中的判断是否有乘客进入的方法时间复杂度较高,可考虑将Elevator类分解,总体满足高内聚低耦合的设计需求。

类图2

时序图2

从时序图中可以看到主线程创建电梯线程Elevator和RequestQueue类,主线程读取输入放入RequestQueue中,唤醒电梯来取,电梯取了之后根据取到的这条请求确定方向,再在RequestQueue中获取同方向能捎带的请求,当主线程结束后给电梯线程发信号,电梯线程接受主线程结束的信号并且无法再从RequestQueue中取到请求整个程序就可以退出了。

按照 SOLID 原则检查自己的程序,列出问题:

  • SRP单一责任原则:每个类都只有明确的职责
  • OCP开放封闭原则:需求改动,不要对类进行任何修改,不存在问题
  • LSP里氏替换原则:没有子类,不存在问题
  • DIP依赖倒置原则:高层次模块依赖于低层次模块的抽象,不存在问题
  • ISP接口分离原则:实现了所有接口,不存在问题

 

3.第7次作业

类规模表3

度量表3

 

 

 

从表中可见

Elevator类有28个方法,类规模为289行,

Schedule类有20个方法,类规模为217行

各个方法的时间复杂度都在5以下,但类的方法数目过多,可考虑将Schedule类拆分,总体满足高内聚低耦合的设计需求。

类图3

时序图3

从时序图中可以看到主线程创建电梯线程Elevator和调度器Schedule类,主线程读取输入请求传给调度器中,调度器根据这条请求是否是直达请求派送给当前请求数最少的电梯,电梯运行过程中考虑同方向捎带,当电梯运行非直达请求使之成为直达请求时,reput弹回调度器重新分配。当主线程结束后给电梯线程发信号,当三部电梯都无法获得新请求时才退出,因为一部电梯没有新请求可能其他电梯会派给它新的直达请求,所以它得等其他所有电梯都无法获得请求才退出。正如时序图中的if all not have next,finish。程序退出。

按照 SOLID 原则检查自己的程序,列出问题:

  • SRP单一责任原则:每个类都只有明确的职责,电梯类负责电梯的运行,调度器负责分配请求
  • OCP开放封闭原则:需求改动,不要对类进行任何修改,例如容量,电梯的能到达层数,电梯的运行速度都从外部传入,没有采用硬编码,没有问题
  • LSP里氏替换原则:没有子类,不存在问题
  • DIP依赖倒置原则:电梯类和调度器类实现过于复杂,考虑是否能进行拆分
  • ISP接口分离原则:实现了所有接口,不存在问题

 

 

三、分析自己程序的bug

主要谈一些对多线程编程有参考意义的bug,不局限于提交之后de出的bug

1.第5次作业

暴力轮询bug:

 刚开始接触多线程,在电梯等待请求来的时候,我让电梯线程一直在while循环中,处于运行状态,这样就造成了cpu资源的浪费,正确的做法是将电梯线程wait()。虽然暴力轮训在本地执行时看不出bug但是实际给cpu带来的负担是很重的,属于很严重的bug,警醒初学者不要再犯。要善用wait(),notify()来解决问题。

调度器不需要单独开线程:

刚开始写的时候把调度器也作为了一个线程,这样会让程序运行有很多冗余的操作,进程同步也会带来很多时间的浪费,后来发现调度器的工作可以用一个单例模式共享一个线程安全的类来实现,工作大大简化,而且越简单实用的架构越不容易出bug。

 

2.第6次作业

第6次作业和第5次作业架构变化不大,加入了捎带策略,没有出现较明显bug。

 

3.第7次作业

顺序问题:

有个bug导致我强测错了一个点,我的优化付之东流555。说到底也是自己粗心了,就是在电梯之间交换请求的时候,第一个电梯我写成了先把请求放回给调度器,然后打印"out"。这样做的后果是恰好有一个点,放回给调度器,然后恰好第二个电梯就在这层恰好其获得请求打印"In"比第一个电梯线程打印"out"要快,导致出现了一个先"In"再"Out"的情况,就是第二部电梯先把人接进去,第一部电梯才把人放出来的滑稽情况。解决方法是第一部电梯先打印"out"再将请求给调度器处理。

这个bug虽小,但是真切地告诉我们多线程的bug很可能就在某个特殊的时机出现,所以我们在设计和编程时要小心,多考虑,偶尔运行时正确并不代表程序没有bug了。

 

 

四、分析发现别人程序Bug所采用的策略

高工大三学生,没有互测环节,跳过...

 

 

五、心得体会

经过4周对多线程编程的磨炼我有以下几点心得体会。

1.调试方面,多线程不方便利用编译器自带的调试工具,更多的是靠分析能力分析结果,将中间过程print与自己所想作比较缩小bug发生的范围,然后多与同学交流自己的架构以及这种架构模式下容易出现的问题,就像吴际老师说的,多线程的bug不是程序员在代码上找找出来解决的,而是架构师们拿着粉笔在黑板上推敲找出来解决的,这就说明多线程调试分析能力的重要性。

2.线程安全方面,善用synchronized关键字,善用notify(),wait(),善于借鉴已有的多线程编程模式,都是保证线程安全的利器。

3.架构要简单而实用,在保证扩展性的前提上。越简单的架构越不容易出现问题,简单实用的架构更需要思维很分析,一定要对自己的架构作简化,不能仅仅满足于运行正确。如指导书中所言,真正靠谱的架构,一定是可以做到兼顾正确性和性能优化的。最后的优化分数也是,往往我和同学们的架构越简单清晰,性能分也会越高。

4.经过这4周的多线程编程和分析,我感觉收获颇丰,谢谢助教和老师们的教导。

posted @ 2019-04-24 16:43  杜一阳  阅读(160)  评论(0)    收藏  举报