BUAA OO Unit2 Summary

BUAA OO Unit2 Summary

零、写在前面

https://www.cnblogs.com/GrapeLemonade/p/14702876.html

初听不知曲中意,再听已是曲中人。

一、同步块与锁

锁的选择

在本单元作业中,我全部采用了JVM层面的synchronized关键字对同步块进行加锁。虽然在后期的理论课和上机实验中也引入了lock读写锁——在资源竞争很激烈的情况下性能吊打synchronized,但是出于对正确性的稳妥考虑并没有使用。(结果最后正确性都没保证😄

同步块设置

在三次作业中,我主要采用的是类似 exp3 的设计然后基本没有大的改动:实现了一个 “线程安全类” RequestQueue作为共享对象,对其中所有方法进行加锁,保证相应的添加、删除和查找等操作具有原子性。同时,需要采用wait-notifyAll的形式编程以避免轮询。在我的理解中,当输入尚未结束但没有新请求进入、或者调度尚未结束但没有投喂给电梯的时候,电梯线程拿不到请求,就要停下来释放锁,即wait();与之对应,当我们输入结束或添加请求时,就要使用notifyAll()来唤醒等待池中的线程进入RUNNABLE状态。这里需要注意,当我们访问RequestQueue中的信息(也就是读操作)的时候,是不需要notifyAll()的。否则,当一部电梯在查询RequestQueue结束之后,就会唤醒同一栋\同一层所有在等待的电梯,然后拿到锁的那部电梯大概率是没事干的,发现没事可做之后又进入WAITING状态。这样频繁无意义的线程唤醒会占用大量CPU资源,甚至会阻塞其他线程(比如输入线程),导致程序都无法运行。

此外,在第三次作业中,为了结束线程,参考 exp4-2 增加了一个RequestCounter类,其单例由输入及电梯线程共享,所以也需要进行同步控制。最后一个不太明显的同步块是针对TimableOutput设计的:为了保证输出的安全性(也就是输出的时间戳必须非递减)需要对其加锁,具体学习了往届学长的博客,不多赘述。

二、调度器设计

在前两次作业中,我并没有设置调度器,一方面是出于设计的简洁另一方面也感觉也没有必要。由于采用了自由竞争的策略,遇到新请求就直接扔到对应building/floor的RequestQueue中,让挂在请求队列上的电梯自己抢。仔细一想,这样的设计有何 “调度” 可言?遂放弃。

然后到了第三次作业,需要处理换乘的问题,一开始还是懒得加调度器,而是考虑赋每栋的电梯访问所有floor队列的权限、每层的电梯访问所有building队列的权限,来实现请求的重新加入。这么一想,电梯似乎不再那么纯了,且 ”调度“ 的意味貌似就在其中了。最后增设了一个总的等待队列以及调度器线程:新来的和重入的请求都放入等待队列,通过调度器分派到应该去的楼层or楼座中。

三、线程协同的架构设计

在本单元的设计中,我主要采用了生产者—消费者模型。到了第三次作业,我参考了上机实验,借鉴了一点流水线模型的思想,新增加了RequestCounter类用于结束线程、 Scheduler类用于实现新请求及换乘的调度,同时形成了:

InputHandler - <waitQueue> -Scheduler - <buildingQueues/floorQueues> - Elevators两级生产者—消费者模型

以下是我第三次作业的时序图 (sequence diagram)。

unit2_SequenceDiagram

电梯策略

电梯的运行逻辑如下:

public void run() {
    while (true) {
        if (Queue.isEnd() && Queue.isEmpty() && noPassengersInside) {
            return;
        }  
        putOffPassengers();
      	PickupList = look();
        getOnPassengers();
      	if (doorOpen) {
      	    close();
        }
        move();
    }
}

如图,对于纵向电梯我采用的是look策略,关于算法本身就不多说了,简单来说就是”捎带同向,同向无请求则反向“,没请求就停着空等。而横向电梯类似,魔改了一个look,要注意转向的逻辑需要修改,防止死循环一直绕圈的现象;也可以采取无条件捎带、或者一直顺/拟时针,在这种最远距离不过3栋的情况下性能应该差别不大。

关于电梯的运行,虽然参看了学长的博客,但我并没有实现所谓的”量子电梯“优化,私以为只是一种“合理利用规则”的行为,但在现实中毫无意义。

分配策略

如上所述,采用的是自由竞争的策略,毕竟没有全局最优,哪为什么不简单点呢(

在我的设计中,每个电梯在每层都会根据策略生成一个捎带列表,然后根据这个列表去接客,只有当真正接客的时候电梯才会从请求队列中取走请求,这样就保证不会出现一个乘客上多部电梯的情况。这种“建议式”的设计自然地就适应了自由竞争的策略。

换乘策略

仅有一瞬间想过最短路,但感觉考虑的因素很多而以我的编程能力估计很难实现,遂采用基准思路。(然而甚至连基准思路都实现不好

  • 根据请求特点,静态拆分为1~3个阶段,一般情况为纵向—横向—纵向

  • 从原来继承了一个MyPersonRequest,增加了中转层midFloor以及当前阶段stage标记,重写了四个getxxx()方法。目的是最大程度保证Elevator类的复用,仅需要小修一下putOffPassengers()

    • 是最后阶段:正常下课,标记完成了一个请求
    • 不是最后阶段:进入下一阶段,扔回waitQueue即可

以下是三次作业的UML类图。

HW5

hw5_ClassDiagram

HW6

hw6_ClassDiagram

HW7

hw7_ClassDiagram

综合看三次作业,第六次增加了横向电梯,第七次增加了计数器、调度器以及修改的请求类,整体上看是迭代的增量开发,基本符合开闭原则。

而对于可扩展性,在我看来,目前这个版本而言是很差的,主要是因为我比较懒()。如果实现以下修改可以提高可扩展性:

  • 增加Elevator类,两种电梯或者新品种的电梯都可继承自父类,采用工厂模式产生新电梯
  • 增加Strategy接口,将电梯本身的运行与其指导策略解耦,然后将实现接口的特定策略作为电梯的属性,相当于电梯自身的“调度器”。

四、bug分析与hack策略

自己的bug

出现在第三次作业,都是功能性的bug……

  • MyPersonRequest类重写的getxxx()方法有错误,没有设置默认的midFloor
  • 采用基准思路寻找中转层的时候没有考虑横向电梯的可达性

他人的bug

  • 第一次作业:直接无效作业(
  • 第二次作业:随机看了两个人的、感觉写得还行,然后用自己出过问题的样例交了两发,没有成功
  • 第三次作业:一早醒来就被hack了,难蚌。然后用同学的测评机跑,一下就出问题了,交上去hack了三个

个人对互测的兴趣不高,也怠于看别人的代码,所以一般是用自己错过的数据以及dalao的测评机生成随机数据来hack。

多线程debug

与第一单元相比,第二单元的debug除了要检查功能的正确性,还要关注线程之间的交互:防止出现死锁、轮询等问题。同时,由于多线程问题很难复现,所以需要一组数据跑多次来调试。总之,

跑一百次没问题,不代表没有问题

跑一次有问题,那一定有问题

对于多线程,可以更多地进行白盒测试,通过阅读代码保证逻辑上的正确性。同时,也可以利用jps、jstack、jconsole等JVM监控工具以及JProfiler(神器)辅助调试。

五、心得体会

线程安全

线程安全可以说是多线程编程最终要的问题。我选择对共享对象类集中加锁,便于管理也便于定位错误。这样一来,只要是对共享对象的处理就能保证一定是读写一致的。同时,为了更优的性能,synchronized不应该简单地作为方法的修饰词,应该把临界区设计得尽可能小;notifyAll()不能无脑加,一般情况只读共享对象时是不需要的。而这些,都要求我们对多线程理论知识有充分的了解。

层次化设计

在经历本单元作业之后,我也对层次化设计有了更深的理解。主线程,输入线程,调度器线程,电梯线程,信息(Request)在其中是逐级传递的:输入获取新请求传给调度器,调度器安排给电梯,电梯再返还给调度器……合理的划分层次应该让各级只承担一种职责,而不是像我的设计中让InputHandler大包大揽。荣文戈老师上课几次讲到了“分而治之”的思想,对于功能、对于优化都是如此:每一层干完自己的就可以扔给下一层。大道至简。

多线程对我来说,是新知识,而且难。接触新知识的过程总是漫长且痛苦的,也让我送出了第一次的无效作业;但当你初探究竟之后,似乎又信手拈来,就像第二周顺带把第一周作业做完的我当时盲目自信的样子;最后一次作业就是给我浇的冷水,让我知道多线程,路还很长。

posted @ 2022-05-04 15:51  ezmoneysniper  阅读(85)  评论(2编辑  收藏  举报