BUAA OO 第二单元总结

王艺杰-21371429 学生 2023-04-16 18:06:22

BUAA OO 第二单元总结

一、概述

本单元作业主要完成的是一个多线程实时电梯调度模拟系统,熟悉线程的创建、运行等基本操作,熟悉多线程程序的设计方法,掌握线程安全知识并解决线程安全问题,掌握线程之间的交互,强化线程之间的协同设计层次架构。

二、同步块的设置和锁的选择

同步块和锁主要解决的是多线程的数据安全问题,具体表现为,如果对于同一个数据在不同的线程同时需要执行读取或者写入操作,将会造成数据不统一的安全问题。所以为了让程序执行的时候数据统一,我们将每一个线程对数据的访问进行一个上锁的操作,其表现为将这个数据变为仅仅当下上锁的线程可以进行读写,这样可以确定程序执行前后数据的统一性。

读写锁的特点是:读读不互斥、读写互斥、写写互斥。优势在于:(1)多个读锁可以同时执行,相比于普通锁在任何情况下都要排队执行来说,读写锁提高了程序的执行性能;(2)读锁和写锁是互斥排队执行的,这样可以保证了读取操作不会读到写了一半的临时数据。在读多写少的使用场景下,读写锁的优势最大。

在三次作业中:

  • 第五次作业:仅使用了synchronized关键字,执行对共享资源上锁的功能。具体来说有两种使用。

    • 第1种方式是修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{ }括起来的代码,作用的对象是调用这个代码块的对象。最典型的是电梯进人方法personIn对候乘表的修饰:
      public void personIn() {
          synchronized (waitQueue) {
              ArrayList<Person> waitPersons = waitQueue.getReqQueue();
              Iterator<Person> iterator = waitPersons.iterator();
              while (iterator.hasNext() && curNum < maxCap) {
                  Person person = iterator.next();
                  if (...) {
                      ...
                      iterator.remove();
                  }
              }
          }
      }
      
    • 第2种方式是修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。典型是候乘表的等待方法:
      public synchronized void waitForAdd() {
          try {
              wait();
          } catch (InterruptedException e) {
              throw new RuntimeException(e);
          }
      }
      
  • 第六次作业:仅对必须要求获取锁的 wait / notify / notifyall 方法保留了 synchronized 修饰,其余均采用读写锁替代 synchronized 关键字,以进行更精细的控制。

    • 仍以 personIn 中提到的的 waitQueue 为例来分析,典型写锁如 personIn 中的使用:
      public void personIn() {
          ArrayList<Person> waitPersons = waitQueue.getReqQueue();
          queueLock.writeLock().lock();
          try {
              ArrayList<Person> waitPersons = waitQueue.getReqQueue();
              Iterator<Person> iterator = waitPersons.iterator();
              while (iterator.hasNext() && curNum < maxCap) {
                  Person person = iterator.next();
                  if (...) {
                      ...
                      iterator.remove();
                  }
              }
          } finally {
              queueLock.writeLock().unlock();
          }
      }
      
    • 典型读锁如对 waitQueue 是否为空的判断:
      public boolean isEmpty() {
          queueLock.readLock().lock();
          try {
              return reqQueue.isEmpty();
          } finally {
              queueLock.readLock().unlock();
          }
      }
      
  • 第七次作业:大体同第六次作业,而对新增的迭代要求——在任意时刻,任意楼层处于服务中的电梯数量小于等于4部,处于服务中的只接人电梯数量小于等于2部,则使用了信号量 semaphore 机制,只需要使用其 acquire 方法与 release 方法便可以申请“开门资源”与释放“开门资源”,并且在资源不足时自行等待。主要使用在了电梯类的开门方法中:

    public void openDoor(boolean type) throws InterruptedException {
        if (type) { acOnly.get(curFloor).acquire(); }
        serveNum.get(curFloor).acquire();
        ...
        if (!type) { personOut(); }
        personIn();
        sleepForOp(windowTime);
        personIn();
        ...
        serveNum.get(curFloor).release();
        if (type) { acOnly.get(curFloor).release(); }
    }
    

三、架构分析

由于三次作业整体架构没有发生改变,每次迭代仅仅是在某些类中增添了一些方法,故均以最终作业的类图分析三次作业架构设计的逐步变化和未来扩展能力。

