BUAA OO 第二单元总结与反思

BUAA OO 第二单元总结与反思

写在前面

本单元的主要内容是了解并学习Java的多线程的相关知识,并且运用所学知识完成三次电梯作业的迭代。需要主要到的是,我们本单元作业真正需要用到的多线程知识是有限的,还有诸如线程池、各类锁、各种封装线程安全的数据结构等等内容需要我们自己去探索和了解。

整体架构模式

在hw5, hw6, hw7三次作业中架构基本没有巨大变化,横向电梯与纵向电梯均采用look策略,同时多电梯直接进行自由竞争,未设计复杂的调度算法。

第五次作业

UML类图

架构分析

建立Person类,每实例化一个person对象贮存一个请求的信息。

等待队列的数据结构选取LinkedBlockingQueue——一个由Java封装好的线程安全的队列结构,队列中的元素为person。

每栋楼的每一层都拥有一个等待队列,存贮从该楼座该楼层出发的请求。

由主类创建并运行一个读入线程RequestDo和五个电梯线程Elevator,读入线程结束前将所有队列setEnd,电梯线程通过这个信号来获取线程结束信号。

具体单电梯设计采用了look算法,思路如下

  • 电梯在上下来回扫描,期间能捎带就捎带。
  • 在同方向上没有请求之后电梯转向。

电梯内部采用了计组中的有限状态机的写法,通过改变state的值结合while循环在case语句中不断跳转模拟电梯运行。

第六次作业

UML类图

架构分析

与第五次作业相比,做了如下调整:

增加了Elevatorline横向电梯类,运行策略为模仿look的横向look策略。

读入线程RequestDo中也可以创造电梯线程——当读入增加电梯请求时。

多部电梯时采用自由竞争策略。

由于同一楼座/同一楼层可能会有多部电梯,因此需要给共享数据等待队列加锁,使得不同线程对同一等待队列的操作具有互斥性。

第七次作业

UML类图

架构分析

与第六次作业相比,做了如下调整:

新增了Cut类,用于对需要换乘的请求进行切分,切分换乘策略采用了指导书的标准策略。

两种电梯的设计更加参数化,满足定制的需求——不同运行时间、特定到达楼座等等。

对准入判断条件进行修改,考虑不可达楼层,以符合题目要求。

增加共享变量summer,用于记录剩余未完全完成的请求数目,并将其纳入电梯线程结束的判断条件。

UML协作图

以第七次作业为例:

该图描述了 线程创造--->一个纵向请求被完成--->程线程结束 的一个完整过程。

同步块的设置与锁的选择

由于我选用的存贮请求的数据结构是LinkedBlockingQueue,一个由Java封装好的线程安全的队列结构。因此一些操作不必再考虑线程安全问题。以最后一次作业的纵向电梯为例,加锁主要体现在一下两个方面:

