OO第二单元博客总结

22373451-郝梓翔 学生 2024-04-20 02:04:56

面向对象设计与构造第二单元博客

同步块的设置和锁的选择

  第二单元三次作业的整体架构中我采用的大体都为inputth-scheduleth-elevatorhandleth,具体架构如图

img

其中elevator是电梯实体类,handleth是操控elevator的线程类
第五次作业中我采用的是Java对象自带的隐式锁,即使用synchornized关键字获取共享对象的锁,整体架构中,共有两个共享对象,waitqueue以及handlequeue,将这两个对象中的读写方法全部用synchornized设置为了同步代码块,第五次作业由于乘客指定电梯分配并不需要调度器实现,因此是单一的生产消费关系,waitqueue的生产者是inputth线程,消费者是scheduleth线程,handlequeue的生产者是schedule线程,消费者是handleth线程,生产者-消费者模型中,生产者调用完生产方法后要使用notifyAll()唤醒消费者,消费者在托盘为空的时候需要wait()以节省CPU运行时间,防止轮询,而在托盘为空的特殊情况为生产者已经停止生产时,消费者就需要终止了,因此第五次作业对于同步块语句的处理方法一方面将共享对象的方法都加上synchornized关键字,并按照生产这个消费者模型将对应的生产方法添加notifyAll(),消费者适时阻塞,具体实现如下

synchronized (this.waitQueue) {
    while (this.waitQueue.GetSize() == 0 && this.waitQueue.GetIsEnd() == false) {
        try {
            this.waitQueue.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

需要注意的是由于判断条件有两个同步方法,并且两个同步方法需要同时满足,因此需要在同一个同步块中,否则如果线程在两个判断条件之间发生切换,容易发生线程安全问题。
  第六次作业开始需要调度器的分配,并引入reset请求,主要有两个问题,调度器在分配的时候需要读取elevator实类的信息,但是handleth线程需要改变电梯的状态,这就涉及到电梯实体类也是共享对象,那就需要加锁来保证线程安全,对于reset请求,一方面由于reset请求可以将handlequeue中的personrequest重新放置到waitqueue中,所以对于scheduleth线程的终止条件需要改变,另一方面有极端情况如果六个电梯同时reset那么就需要scheduleth线程也wait()等到某一个电梯reset结束之后重新唤醒。由于在第六次作业之前我并没有想清楚,导致bug频出,在后面bug修复阶段选择了部分重构,引入了显式锁来替代部分隐式锁,比如handlequeue中使用了可重入读锁和可重入写锁,但是读锁和写锁并不能调用condition类的await()signalAll()方法, 这部分仍然使用隐式锁的wait()notifyALl()方法,elevator对象也使用的读锁和写锁,waitqueue仍然用隐式锁实现,同时在六个handlethscheduleth中共用一个resetLock显式锁,用来处理六个电梯同时处于reset状态时让scheduleth阻塞,并在每一个reset请求处理之后都调用相应的唤醒方法。
  第七次作业在第六次作业的基础上,引入双轿厢电梯,由于处理双轿厢电梯请求之后,重新构造出来两个电梯,在原本scheduleth中我采用Arraylist来存放六个电梯,生产双轿厢后,我用新生产的两个电梯替换原来一个电梯,这要会产生线程安全问题,如果在生产的过程中,scheduleth读取电梯状态就会产生数据冒险,我的解决办法是添加一把doubleCarLock锁,保证同步块的正常执行,后续和其他同学交流,其实深克隆出两个电梯也是一种很好的解决办法,过多的锁会显得逻辑过于复杂。双轿厢的另一个问题是防止防止电梯在换乘楼层撞车,好的解决办法显然是加锁,在生产双轿厢电梯的时候,两个电梯会共有transferLock锁,只有持有这把锁才可以进入换乘楼层,否则阻塞,在离开换乘楼层的时候再释放锁。

调度器的使用和调度策略的设计

  在第六次作业和第七次作业使用了调度器,我在第六次作业使用的调度器比较简单,主要处理调度器和线程交互的问题。第六次作业重构之后,我将调度器放在了scheduleth线程中,单独设计了depatchStrategy类,具体的共享对象waitqueuehandlequeue都作为该类的属性, 用于调度分配,scheduleth中内嵌depatchStrategy对象,只需要调用相应的方法就可以实现分配。第六次作业的调度策略很简单,就是选择当前不处于reset状态的电梯中请求数量最少的电梯,在第七次作业中采用优先级高低的策略,即优先分配给双轿厢电梯中请求数量最少的电梯,否则分配给不处于reset状态的请求数量最少的普通电梯,这种分配方法表现从最后强测分数看来表现中规中矩。
  三次作业中,对于电梯的运行,基于贪心策略,采用了一种look的改进算法,对于每一层楼层,都有两个队列,一个上行队列和一个下行队列, 两个队列都是由优先队列priority实现,排序值由该请求的移动距离决定,该方法的核心思想是在电梯上行或者下行的时候,优先选取和电梯运行方向相同的请求,在运行方向相同的请求中,优先选取移动楼层短的请求进入电梯,在乘客进入电梯的过程中,根据乘客的目标楼层不断更新电梯的目标楼层,从而实现改进版look算法。

三次作业的UML类图

img

img

img

  作业三次的UML类图,可见从第六次作业到第七次作业的过渡是较为顺利的,而从第五次作业到第六次有了比较大的重构。三次作业稳定的内容就是线程类和共享对象类,而具体的具体线程之间调用共享对象之间的锁是易变的内容,调度策略的具体内容也有比较大的改变。

img

分析程序出现的bug以及解决办法

  在第二单元多线程代码的编写中,我主要出现的bug是CTLE和RTLE两种bug。首先是CTLE,这种错误就是由于有些线程并没有阻塞而持续占用CPU资源而导致的错误,在第六次作业中,我第一次代码的主要问题就是CPU轮询问题,解决办法是后面进行了锁的重构。另一种RTLE,主要有两个问题,一个是一个进程得到阻塞后,并没有其他线程能够及时唤醒,另一个问题是当其它五个电梯都处于Reset状态时,这时候来了大量的请求会导致过多的请求来到一个电梯里面,导致运行速度过慢造成RTLE,前一种问题的解决方案就是检查代码的逻辑是否正确,第二种问题就是在互测中出现的问题,解决办法是设置电梯的最大请求数量,从而强制让调度线程等待电梯执行线程解除Reset状态。在具体的debug过程中,我常常采用的就是print出中间量,一步步排查程序运行和自己预期的差异,如果需要debug的数据量比较大,那我会不断尝试缩短数据,找到让程序出现问题的典型数据。如果是线程安全问题,在本地很难复现的问题,我并没有找到很好的办法,只能充分的白盒测试(阅读代码逻辑),确保自己的逻辑是否正确。

心得体会

通过第二单元多线程程序的学习,我对于多线程有了更深的理解。在线程安全上面,主要使用锁来实现,对于共享对象来说,如果用隐式锁,则将共享对象的方法要用synchornized关键字修饰,特别注意在if或者while判断需要用到共享对象的多个同步方法时,需要用整个synchornized同步代码块修饰保证在条件判断时候的线程安全;对于多线程的层次化设计,我认为需要深刻理解生产者-消费者模型,将实际问题抽象为生产者线程类,消费者线程类,以及托盘共享对象实体类,从而就可以合理去设计自己的多线程程序架构。

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

301

社区成员

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

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