OO Unit2 总结

周佳琪-22373171 学生 2024-04-18 17:07:56

第一单元总结

前言

在第二单元的学习和实践中,我并不顺利,我在每一次作业都有bug,每次作业都需要花很长的时间写,我在写程序的时候会花很长时间debug,我在强测之后也会花很长时间debug,这令我十分沮丧。究其原因,是我在一开始就没有学明白多线程,我在一开始也没有想清楚电梯的架构。除此之外,第二单元的电梯作业本身的特点也是让我痛苦的原因。因为电梯调度的算法是一个权衡的问题,在某一方面的优化可能会导致另一方面的劣化,很有可能在花费了大量时间实现的算法效果还不如最简单的随机分配。我在写程序的时候就花费了大量时间研究分配的方法,在这个过程中产生了不少bug,我又进行了不少debug的工作,这实在是一个熬人的工作。但是,第二单元总算是结束了,不管怎样,向前看吧。

作业分析

我在三次作业中并没有进行过重构,我主要花费的时间是在构建新增的具体要求和debug,所以在这里我只展示第三次作业的uml类图和时序图。

架构和时序图

架构设计

img

时序图

img

架构分析与迭代设计

我采用的是三类线程的架构设计,即输入线程,调度线程和电梯线程。在整体的设计模式中,我使用的是生产者-消费者模式。我们可以认为输入线程是生产者,电梯线程是消费者,中间的调度线程是那个“托盘”或者“桌子”。在第二次和第三次作业中,电梯线程就不单纯是生产者了,由于电梯线程也会产生请求,所以电梯线程也成了消费者。在这次多线程作业中,请求队列是线程之间的共享对象,所以请求类中的方法需要加锁,在其他涉及到遍历或者修改请求队列的方法中也需要单独为这个请求队列加锁。

在第一次作业中,输入已经为乘客分配好了电梯,我们不需要管电梯的分配,只需要负责电梯的运行就好了。我采用的是大部分人都使用的LOOK策略,为了策略与运行相分离,我单独创建了一个策略类,事实证明这是一个很好的做法。在之后的迭代过程中,电梯类已经接近500行,试想如果我们把策略类的方法也放进电梯类里,那么电梯类就会变得及其臃肿,而且我们将运行与策略分离,能给debug带来方便,使得职责更加分离,有利于后续的迭代开发。

在第二次作业,不再为电梯指定电梯,新增了reset请求。对于第一点,我采用打分的方法来选定一个最好的电梯,对于第二点,我在电梯类中新增方法用于抛出新的乘客请求,而调度器将这个新增的乘客请求视为一个普通的请求。这样就能完成重置请求。

在第三次作业中,新增了重置双轿厢电梯请求。重置双轿厢和重置普通电梯的过程是一样的(放乘客下电梯,抛出请求到主请求),只有结果不一样(双轿厢电梯的结果是产生了两部在同一个井道的电梯,普通重置电梯的结果是原来电梯的参数发生改变)。那么关键是如何管理双轿厢电梯。我采用的方法是在原来的管理电梯ArrayList新增12部双轿厢电梯,为每一部电梯设置一个是否弃用的标识,在得到双轿厢电梯的请求之后,我们再弃用原来的电梯,启动新增的电梯。(避免相撞的方法在后面)

同步块的设置和锁的选择

在多线程编程的过程中,最重要的一步就是为共享对象设置锁。如果设置锁错误,要么会运行结果与预期不符,要么运行时出现错误。在三次迭代的过程中,锁的设置也是相当于迭代设计,也就是需要上锁的地方越来越多了。但是如何上锁也是一门学问,我比较幸运,在这几次作业中我都没有遇到过死锁问题。据讨论区和微信群大伙的反馈,似乎死锁问题十分难搞。

第一次作业中我们需要上锁的就是请求队列类的所有方法和电梯类内部的请求队列类。我并没有自己设置锁,也没有设置读写锁,或许如果自己造锁的话,程序的性能会更高一点,但是那样的话又比较麻烦,难以找到bug。

在第一次作业的基础上,第二次作业需要上锁的内容是调度类的reset方法。因为在第二次作业中,调度线程是输入线程的一个属性,当输入线程得到reset请求之后,会调用调度线程的reset方法来使得电梯重置,在这个方法中,主请求会接受来自电梯得到的请求,为了保证线程安全,选择在这里上锁。

在第二次作业的基础上,第三次作业需要上锁的内容是调度类的resetDouble方法。理由同上,也是为了主请求的线程安全。第三次作业还增加了双轿厢电梯,我为了避免在遍历电梯时出错,选择在一开始就初始化了所有的电梯,在电梯需要重置为双轿厢电梯时再进行替换,这样再遍历的时候保证电梯的ArrayList不会被修改,而在替换电梯的时候为替换电梯的方法加锁。

调度器设计

我的调度器设置为Schedule类,在生产者-消费者模式中充当托盘的角色。调度器线程的主要作用是获取输入线程的请求并且响应请求。如果是普通的乘客上电梯请求,需要为乘客分配电梯。如果是重置电梯类请求,需要响应这个重置请求。

在这里我想多提一嘴,那就是职责一定要分清,也就是单一职责原则。在这里,我们考虑调度器的职责,它的职责很明确,就是调度——为乘客分配电梯,响应重置请求。那么响应重置请求是什么?是不是以为着调度器也要参与重置的过程?不是这样的!调度器只是把重置请求的信号(还有需要改变的电梯参数)发给电梯而已,至于电梯怎么重置,那和调度器没有关系。在这次作业中也就意味着,如果是重置普通的电梯,那应该是电梯负责把乘客放走,再等1.2s之后告诉自己:你已经重置完成了;如果是双轿厢电梯的重置,那就是电梯负责把电梯里的乘客放走,再等1.2s之后告诉自己:你已经重置完成了,开始被弃用了,然后把新的两部电梯开启。我在dubug的时候发现我在重置双轿厢电梯的那1.2s里,调度器不工作了!因为在我原来的程序架构中,是调度器负责开启两部新电梯的,这样就会导致本来应该是电梯类该做的事情被调度器做了,调度器要因为电梯的重置空等1.2s,这大大降低了效率,因为在这段时间里调度器本来是能够响应其他乘客的请求的。