一是在某楼层判断是否有人有资格被捎带,以及从等待队列取出放入电梯的过程,由于这两个过程均会对该楼层的等待队列进行便利,也即意味着频繁的存取元素,因此锁住该层的等待队列,使不同线程间的操作互斥。代码示例如下下:

    private boolean getpeek() throws InterruptedException {
        synchronized (waitQueues.get(this.floor)) {
            int len = waitQueues.get(this.floor).size();
            int len1 = 0;
            int flag = 0;
            while (len1 < len && !waitQueues.get(this.floor).isEmpty() &&
                    passengers.size() < this.maxman) {
                Person temp = null;
                temp = waitQueues.get(this.floor).getOneRequest();
                if ((temp.getTo() - temp.getFrom() > 0 && upanddown == 1) ||
                        (temp.getTo() - temp.getFrom() < 0 && upanddown == 2) ||
                        upanddown == 0 || (passengers.isEmpty() && tofloor == floor)) {
                    flag = 1;
                    waitQueues.get(this.floor).put(temp);
                    break;
                }
                waitQueues.get(this.floor).put(temp);
                len1++;
            }
            return flag == 1;
        }
    }    

	private void getIn() {
        synchronized (waitQueues.get(this.floor)) {
            int len = waitQueues.get(this.floor).size();
            int len1 = 0;
            while (len1 < len && !waitQueues.get(this.floor).isEmpty() &&
                    passengers.size() < this.maxman) {
                Person temp = null;
                try {
                    temp = waitQueues.get(this.floor).getOneRequest();
                    if ((temp.getTo() - temp.getFrom() > 0 && upanddown == 1) ||
                            (temp.getTo() - temp.getFrom() < 0 && upanddown == 2) ||
                            upanddown == 0 || passengers.isEmpty()) {
                        this.passengers.add(temp);
                        this.upanddown = (temp.getTo() - temp.getFrom() > 0) ? 1 : 2;
                        printman(0, temp.getId());
                    } else {
                        waitQueues.get(this.floor).put(temp);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                len1++;
            }
        }
    }

二是对于共享变量summer(用于记录剩余未完全完成的请求数目)的加减操作,由于自加(减)并不具有原子性,因此需要直接对该方法加锁,保证任意时刻最多有一个线程在调用改方法。代码示例如下下:

	private static int summer = 0;

	public static synchronized void adorsub(boolean a) {
        if (a) { summer++; }
        else { summer--; }
    }

调度器设计

在hw5、hw6(自由竞争)中,我没有设置专门的调度器。

对于纵向请求,我给每个楼座的每层楼都设置了一个等待队列;对于横向请求,我给每一个楼层设置了一个等待队列。请求的分配工作在读入线程中可根据读入信息轻松完成分配。剩下的工作则完全交给电梯完成——在到达一个楼层后访问对应的等待队列,根据设计的策略判断一个请求是否应当被拉进电梯。

我认为对于前两次作业比较简单的请求情况而言,调度器显得有些多余;甚至从性能上讲,砍掉调度器能够一定程度上提升性能,同时也降低了代码维护难度。

借用研讨课分享同学ppt的一张图片:

在hw7中,增加了换乘需求,我采用了基准指导书提供的基准换乘思路。于是很自然地设计了针对请求(人)的调度器,完成请求的静态拆分拆分。

bug分析与测试

本地自测

hw5中由于忘记封装输出为线程安全类,在互测中惨遭hack。

hw7中有一个强测点出现了rtle的情况。

经过反复测试,修改和查找,终于发现了问题所在——在电梯空时前往接乘请求时的捎带存在一出不合理的设计,更改后总运行时间立刻提升了十几秒,顿时我才醒悟过来,为何我精心优化的look算法在第一次和第二次作业中只取得了96.2439分和92.1299分,低于采用相同策略的其他同学。

本地测试中我认为最关键的还是对模块进行针对性测试,我习惯于在完成一部分完整功能后就编写针对性的样例进行测试,尽管这样可能会减缓进度,但是能最大程度的减少bug出现,体验写完后快速通过中测的快乐(bushi),剩下的关注点则应当集中在线程安全上。

互测策略

本单元作业我并未花费过多时间纠缠于互测当中,大数据轰炸也是最主要的互测方式,能够将可能存在的线程安全问题更大概率的复现出来。

我更多的时间则是花在了阅读代码,学习别人的架构上,并于自己的架构进行优劣比较,这也在某种程度上给我后续作业的迭代方向引发了一些思考。

心得体会

线程安全

线程安全关键需要关注的有两点,一在于共享数据,哪些数据是共享的,可能被多个线程同时操作,那么在出现这些共享数据的代码段中就需要着重考虑是否有线程安全问题;二在于设计线程安全的类, 通过线程安全类来保证各个线程的行为是安全的。

同时考虑性能问题,则关键在于选取合适的临界区,在保证线程安全的情况下使得临界区尽量小。

层次化设计

三次作业基本是迭代的, 没有重构。每次作业都在上次的作业上新增了一些功能, 较好的实现了代码复用。

所谓优秀的层次化设计,我认为也分为两个方面:

一是体现在每个类只负责自己的行为, 比如:

​ RequestDo为读入类,负责拿到请求、处理请求,并将其放入合适的队列。

​ Cut为切分类,对RequestDo中判断并传入的请求按照一定策略进行切分。

​ Person为请求类,负责每一个person对象存贮了一个完整的请求信息。

二是体现在各类之间的关系明确且尽可能的简单,能更好的协同完成工作。

真实感悟

一开始尽管依然比较茫然,但我已经不再会出现第一单元开始时茫然无措、不知如何下手的情况了。从能写的部分开始写,不太理解的部分(如线程安全)先搁置,在确保已完成部分功能的正确性的基础上=逐渐补全之前搁置的一些部分,慢慢就能搭出一个相对完整的框架了。

后两次的作业自己完成的其实比较规范,都是在构思好大部分的设计之后再开始写代码。因此即使从周五才开始写,也没有感觉到那么慌张。

还有就是不理解、不清晰的地方一定要及时请教助教,即便已经通过了作业,即便可以用一写技巧绕过这些地方。

最后希望自己能更细致耐心的完成后面两个单元的作业。

posted @ 2022-04-30 01:22  Arosy_24  阅读(38)  评论(1编辑  收藏  举报