OO第二单元总结

OO第二单元总结

作业要求:

  • (1)总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系
  • (2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
  • (3)从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性
    • 画UML类图
    • 画UML顺序图(sequence diagram)来展示线程之间的协作关系(别忘记主线程)作业
  • (4)分析自己程序的bug
    • 分析未通过的公测用例和被互测发现的bug:特征、问题所在的类和方法
    • 特别注意分析那些与线程安全相关的问题(特别要注意死锁的分析)
  • (5)分析自己发现别人程序bug所采用的策略
    • 列出自己所采取的测试策略及有效性
    • 分析自己采用了什么策略来发现线程安全相关的问题
    • 分析本单元的测试策略与第一单元测试策略的差异之处
  • (6) 心得体会
    • 从线程安全和层次化设计两个方面来梳理自己在本单元三次作业中获得的心得体会

同步块与锁

总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系

先给出三次作业大体上的架构:

输入线程将乘客放到unsignedRequestQueue里,表示得到了,但是未被“分配”的乘客;

调度器Schedule将乘客“分配”给每个电梯,具体操作是将Request放到和电梯共享的一个队列requestOutElevator里;

电梯将自己被分配到的Request接到内部,表示这些Request进入了电梯,然后到目的地送出去。

具体的架构如下:

第一次作业

第二次作业

第三次作业

同步块的设置和锁的选择

首先要确定哪些东西是要被共享访问的,分别是unsReqQue,reqOutElev,然后确定访问它们的对象要做哪些操作,如InputThreadadd方法,Schedulemove等。对于这些要访问共享对象的方法,就要给它们加一把锁。

以第一次作业的InputThread的对unsReqQue的操作为例:

synchronized (unsReqQue) {
    if (request == null) {
       unsReqQue.close();
       unsReqQue.notifyAll(); // wake up every thread
       return; // terminate the InputThread
     } else {
         if (arrivePattern.equals("Night")) {
            unsReqQue.addReq(request); // 不要提醒线程,
         } else {
             unsReqQue.addReq(request);
             unsReqQue.notifyAll();
         }
     }
}

好了,同步块的设置和锁的选择分析完了。

锁与同步块中处理语句的关系

同步块中的处理语句要对这个被上了锁的对象进行添加、删除或者读取,这就是锁和同步块最直接的关系。

调度器设计

总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互

调度器运行流程如下:

lock(unsReqQue) {
    for req in unsReqQue:
        generate a strategy
        select an reqQueOutelev by strategy
        lock(reqOutElev) {
            put req into elev.reqOutElev
        }
}

调度器设计

调度器设计的关键是调度算法的设计,即select an elevator,在我的架构中,说得通俗点就是,调度器Schedule面对一个“未分配”的人,该放到哪一部电梯里,可以使得所有电梯的运行时长最小。

第一次作业非常简单,因为只有一部电梯,所以直接放里面就好了;

第二次作业我的方法并不高明,直接根据reqOutElev.size()判断,哪个电梯外面的人少就分配给谁,优点是这样较为均衡,缺点是没法结合电梯自身运行情况、电梯内部人员的属性进行判断。

第三次作业要处理换乘,我的策略是,当乘客的路线符合一定条件,那么就会触发换乘机制。为要换乘的乘客设置toFloornextToFloor两个成员变量,对其进行修改。修改之后,再选择reqQueOutElev进行投放。选择时,策略和第二次就一样了,挑个人少的电梯进去。

和线程交互

这是个比较简单的事情。

首先呢,在创建线程的时候,就要把共享的对象传入进去,

运行的时候呢,Schedule锁住俩队列,然后一个remove一个add就完事,

InputThreadElevator在需要访问这些共享对象时上锁就行了。

而且在我的架构中,调度这一部分不用考虑死锁,因为Elevator对reqQueOutElevunsReqQue的访问是分开的,没有“锁中锁”的情况。

可扩展性分析

从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性

  • 画UML类图
  • 画UML顺序图(sequence diagram)来展示线程之间的协作关系(别忘记主线程)作业

各种图

第一次作业

类图如下:

顺序图如下:

通过各个类之间的依赖关系,可以看到,这一次作业架构清晰,简单明了。不过电梯类十分地臃肿,因为那个调度算法十分地面向过程。

第二次作业

类图如下:

顺序图如下:

本次作业封装了PersonRequest类为MyReq类,可以实现更多的其专有功能,如判断方向等,更体现面向对象的思想,但是给它的方法太多了。

除此以外,还在InputThread中添加了新建电梯的代码,这也是作业所要求的。

第三次作业

类图如下:

顺序图\(Sequence \ Diagram\))如下:

相较于上次,多了乘客换乘部分。

部分度量分析如下:

分析

让我们来看看这几个泛红的方法,它们集中在ElevatorSolveReq内部,功能是实现捎带和调度,主要问题是if-else分支太多。

我将电梯的运行分成5个部分:

  1. 调整边缘楼层方向
  2. 选择目标楼层
  3. 向目标楼层移动
  4. 放出乘客
  5. 接收乘客

