OO2022第二单元个人总结

OO2022第二单元个人总结

  本单元主要内容为设计多线程电梯运行程序,在架构方面我主要采用了生产者-消费者模型,三次作业的主要结构都为input类,elevator类,waitQueue类,运行逻辑上可以概括为由elevator向waitQueue发起查询后再获取目标容器,其中可能产生诸如读了脏数据等多线程交互问题,我将在后文说明自己是如何处理并解决这些问题的,对于加锁问题本单元作业只用了synchronized进行加锁。

1 第一次作业

  第一周的作业内容较为简单,只会出现纵向请求,故在设计方面采用了实验代码中的input-schedule-waitQueue-elevator的架构,输入线程将请求陆续放在一个大盘子内,由分配器将这些请求选择分配给各个电梯去执行。

  架构方面大体上还是采用了生产者消费者模式,只是第一周作业多加了一个分配用的schedule,对于托盘方法而言,无论personRequest的结构发生怎样的变化,都可以用query-get-remove三个过程解决,而且通过一些特殊处理后是可以忽略读相关的异常的(见2.2),故在添加读方法的时候也不用担心方法数量过多大幅降低并行效率的问题,故托盘类从设计角度讲拓展性还是很好的。
  同时在电梯属性的设计上为了方便处理后期可能出现的个性化电梯,这里尽可能将所有能想到的属性都进行了私有化处理,且根据<<clean code>>对电梯边界也设置了final static的相关属性。

image

1.1 同步块设置及锁的选择

  由于各个电梯只对自己的小托盘进行访问,没有严格的临界区,所以只需要考虑在大托盘里面加锁,小托盘和电梯内部的所有方法操作都不会影响正确性。
  本次作业的设置方式各个电梯的不存在共享数据的问题,自然不用考虑各种电梯相关的读写异常,只有在input类和schedule类同时访问的方法里需要注意进行加锁,所以这里将所有input类和schedule类在waitQueue类中访问到的方法设置为同步方法,即访问这些方法时会将大托盘WaitQueue家锁,其样例如下。

public synchronized void addRequest(PersonRequest request) {
        requests.add(request);
        notifyAll();
    }

  为了避免读写异常,我采用的方法是在每个读方法中对容器的副本进行访问,由于本次作业没有电梯竞争,所以不用担心副本内的对象不存在的问题,这样就避免了对大量的读方法也加锁的缺点。
  电梯在小托盘没有请求且没有结束的情况下就会wait,相应的会在托盘类的addRequest方法和setDone方法中对所有线程进行唤醒操作。

public ArrayList<PersonRequest> queryByFromFloor(int level) {
        ArrayList<PersonRequest> ans = new ArrayList<>();
        ArrayList<PersonRequest> requestCopy = new ArrayList<>(requests);
        for (PersonRequest personRequest : requestCopy) {
            if (personRequest.getFromFloor() == level) {
                ans.add(personRequest);
            }
        }
        return ans;
    }

1.2 调度器设计

  第一次作业参考了实验代码的相关架构,用一个Schedule类负责从大的托盘里获得各个请求分别分配给各个电梯对应的小的托盘,调度器的属性包含大托盘和各个小托盘,由此来达到和各个线程进行交互的目的,设计上比较简单。

1.3 bug修复

  没注意到输出包不安全的问题导致被hack了,对输出方法加了个锁就解决了。

2 第二次作业

  第二次作业只是在第一次作业的基础上加了横向的电梯且没有斜向请求,所以架构上相较于第一次作业没有太大改变,但是此时电梯之间会出现竞争问题,而我只希望线程对某个请求的竞争仅有CPU调度决定,且指令最多只有70条,所以我取消了Schedule类,让所有电梯同时对大托盘进行访问,由此来实现自由竞争的目的。
  WaitQueue类里定义了一个GO_FROM_TABLE是方便横向电梯判断左边接人还是右边接人打的一个表,楼座只有5个所以就懒的用掩码折腾了,干脆打个表快速判断。

  除了取消了schedule类,架构与第一次作业没有较大变化,取消了分配者这么一个结构后,看起来更像生产者消费者模型了一些 ,也正如上文提到的,第二次作业添加了大量新的读方法,进行加锁处理后效率会变得异常底下,故托盘类在新增了横向请求之后依然能保持较好的可拓展性,考虑到第三周必出斜向请求,目前的架构应该是能完美解决的。
image

2.1 同步块设置及锁的选择

  这次电梯存在一个较大的临界区,锁的设计上会比第一次作业复杂很多,所以为了保证逻辑上的简洁性,我在架构上依然只对所有写方法进行加锁,读方法通过特殊处理来避免加锁的过程。
  在WaitQueue类(大托盘)的设计中主要要两大类方法,一类为query方法,一类为对托盘的私有属性进行写操作的方法,为了减少不必要的同步操作,我将所有的写方法设置为同步方法,对所有的读方法不进行加锁,这样可能产生读异常、读写异常等相关问题,对此我的处理方法是,除了对每个读方法手动添加一个临时的容器副本外,电梯在获得了这个读方法的查询结果准备遍历这个结果进行写操作时,会对每次遍历到的请求对象进行一次筛选,如果这个对象是null,说明已经被别的线程竞争到,此时只需要忽略它。这种方法让代码安全性逻辑上变得简单了许多,只会在WaitQueue的写方法中看到同步块,而且在后续进行拓展的时候,仍然不用考虑读问题造成的相关异常,大大减少代码逻辑上的复杂程度,缺点就是这种静态的处理方式不利于电梯性能,比如我在拷贝了一个副本之后又来了一个刚好能接到的请求,此时就只能把它放到下一轮循环了,读操作的样例代码如下。

