BUAA_OO_Unit2

同步块与锁

在3次作业中,我只使用了同步块(synchronized),原因是同步块就完全可以解决共享数据安全问题,并且语法简单、固定并且性能也不差。而锁(lock)虽然更加灵活,但是在作业中并不必要,而且容易出错。

在3次作业中,存在共享数据安全问题的代码主要在"生产者—消费者"模式下的 一级托盘(总队列)和二级托盘("楼座"/"楼层"),共享数据就是总队列和楼座/楼层队列。对其进行查询、增加一个元素、删除一个元素、判断是否为空/结束 都需要进行同步互斥。因此,synchronized语句仅仅出现在两个托盘类中所有的方法、电梯策略类查询托盘的方法。

调度器设计

三次作业我都使用了相同的调度器设计。调度器只有一个,而且是一个线程。本人采用两级生产者消费者模式,第一级“生产者—消费者”的生产者线程是输入线程,乘客请求输入后进入第一级托盘,被调度器(第一级消费者同时也是第二级生产者) 取出。之后调度器根据乘客的需求将其放入第二级托盘中(楼座/楼层类),由电梯线程(第二级消费者)取出。

下面解释三次作业中如何根据乘客需求将其放入指定的第二级托盘:

第一次作业,调度器仅仅根据乘客的fromBuilding将其放入对应的纵向电梯队列(楼座)。

第二次作业,调度器实际上是根据乘客是横向还是纵向移动将其放入对应的楼座/楼层队列中。

第三次作业,调度器的设计由于涉及乘客类的一些属性,请见下文。

架构设计

在本单元,3次作业都属于迭代开发。

第一次作业实现了简单的两级生产者—消费者模型,输入线程作为第一级生产者,总请求队列是第一级托盘,调度器同时是第一级消费者和第二级生产者,各个楼座类共5个队列作为第二级托盘,电梯线程是第二级消费者。

为了更好的性能,电梯运行策略选择look策略,尽可能少地改变电梯运行方向,和生活中的电梯运行策略相同。

为了更好的拓展,为每个电梯实现策略类,为电梯提供运行方向建议、本楼层接谁的建议。

第二次作业增加了10个楼层类扩展第二级托盘,同时引入了流水线架构思想,主要是为了第三次作业写的能更简单。流水线架构思想的运行逻辑如下:

为每个乘客设定6个属性:

fromFloor:乘客目前在哪个楼层,即当前楼层

toFloor:乘客目前能直接到达哪层。"直接"意思是只坐一次电梯可以到达哪层

destinationFloor:乘客终点楼层。“终点楼层”就是输入请求时的目标楼层。

fromBuilding:乘客目前在哪个楼座,即当前楼座

toBuilding:乘客能直接到达哪个楼座。

destinationBuilding:乘客终点楼座。

其中,除了destination~属性固定不变之外,from~, to~属性是动态改变的。fromFloorfromBuilding在乘客出电梯时被电梯线程修改,改为当前电梯的位置,再放入第一级托盘; toFloortoBuilding是调度器调度乘客时根据当前电梯情况,通过setPerson方法为其设置,根据fromto属性再将其放入不同的楼层/楼座队列(二级托盘)。

第三次作业由于在第二次作业已经实现了流水线架构思想,只是根据具体需求修改了调度器和电梯线程。

调度器设计:

调度器内部完善了setPerson方法,当调度器从一级托盘inputQueue获得乘客时,顺序进行如下判断:

  1. 判断乘客fromBuildingdestinationBuilding是否一致,若一致则只需要乘坐纵向电梯;否则之后需要乘坐横向电梯
  2. 寻找第几层存在电梯能满足乘客横向移动的需求,且该层位置满足与fromFloordestinationFloor作差后的绝对值之和最小。
  3. 如果横向电梯所在楼层与乘客当前楼层fromFloor相同,那么直接乘坐横向电梯即可
  4. 如果不同,需要乘坐纵向电梯到达横向电梯所在楼层。

setPerson方法:

private void setPerson(Person person) {
    //1. 判断是否只需要乘坐纵向电梯
    if (person.getFromBuilding() == person.getDestinationBuilding()) {
        //only verticalEle is OK!
        person.setToBuilding(person.getDestinationBuilding());
        person.setToFloor(person.getDestinationFloor());
        return;
    }
    //2. 寻找理想的横向电梯所在楼层
    int flagFloor = 0;
    int min = 100;//person can get in horizontalEle from which floor
    for (int i = 1; i <= 10; i++) {
        if (hrequestQueues.get(i - 1)
            .isWorking(person.getFromBuilding(), person.getDestinationBuilding())) {
            int dis = Math.abs(person.getFromFloor() - i)
                + Math.abs(person.getDestinationFloor() - i);
            if (dis < min) {
                flagFloor = i;
                min = dis;
            }
        }
    }
    //3. 若横向电梯楼层与当前乘客所在楼层相同,那么直接坐横向电梯即可
    if (flagFloor == person.getFromFloor()) {
        person.setToFloor(person.getFromFloor());
        person.setToBuilding(person.getDestinationBuilding());
        return;
    }
    //4. 需要先纵向移动
    person.setToBuilding(person.getFromBuilding());
    person.setToFloor(flagFloor);
    return;
}

run方法:

public void run(){
    ...
	Person person = inputQueue.get();
	setPerson(person);
	if (person.getFromBuilding() == person.getToBuilding()) {
    	//放入相应的楼座(纵向电梯队列)
	} else if (person.getFromFloor() == person.getToFloor()) {
    	//放入相应的楼层(横向电梯队列)
	}
    ...
}

电梯线程设计

当乘客出电梯时,判断乘客是否到达终点,若到达则RequestCounter执行release方法;否则加入到第一级托盘中。

private void getOff(List<Person> persons) {
    ...
	if (person.getToFloor() == person.getDestinationFloor()
        	&& person.getToBuilding() == person.getDestinationBuilding()) {
        //person arrived destination
        RequestCounter.getInstance().release();
    } else {
        person.setFromFloor(curFloor);
        person.setFromBuilding(buildingName);
        inputQueue.addRequest(person);
    }
    ...
}

第三次作业UML图:白色代表第一次作业实现的,灰色代表第二次作业拓展的,淡绿色代表第三次作业拓展的

第三次作业UML简约协作图如下:

其中第8个message是第二次作业实现流水线架构思想增加的。

问题分析

这三次作业很感恩,没有出现任何bug。

互测策略

自从第一单元第一次作业开始就没有hack过他人。但就自己如何找到自己程序的bug进行一些说明:

在第二次作业中,横向电梯我采用了look策略,刚开始仅仅照猫画虎修改了一下纵向电梯的look代码就搬过来了,导致这样一组数据我会出现来回震荡,电梯空跑不接人:

[0.0]ADD-floor-7-2
[1.5]1-FROM-A-2-TO-B-2
[1.5]2-FROM-B-2-TO-A-2
[1.5]3-FROM-C-2-TO-B-2
[1.5]4-FROM-D-2-TO-C-2
[1.5]5-FROM-E-2-TO-D-2

解决方法就是横向电梯内部没有人的时候,到了一个有人的楼座直接接人。

心得体会

线程安全:

在做第一次作业之前,看到往年学长们的博客就强调:写synchronized不能瞎写,要先想好哪些是多个线程之间的共享对象。同时,把线程安全相关的问题放到一个线程安全类中考虑。照着这样的思想,三次作业基本所有用到的synchronized的语句都封装在了托盘的各种方法当中。这样做的好处是其他类写起来不必关心线程安全的问题。

层次化设计:

本人在写三次作业中,还是以第一次作业就打好底子不重构的思想开始,先确定架构,生产者消费者模式,再预测将来可能作业拓展的方向,于是第一次作业就采用了两级生产者消费者的架构,以调度器线程调度乘客到不同电梯。第二次作业出现横向电梯后,就考虑到第三次作业可能会出现乘客随便乱走的情况,于是借鉴了上机的流水线架构,导致第三次作业只是修改了很少一部分就完成了。

本人最大的感受还是和第一单元一样,先想好架构和重要的细节,再动笔写。既避免了重构的风险,又能为接下来的作业做好扩展的准备。

posted @ 2022-04-29 10:57  FYQS  阅读(26)  评论(0编辑  收藏  举报