OO第二单元作业总结性博客

架构与设计

在三次作业中,我主要是对现实世界的电梯情况进行了模拟,将人上电梯的过程分了三个行为主体,分别是人、电梯、和调度器。

人不关心电梯怎么调度,人只管电梯开门时进出,另外在本次作业中人需要自己决定是否转乘以及在第几层转乘。

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

电梯

电梯不关心怎样接人效率高、不关心有没有载人、不关心载的人在几层下。电梯需要知道的只是要往几层跑以及到一层时要不要开门。

调度器

调度器完成信息沟通和电梯调度工作。一是从人获得楼层信息然后综合计算后告知电梯运行方向以及停靠信息;二是从电梯获得到达信息并通知给人。

输入数据放在main中是主线程,另外每一个电梯是一个线程,以第三次作业为例,主线程不停的读入数据,根据读入的PersonRequest对象构造Person对象,添加到调度器维护的等待队列中并唤醒等待中线程,如果输入结束,将结束标志位endMark置位,唤醒等待中线程然后退出。

public static void main(String[] args) {
       TimableOutput.initStartTimestamp();
       ElevatorInput elevatorInput = new ElevatorInput();
       Scheduler.floorsInit();
       Elevator elevator1 = new Elevator('A',400,false);
       Elevator elevator2 = new Elevator('B',500,false);
       Elevator elevator3 = new Elevator('C',600,true);

       elevator1.start();
       elevator2.start();
       elevator3.start();
       while (true) {
           PersonRequest p = elevatorInput.nextPersonRequest();
           if (p == null) {
               Scheduler.setEndMark();
               synchronized (Scheduler.lock) { Scheduler.lock.notifyAll(); }
               return;
          }
           Scheduler.add(new Person(p));
           synchronized (Scheduler.lock) { Scheduler.lock.notifyAll(); }
      }
  }

每个电梯线程建立后会向调度器要任务,如果要到就开始运行,每到一层向调度器询问是否需要停,如果停靠则执行停靠动作并通知调度器到达。具体流程见下图:面向对象第二单元总结 随笔 第1张

 

面向对象第二单元总结 随笔 第2张

public class Elevator extends Thread {
   private int nowFloor;
   private int aimFloor;
   private char label;
   private int eleSpeed;
   private boolean up;

   public Elevator(char l,int s,boolean u) {
       nowFloor = 1;
       aimFloor = 1;
       label = l;
       eleSpeed = s;
       up = u;
  }

   @Override
   public void run() {
       while (true) {
           aimFloor = Scheduler.getTask(nowFloor,label,up);
           if (aimFloor == 0) {
               if (Scheduler.getEndMark()) {
                   synchronized (Scheduler.lock) {
                       Scheduler.lock.notifyAll();
                  }
                   return;
              }
               synchronized (Scheduler.lock) {
                   try {
                       Scheduler.lock.wait();
                  } catch (Exception e) {
                       e.printStackTrace();
                  }
              }
          } else {
               if (aimFloor > nowFloor) { up = true; }
               else if (aimFloor < nowFloor) { up = false; }
               move();
          }
      }
  }

   private void move() {
       if (aimFloor > nowFloor) {
           while (nowFloor < aimFloor) {
               if (Scheduler.needToPause(nowFloor,label,up)) {
                   openDoor();
              }
               try { sleep(eleSpeed); }
               catch (Exception e) { e.printStackTrace(); }
               if (nowFloor == -1) { nowFloor = 1; }
               else { nowFloor++; }
               TimableOutput.println(String.format(
                       "ARRIVE-%d-%c",nowFloor,label));
          }
      } else if (aimFloor < nowFloor) {
           while (nowFloor > aimFloor) {
               if (Scheduler.needToPause(nowFloor,label,up)) {
                   openDoor();
              }
               try { sleep(eleSpeed); }
               catch (Exception e) { e.printStackTrace(); }
               if (nowFloor == 1) { nowFloor = -1; }
               else { nowFloor--; }
               TimableOutput.println(String.format(
                       "ARRIVE-%d-%c",nowFloor,label));
          }
      } else {
           TimableOutput.println(String.format("OPEN-%d-%c",nowFloor,label));
           try { sleep(200); }
           catch (Exception e) { e.printStackTrace(); }
           Scheduler.arrive(nowFloor,label);
           try { sleep(200); }
           catch (Exception e) { e.printStackTrace(); }
           TimableOutput.println(String.format("CLOSE-%d-%c",nowFloor,label));

      }
  }

   private void openDoor() {
       TimableOutput.println(String.format("OPEN-%d-%c",nowFloor,label));
       try { sleep(200); }
       catch (Exception e) { e.printStackTrace(); }
       Scheduler.arrive(nowFloor,label,up);
       try { sleep(200); }
       catch (Exception e) { e.printStackTrace(); }
       TimableOutput.println(String.format("CLOSE-%d-%c",nowFloor,label));
  }
}