最复杂的地方就是选择目标楼层,因为我自己写了一个算法来实现捎带,我需要考虑电梯内部是否为空、电梯里的人是不是往同一个方向走,电梯既能上、又能下时,怎么抉择……

为了防止考虑遗漏,所以采用了类似于遍历真值表的方式。

性能还可以,在进行了MorningNightRandom调度策略区分的情况下,强测能拿到97分左右。

对扩展性而言,可以对调度策略,捎带策略和电梯类型进行分析。

调度策略一般般吧,就区分了MorningNightRandom,以及专门弄了个SolveReq类进行调度,要换策略不是很难。

至于捎带策略,我自己在写完电梯的捎带算法以后,就几乎没动过,一来是算法性能尚可,二来是改动的话,还是会在Elevator里面进行修改,Elevator类的最终结果还是臃肿(虽然已经很臃肿了),所以扩展性不强。

如果有新的电梯类型加入,会导致我捎带策略的改变,这并不优雅。因为在我的架构中,Schedule保证Elevator添加的RequesttoFloorfromFloor在电梯的停靠楼层内。电梯不关心自己可以在哪些楼层停靠。

从现在来看,将工厂模式应用到本次作业会是一个不错的选择,同时,各个电梯本身的属性也可以专门做一个迭代类来实现,这样能避免电梯类的臃肿。

所以本次作业扩展性比较差

bug分析

分析自己程序的bug

  • 分析未通过的公测用例和被互测发现的bug:特征、问题所在的类和方法
  • 特别注意分析那些与线程安全相关的问题(特别要注意死锁的分析)

第一次作业

在公测中出现的bug:

  1. 使用迭代器访问共享资源时,没有锁住,导致一者访问,一者添加,然后报expectedModCount错误
  2. 输出close没大写
  3. 模拟电梯在楼层间运行时,sleep和输出分离了。比如跑两层,先sleep0.4s,然后连续输出arrive。正确的做法应该是sleep一层,arrive一层。
  4. 模拟电梯开关门出错,正确顺序应该是先open,再sleep;先sleep,再close。

这些bug的特征非常明显,就是线程安全、题目理解和思考能力。

我以为访问共享资源时,如果不追求精度,锁在一定的条件下可以省略,但是Java的迭代器会有一个自我检验的步骤,这也让我在后续作业中更加小心。

强测和互测中均未出现bug。

第二次作业

公测出现一个bug:

Schedule只在一个电梯的缓冲区添加了Request,却notify了所有电梯,导致有电梯被唤醒后不知所措,说明大E了。

本地测评时还发现一个奇怪的现象,就是在输入以后,有乘客没有被输出,一定要我按下Ctrl+D才能显示输出,后来发现是我在输入乘客信息时没换行,虚惊一场😓。

强测和互测均未出现bug。

第三次作业

在公测中出现了一个线程安全问题,不过不是死锁,是调度器结束条件判断顺序问题。特征就是对资源的访问和状态更新的代码顺序,位置是Elevator实现换乘的地方,特别是调度器结束的判断条件,是本次作业的难点。

在进行一番缜密的逻辑推导后,用一个非主流的操作de掉了,这个操作比较骚,就不细说了。

强测和互测均未出现bug。

发现别人的bug

分析自己发现别人程序bug所采用的策略

  • 列出自己所采取的测试策略及有效性
  • 分析自己采用了什么策略来发现线程安全相关的问题
  • 分析本单元的测试策略与第一单元测试策略的差异之处

别人都没有hack我,我为什么要去hack别人?

好吧,实在是太菜+太忙,没有精力去hack别人。

不过可以试着分析本单元的测试策略与第一单元测试策略的差异之处

第一单元测试策略比较直接,自己生成好测试数据后,交给要测试的对象,拿到相应的输出后,与现成的工具进行比对就行,这一单元,不仅要实现定时投放测试数据,还要自己写一个对输出结果进行检验的工具,比较繁琐。

我相信大部分同学是不会去阅读房间里别人的代码的,都是测评机在大开杀戒。能看一看的就是电梯的捎带策略了,如果用了什么性能较差的算法,还是可以有针对性地构造数据集去hack。

心得体会

  • 从线程安全和层次化设计两个方面来梳理自己在本单元三次作业中获得的心得体会

线程安全

无非是对线程安全有了更深入的理解云云。

每次写作业前,我都尝试把架构画出来,注明每个线程共享的对象,每使用一次wait(),我都会检查一遍所有的notify(),保证能被唤醒。当条件多变时,通过真值表的方式,防止遗漏情况。这对线程安全和思考全面以及复杂度的增加都有很大帮助。

层次化设计

废话不多说,这个单元加强了我对层次化设计的理解和运用。

感恩部分

感恩B站,带我走进了多线程的世界;

感恩上机,教会了我wait和notify的使用;

感恩刘意,他是我的多线程启蒙良师;

感恩同学,和他♂们的交流让我收益匪浅;

感恩助教,为同学们答疑解惑;

感恩老师,教授我多线程的知识;

……

感恩思密达。

posted @ 2021-04-10 09:28  ticlab  阅读(75)  评论(0编辑  收藏  举报