1. UML类图

img

2. UML协作图

img

3. 调度器分析

  • 调度器设计
    同时设计了总队列-子队列的模式进行分配,在InputHandler中实例化一个scheduler对象,实现调度器与输入的交互,并将总队列,所有电梯,所有子队列传入了scheduler中,以此实现调度器与其它线程的交互。

  • 调度策略
    大体上采用了 LOOK ,具体实现为:

    • 移动策略:初始电梯运行方向为上,请求到来之后判断与电梯运行同方向是否有请求(例如如果电梯运行方向为上,则检查当前楼层之上是否有请求),如果有,电梯继续向相同方向运行,否则转向。
    • 捎带判断:与电梯运行方向相同即可捎带。

    总队列-子队列的分配上,设计了一个简单的权重公式:|方向 * 请求发出楼层 - 方向 * 电梯所在楼层| * 电梯速度。首先将总队列中的请求按权重从大到小排列,然后依次按容量分配给所有电梯,之后不管总队列中是否非空,调度器均 wait,电梯运行时,内部队列与等待队列总人数小于容量时则会唤醒调度器,再次进行分配。

4. 迭代分析

  • 第五次作业任务是需要模拟一个多线程实时电梯系统,系统中存在着6部电梯,可以在1-11层之间运行,主要目标是模拟电梯的上下行,开关门,以及模拟乘客的进出。

  • 第六次作业新增了具有不同参数的新电梯的加入、电梯维护两种请求;

  • 第七次作业新增了电梯可达性以及电梯调度逻辑合理性。

三次作业稳定的内容:
调度策略可以沿用。

三次作业易变的内容:
主要是电梯参数、电梯行为两方面。对我而言,Elevator 类是三次作业中主要修改的部分,包括第六次作业中针对 maintain 指令退请求的设计,第七次作业中针对电梯可达性和服务数目限制的设计。其次是 Scheduler 类,因为可达性的缘故,需要先采用 Dijkstra、Floyd 或者 BFS 等路径规划算法先进行规划,在计算权重,最后再进行分配。

四、Bug分析

  • 本人bug
    三次作业均未出现bug。
  • 他人bug
    在第六次作业中:
    • 一些同学的调度器在乘客请求均为空且输入线程结束之后便会结束,因此如果最后乘客已经调度完但还有maintain请求没有读取时便无法处理。
    • 死锁
  • 在第七次作业中:
    • 一些同学 maintain 后移动超过了两层,大概是想专门卡这个时间的缘故。可惜由于多线程的缘故,交了5次都没刀中。
    • 一些同学存在结束后乘客留在电梯里的问题,猜测和第六次作业一样,但是也没有刀中。

五、心得体会

  • 线程安全
    多线程问题的主要给我带来了两方面的挑战。一方面,开始很难构思多线程之间的协作;另一方面多线程的测试结果难以复现,debug和优化都比较困难。在保证线程安全方面,个人总结了几点:
    • 构思清楚线程之间的协作图。能意识到哪里会可能出问题,哪个方法需要加锁,哪里可能出现死锁等等。
    • 采用成熟的多线程设计模式。生产者-消费者模式作为比较简单的多线程模式,可以大大降低了本次作业的难度。
    • 多做测试。例如模拟高并发时的情景。
  • 层次化设计
    • 设计模式
      本次作业主要采用了单例模式、策略模式和工厂模式,大大降低了类与类之间的耦合,保证了程序良好的可扩展性。得益于设计模式,我的三次作业沿用了最开始的架构,没有重构,也没有新增类,仅仅是在一些类中增加或修改了一些方法和对象。
    • 优化设计
      由于性能分的评分标准比较全面,实际上全局优化是较为困难的,更多的是一次次的局部优化,本人就只是着重优化了耗电量,等待时间和总运行时间方面则是有所缺失,有失有得。
...全文
69 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

442

社区成员

发帖
与我相关
我的任务
社区描述
2023年北京航空航天大学《面向对象设计与构造》课程博客
java 高校 北京·海淀区
社区管理员
  • 被Taylor淹没的一条鱼
  • 0逝者如斯夫0
  • Mr.Lin30
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

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