调度器相关方法均使用synchronized同步

基于度量的程序结构分析

因为第一次作业结构在第二三次中也能有比较好的体现,以下主要是第二三次作业的分析

第二次作业

第二次作业类图如下:

面向对象第二单元总结 随笔 第3张

 

面向对象第二单元总结 随笔 第4张

主线程创建Elevator线程,在Elevator线程中要实现人的上下时又创建了一个Stepio线程,输出与Elevator线程的sleep(400)并行(实际意义不大,在第三次中取消了)

第二次作业协作图如下:

面向对象第二单元总结 随笔 第5张

 面向对象第二单元总结 随笔 第6张

 

第三次作业

第三次作业类图如下:

面向对象第二单元总结 随笔 第7张

 

面向对象第二单元总结 随笔 第8张

线程间调度情况同作业二,只是取消了Stepio

协作图如下:

面向对象第二单元总结 随笔 第9张

 

面向对象第二单元总结 随笔 第10张

可以看出我的设计电梯与人的独立性都比较好,但相应的调度器承担了比较重的任务,在调度变得复杂时代码显得臃肿,后来思考改进方法可以把调度器进一步拆分,实现层次化调度。

BUG分析

在本单元三次作业中强测和互测都没有被找出bug,但是在第三次作业设计上我其实有一个bug,并且没有改正的交了上去。

while (true) {
           aimFloor = Scheduler.getTask(nowFloor,label,up);
           if (aimFloor == 0) {
               if (Scheduler.getEndMark()) {
                   synchronized (Scheduler.lock) {
                       Scheduler.lock.notifyAll();
                  }
                   return;
              }
               synchronized (Scheduler.lock) {
                   try {
                       Scheduler.lock.wait();
                  } catch (Exception e) {
                       e.printStackTrace();
                  }
              }
          } else {
               if (aimFloor > nowFloor) { up = true; }
               else if (aimFloor < nowFloor) { up = false; }
               move();
          }
      }

如上是我第三次作业电梯线程run方法的主循环,可以看到当电梯没有分配到任务时先向调度器询问是否下班,若得到否定回答则在拿到lockwait

现在设想这样一种情况,主线程输入大批数据后没有接着结束输入二是处于等待输入的阻塞态。三个电梯接到任务后各自运行,很凑巧的,他们同时结束了运行任务都到了向调度器再要任务的状态。并且由于等待队列为空都没要到任务,于是都开始询问是否可以下班。

A询问,由于输入未结束,endMark未置位,不可下班,A准备拿锁。此时主线程输入结束将endMark置位并准备拿锁

if (p == null) {
               Scheduler.setEndMark();
               synchronized (Scheduler.lock) { Scheduler.lock.notifyAll(); }
               return;
          }

线程调度出于某种不可知的原因脑抽的将锁先给了main线程,main愉快的notifyall然后退出,此时endMark置位后B和C也来到了等锁状态,而我们已经疯掉的线程调度继续把锁给了B和C,他们也愉快的notifyall然后退出。此时我们可怜的A线程才拿到锁进入wait,而此时已经不会有人再去叫醒他了................

以上的逻辑听起来很疯狂,真要实现这种极端情况需要满足的条件几近苛刻,首先主线程必须神经病到输入完有效数据后不结束以便在A询问是否下班时抓住时机输入结束hack掉A,然后接到任务的电梯三兄弟必须心意相通的同时结束运行到达同一状态,再然后我们的线程调度必须疯掉不按顺序调度,最后A要正好得罪了线程调度成为调度疯掉的受害者..........

但不是不可能对不对(此处杠精

作为架构设计者应当尽量从逻辑层面避免错误的可能性,而不能把希望寄托于底层正常运行。(比如万一碰上了我们OS写的操作系统,还跑在我们祭祖烧的CPU上

虽然这么说我最后还是没想到解决办法不然也不会带着bug往上交,如果有逻辑层面避免这种情况的办法还请不吝赐教。

BUG测试

比起各位神仙大佬搭的评测机,我选择用C写了一个二十行的小程序生成符合要求的随机数据进行测试 ,不过正确性还是靠眼看的(听起来很没用)。与互测中没有被hack相应,我也没有hack别人,三次作业都是一屋子空刀,刚刚够活跃度下限,佛系好啊。

心得体会

线程安全方面,一是注意临界资源使用的同步,二是注意避免死锁,临界区应该尽量的短,不用同步时立即释放锁,尽力避免锁内拿锁。

设计方面,应该分离各部分功能,使得更改要求时不至于全部更改,注意对一个类功能的抽象,建立完后审视,那些是这个,类必须具有的方法,那些是这个类可有可无的方法,对类进行瘦身,才能在后来更好的继承,增加功能总比增删功能要简单些也不容易出错。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