调度策略

我的调度策略是打分制。即综合考虑各项参数,为每一个参数加权,得到总分,为乘客分配分数最高的电梯。我考虑得比较简单,考虑到电梯到乘客的距离,电梯当前的请求数,电梯的运行速度和电梯的人数这几个因素,我们很容易知道,电梯到乘客的距离越短越好,电梯当前的请求数越少越好,电梯速度越快越好,我们把这几个因素综合考虑,调一下加权即可。
我在互测中见到有人平均分配,实际上如果不同电梯的参数都相同,采用平均分配或者随机分配策略可能会好一点,但是在重置电梯之后,电梯其实就已经有了优劣之分,在这种情况下,采用这种策略的效果可能就不会很好了。但是这两种方法简单是真的,不容易出bug也是真的。而如果采用打分或者影子电梯,都有可能出现把多个人分配给一个人的情况(这个在之后debug那部分细说)

双轿厢避免相撞

我采用的讨论区的方法来避免相撞,即为双轿厢电梯设置flag指示是否处于换乘层。

public class Flag {
    enum State { OCCUPIED, UNOCCUPIED }

    private State state;

    public Flag() {
        this.state = State.UNOCCUPIED;
    }

    public synchronized void setOccupied() {
        waitRelease();
        state = State.OCCUPIED;
        notifyAll();
    }

    public synchronized void setRelease() {
        this.state = State.UNOCCUPIED;
        notifyAll();
    }

    private synchronized void waitRelease() {
        notifyAll();
        while (state == State.OCCUPIED) {
            try {
                wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

在电梯类中,如果电梯此时正处于换乘层且不在接人,需要移动一层。这里需要注意,即使电梯已经获取了结束请求,也要判断是否在换乘层,如果电梯在换乘层,也要移动一层。

我的策略是尽量避免双轿厢电梯在换乘层待着,也有人是这样做的:双轿厢电梯停在换乘层的话就先停在那里,直到有电梯即将占用换乘层,才把原来停在换乘层的电梯移走。这种方法相比我的方法移动次数更少,在耗电量上有优势。

bug分析

第一次作业

  • 在写程序的过程中,没有充分理解多线程编程,有的地方没有添加notifyAll(),程序没有能够正常结束
  • 互测过程中,电梯类中的请求队列在遍历时没有synchronized,当调度器改变电梯内的请求队列时,如果没有加synchronized,遍历请求队列会发生运行时错误。

第二次作业

互测过程中,调度器的问题。

这个问题许多使用打分和影子电梯的人都遇到了。也就是先reset五部电梯,再在这五个电梯reset的时间内投喂大量请求,这时所有的请求全被分配给一部电梯,导致超时问题。

对于我来说,我设置了一个分数的下限,当分数低于这个下限时,调度器就等一会儿再分配(如果给一部电梯持续分配人的话,那么这部电梯的分数一定是会不断降低的)

第三次作业

  • ArrayListadd()方法和set()方法的使用
  • 结束条件的判断:每个电梯的人数是0和请求人数是0
  • 换乘层的处理:当双轿厢电梯到达换乘层时要开门放人和接人,我在之前是直接在换乘层开门,这样会导致在换乘层原本应该上楼的人可能会进入下半部分的电梯,之后在运行过程中把这个人放回换乘层,然后再接上这个人,这样会反复一直接这个人,导致超时
  • 调度类的处理:只负责将reset信号传给电梯和接受电梯抛出的请求,电梯的弃用和启用由电梯类处理,之前双轿厢电梯的开启是由调度器管理的,在reset双轿厢电梯时,调度器会一直等reset完成,开启双轿厢电梯之后才会处理新的请求,这样的话,如果对一个电梯进行多次普通的reset,且中间隔的时间比较短,那么第一个reset就会处理得很晚,第一个reset还没有处理完,第二个reset请求就会出现

debug方法

在多线程编程的过程中,断点调试几乎是不可用的,我采用的是打印调试,为每一部电梯输出电梯的状态,通过这种方式我解决了不少bug,在分享课中,也有人提出在synchronized前后分别加输出语句,从而判断是否出现了死锁,只不过我自己一直没有出现死锁问题。在debug的过程中,还会出现一种情况,那就是在添加了打印语句之后bug无法复现,这是线程安全问题,往往加一个sleep语句就能规避。

我在讨论区看到了有人专门做了一个debug类来输出信息,老师在课上也提到了用idea的log来调试,我自己都试了试,感觉效果也可以

心得体会

线程安全

我最大的心得体会就是一定要学好理论知识,我的每一次作业都是因为没有理解多线程导致的各种各样bug,我花了很长时间debug,也见到了不少十分神奇的事情:bug无法复现、重复执行结果不同……后来在debug的过程中我才逐渐理解多线程的基本概念。当然这次作业结束之后我还需要继续了解多线程

层次化

这次的电梯作业层次化不如第一单元的表达式那么明显,但是也是有的。我的体会就是单一职责,这也是我上面提到的。除此之外就是“谁管谁”,输入线程为主请求添加请求,调用调度线程的重置方法,调度线程管理主请求和所有电梯,电梯负责运人和重置……最后是运行和策略分开,所以专门设置策略类,根据电梯的状态为电梯的运行提供策略。

...全文
73 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