public ArrayList<PersonRequest> queryByUpperFloor(char buildingName, int level) {
        ArrayList<PersonRequest> ans = new ArrayList<>();
        ArrayList<PersonRequest> requestCopy = new ArrayList<>(queryVerticalRequest(buildingName));
        for (PersonRequest personRequest : requestCopy) {
            if (personRequest == null) {
                continue;
            }
            if (personRequest.getFromFloor() > level) {
                ans.add(personRequest);
            }
        }
        return ans;
    }

  除此之外再判断电梯是否需要进行等待时仍然需要对大托盘进行一次加锁,否则可能会出现在判断过程中其他线程对大托盘的属性进行了修改导致的不必要的轮询和忙等,此外就没有需要加锁的代码了,其样例如下。

synchronized (waitQueue) {
        if (!waitQueue.isDone() && waitingRoom.isNoRequest()
                && readyQueue.isEmpty()) {
            //System.out.println(getName() + " waiting");
            try {
                waitQueue.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            continue;
        }
    }

2.2 调度器设计

  直接取消调度器,只有70条指令的情况下消耗其实并不大(虽然很不OOP),让电梯自己去大盘子里面查,查到了就尝试拿,没拿到说明被别人抢了就尝试拿别的,这种思路大大简化了实现电梯竞争的过程。

2.3 bug修复

  被实习压榨的死死的导致没时间去查bug,bug修复的时候发现横向电梯的运行逻辑出了问题导致电梯停了都会一直有人接不上,怒被砍6刀,最后决定不整花里胡哨的调度,横向也改成了look就全解决了...

3 第三次作业

  第三次作业中横向电梯出现了可达信息,且出现了斜向的请求,但整体上和第二次没有太大的区别,故我仍然沿用了第二次作业的设计。

  架构上比起第二周就仅仅只是多了一些读和判断转乘用的方法,没有新增过多的锁,除此之外出现了一些自己不太想看到的情况,就是我对换乘的处理是把它再丢回托盘类中一个额外的私有属性里去,这开始模糊了我的设计模式。
image

3.1 同步块的设置及锁的选择

  本单元作业对于同步块的设计考虑同第二单元作业,故不再赘述。

3.2 调度器设计

  和第二单元没有太大的区别,在托盘中新加了一个transitMap用来查询某一层的横向电梯情况,方便进行换乘,这个transitMap由input线程负责更新,key为楼层value为可达信息的列表,在纵向电梯放人的时候会查一下这个map在根据自身调度策略选择要不要在这层放人。另外还加了horizontalTransitQueue和verticalTransitQueue分别负责横向和纵向电梯接收换乘请求,每次换乘的时候会判断当前这个换乘请求是否已经结束,如果结束了就将transitQueue中对应的请求删掉(在输入线程加请求的时候会判断这个请求是否需要换乘 如果需要就把它也添加到tansitQueue中),事实上这个处理过程也可以用future方法解决,但我懒的改了...

3.3 bug修复

  特征在于被hack的点都有请求一直接不上,就去查了一下数据,好死不死,偏偏忽略了横向不可达请求的处理,中强测还刚好妹出,寄!修改方法为某个横向请求当前楼层不可达就把它丢到一楼去。

4 hack策略

  被实习填压,没时间搞互测,也不想折腾20级的同志们,故策略为不hack。

5 心得体会

  其实就层次化设计而言,个人觉得好好学学AOP设计比OOP能更好地去理解层次化的意义,但不得不说本单元作业个人的完成度其实是不层次化的,为了节省时间在设计角度我直接就采用三个类:输入-盘子-电梯 来解决所有问题,看似职责是分明的,但本身仍然不够明确,比如电梯就应该只负责进出人和移动,怎么进怎么出怎么移动是电梯这个对象不关心的,应该给它再定制一个策略类来告诉他要接什么人放什么人往哪里走,我目前这堆代码个人感觉就是个图省事的屎丘,可读性确实一般。
  线程安全这方面还是要理清楚临界区的相关问题,尤其要搞清楚访问公共资源的方法里面关心的是对象的什么要素,比如终止条件里只关心托盘结没结束还有没有请求,为了避免判断的时候被改了就得对他加锁,又或者说自己想实现共享读但是不想使用读写锁这些略复杂的处理机制,那就要思考非共享读可能导致的问题是什么,针对问题进行特判来解决。其实说到底我的评价就八个字“建议力扣六脉神剑”链接如下:
https://leetcode-cn.com/problems/print-foobar-alternately/solution/duo-xian-cheng-liu-mai-shen-jian-ni-xue-d220n/
  最后说个题外话,要警惕知识优越感。

posted @ 2022-04-26 17:10  Horatio201  阅读(70)  评论(1编辑  收藏  举报