OO2022第二单元个人总结

在第二单元中,我们学习了多线程程序的运行,同时也了解了如何在并发运行时,保障程序的可靠性与安全性。在第一次作业中,由于每个楼座只有一座电梯,实际上并没有多电梯共享同一资源的情况,几乎不会出现安全问题(除了需要重新封装安全化输出类😂)。在第二次作业中新增了允许多台电梯与横向电梯,此时才出现了多电梯共享同一资源的情况,线程安全问题开始显现。在第三次作业中,需要转楼的乘客开始出现,此时便出现了电梯接力的情况(即一个电梯的离开乘客成为另一个电梯的进入乘客)。三次作业在架构上层层递进,所以每一次作业的功能设计与可扩展性的设计就显得尤为重要。

1.第一次作业

1.1 线程设计

由于之后的每次作业都是在第一次作业的框架上进行拓展得到的,所以保障第一次作业框架设计的正确性就显得尤为重要,不仅要有能处理现成的基本问题,还要预留充足的可扩展空间,在写代码之前我们应该明确以下几个问题。

共享区(共享数据)是什么?

显然,各个共享区之间的数据应该互不干扰,所以在第一次作业中,应该把A、B、C、D、E,五个共享区,(并不推荐把每座楼的每一层当作共享区),而每个共享区的请求队列就是共享数据。在本次作业中,我将所有的操作共享数据的方法都放到了共享区内,即电梯是通过共享区来操纵共享数据。

电梯的行为由谁来控制?

在本次作业中,我将电梯的运行权限都交给了电梯。电梯是否开门:电梯每到一层去判断自己是否需要开门(门外有人想要进入,电梯内有人想外出);电梯向那移动:根据电梯自己的方向;电梯空了向哪接人:电梯内无人时自己去遍历共享区内队列;电梯是否停止:电梯内无人,且发现共享区内队列为空,共享区的结束标志位已置为,自己退出循环。

安全问题有哪些(该怎么加锁)?

写写类:输入线程生产请求,电梯消费请求,不能同时进行。

读写类:电梯遍历请求,输入线程生产请求,不能同时进行。

解决方案:将共享区内读写请求队列的函数上加上synchronized,就会将this(共享区当作锁),就能保障对于一个共享区,同一时刻只能有一个线程在读写共享数据。

还有一些细节上的问题值得一提。

  • 电梯在某层开门后,本层来了新人要怎么处理?

//判断是否需要开门
           if (tray.ifEnter(in, type, layer, direction) || tray.ifLeave(in, layer)) {
               Output.output("OPEN-" + this.type + "-" + this.layer + "-" + this.id);
               tray.leave(in, type, layer, id);
               try {
                   Thread.sleep(400);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               tray.enter(in, type, layer, direction, id);
               Output.output("CLOSE-" + this.type + "-" + this.layer + "-" + this.id);
          }

由于制片人没有厚度,采用开门-->出人-->等待-->进人-->关门的模式。

  • 电梯处于等待状态的某一时刻来了多人(但输入总有先后,可能其他楼层请求在前,本层请求在后,电梯就直接离开了)?

if (in.isEmpty()) {
               if (tray.ifEmpty(type)) {
                   if (tray.getOver()) {
                       break;
                  }
                   tray.setWait();
                   try {
                       Thread.sleep(1);
                  } catch (InterruptedException e) {
                       e.printStackTrace();
                  }
              }

我的策略是在电梯醒来后,再睡1ms(🐶,既不会怎么影响性能,又可以解决问题)。

1.2 调度器的设计

在本次作业中,我的调度器只负责将输入的请求分配到相应的共享区中,即调度器只负责将请求加入各个共享区,其余请求由电梯自主完成(电梯每到一层会自主去判断共享区与自身的状态,从而选择合适的操作【开门,进人,找人(als),等待,结束】)。

 

 

1.3 第一次作业bug

  • bug1:作业给的输出函数类并不安全,需要重新封装(幸运的没有被检测出来😄)。

  • bug2:注意增强for使用的是迭代器来遍历,就算遍历时没有修改数据仍然有可能出现bug,建议使用普通for来遍历。

2.第二次作业

2.1线程设计

线程安全问题在本次作业中就开始突显,为了更好的完成本次作业,我们也需要明确一下几个问题。

共享区(共享数据)是什么?

本次作业增加了横向电梯,我们会发现,由于乘客无论是走横向还是纵向都只能走直线,虽然楼层在各楼座的内部,但实际上,二者在共享区上没有任何联系,所以在本次作业中可以划分出15个共享区,即楼座A~B,与楼层1~10。

安全问题有哪些(该怎么加锁)?

解决方案:本次作业沿用上次作业的加锁方案,将共享区内读写请求队列的函数上加上synchronized,把this(共享区)当作锁,使得对于一个共享区,同一时刻只能有一个线程在读写共享数据。

还有一些细节上的问题值得一提。

  • 由于在多电梯的接人设计上,常用的是自由竞争的策略,可能会出现开门但不接人的情况,从而影响性能。

//判断是否需要开门
       if (tray.ifEnter(in, type, layer, direction, id)) {
           try {
               Thread.sleep(400);
          } catch (InterruptedException e) {
               e.printStackTrace();
          }
           tray.enter(in, type, layer, direction, id);
           Output.output("CLOSE-" + this.type + "-" + this.layer + "-" + this.id);
      }
//判断此层是否有人要进入电梯
   public synchronized boolean ifEnter(ArrayList<Person> in,
                                       Character type, int layer, int direction, int id) {
       boolean flag = false;
       for (int i = 0; i < this.ap.size(); i++) {
           Person item = ap.get(i);
           int dir = 1;
           if (item.getRequest().getToFloor() - item.getRequest().getFromFloor() < 0) {
               dir = -1;
          }
           if (item.getRequest().getFromFloor() == layer && dir == direction && in.size() < 6) {
               flag = true;
          }
      }
       flag = flag | ifLeave(in, layer, id);
       if (flag) {
           Output.output("OPEN-" + type + "-" + layer + "-" + id);
           leave(in, type, layer, id);
           enter(in, type, layer, direction, id);
      }
       return flag;
  }

流程变为(开门+立刻出人+离开进人)+等待+进人+关门,括号内写在一个函数中即可,

2.2调度器的设计

在调度器的设计上也基本沿用了上一次的方案,即调度器只能向共享区中存放数据,而不能对电梯有任何的直接命令,只能通过共享区简介交互。

 

 

2.3第二次作业bug

  • bug:注意增强for使用的是迭代器来遍历,就算遍历时没有修改数据仍然有可能出现bug,建议使用普通for来遍历。(第一次作业就出现的bug,但我第一次作业再交一次就对了,没怎么在意,结果第二次作业又出现了😭)。

3.第三次作业

3.1线程设计

在第三次作业中,相较于前两次作业也出现了较大的变化,我重新封装了PersonRequest类,加入了一些新的属性,如下图所示。

public class Person {
   private PersonRequest request;
   private boolean transverse;
   private char tempBuildingStart;
   private char tempBuildingEnd;
   private int tempFloorStart;
   private int tempFloorEnd;
  ......
}

在请求进入等待队列时,我会根据他的起始位置【临时起点】(tempBuildingStart,tempFloorStart),为他们规划一条最短路径,分配各他们一个临时的终点(tempBuildingEnd,tempFloorEnd)加入到各个共享区中,当请求从电梯中离开时,当临时的终点与请求的终点不同时,会将临时的终点赋值给临时起点,并重新加入等待队列中。

首先,我们还是来回答一下几个问题。

共享区(共享数据)是什么?

第一,我们仍然将楼座A~B,与楼层1~10作为共享区。但是,由于本次作业中离开电梯时乘客请求若未被处理完全,也会再次加入等待队列,并根据等待队列(调度器)中保留的横向电梯数据来规划路径。此时,输入线程与各个共享区都会向等待队列(调度器)写入,所以等待队列(调度器)也成为了一个共享区,其中的。

线程要如何结束?

与前两次实验不同,本次实验无法让通过以前的方法(电梯内无人,且发现共享区内队列为空,外部输入结束)自行判断是否应该结束,因为在满足上述情况时,也能会有其他共享区的乘客会专程到这个共享区。本次实验中我在等待队列(调度器)中增加了一个计数器,当计数器归零与外部输入结束,我才会将各个共享区的结束标志置为,再让电梯自行判断是否结束。

安全问题有哪些(该怎么加锁)?

在本次作业中,出现了两种交互的共享区:等待队列(调度器),以下称为大共享区;各楼座楼层,以下称为小共享区。我们分别加上synchronized,把this当作锁,由于两个共享区会相互调用,所以我们会出现锁的嵌套,此时我们要避免死锁问题。当各楼座楼层的方法调用等待队列的方法时,显然是小锁--大锁的形式,考虑到如果我们将等待队列中的所有方法加上synchronized,当我们调用向小共享区分配请求的函数时会出现大锁--小锁的情况,会造成死锁,所以对于分配请求的函数,我们不加synchronized,形如:

public void addPerson(Person request) {
    scheduling(request);//大锁
    //加入队列
    if (request.isTransverse()) {
        Character tmp = (char) (request.getTempFloorStart() + '0');
        waitQueue.get(tmp).add(request);//小锁
    } else {
        Character fromBuildingType = request.getTempBuildingStart();
        waitQueue.get(fromBuildingType).add(request);//小锁
    }
    addCounter();//大锁
}

就不会出现大锁--小锁的情况,从而规避死锁。

还有一些细节上的问题值得一提

  • 注意对于那些未完全完成,需要再次加入等待队列的请求,我们要先把请求加入其他队列,再将他从本队列删除(否则计数器会在不该为0时置0)

//电梯内的人离开电梯
   public void leave(ArrayList<Person> in, Character type, int layer, int id) {
       for (int i = 0; i < in.size(); ) {
           if (in.get(i).getTempFloorEnd() == layer) {
               Output.output("OUT-" + in.get(i).getRequest().getPersonId() + "-" +
                       type + "-" + layer + "-" + id);
               if (in.get(i).getTempFloorEnd() != in.get(i).getRequest().getToFloor() ||
                       in.get(i).getTempBuildingEnd() != in.get(i).getRequest().getToBuilding()) {
                   in.get(i).setTempFloorStart(in.get(i).getTempFloorEnd());
                   in.get(i).setTempBuildingStart(in.get(i).getTempBuildingEnd());
                   wq.addPerson(in.get(i));
              }
               in.remove(i);
               wq.subCounter();
               continue;
          }
           i++;
      }
  }

 

3.2调度器设计

 

本次实验的调度器也成为了一个共享区,可以和小共享区之间相互访问,调度器会将请求队列中的请求分配给小共享区,小共享区会将未完成的请求再次加入请求队列,所以此次实验的线程结束就不能只靠电梯自己完成。电梯会在两种情况下判断是否结束:1.输入线程结束,判断计数器是否已经归零;2.计数器是否已经归零,判断输入线程是否结束。

3.3UML类图

 

 

3.4UML协作图

 

 

3.5debug3

本次作业幸运的并没有出现bug,但考虑到可能出现超时的问题,相比于als,个人更加推荐look策略。

4.分析bug所用策略

由于没时间(就是摆🐶),个人没有去具体的看他人的代码,互测时也只是随便弄了几个数据,随便构造了几个极端的数据(在允许的最后时刻放入最多的数据),就不献丑了。

5.个人心得

在层次化设计设计方面,感觉自己还是去偷懒了,个人为了写起来简单,并没有赋予调度器更多的权限去解决可能出现的极端情况,同时也不敢把电梯作为共享数据(电梯的行为对共享区而言完全是不可见的),就会出现一些损失性能的地方,比如是,应该根据到来的人数去判断应该有几座电梯动起来,来了一个人就只让一辆电梯动起来,而是让电梯自己去强人来运输,就、会出现来了一个人却有三辆电梯去接的情况,三次作业的总体架构都是输入--调度器--共享区--电梯。

同样是为了简单起见,我都只在函数上加synchronized,把this当作锁,而没有自己去建造锁什么的,最开始时是几乎每个涉及共享数据的函数我都加上了synchronized,但此时出现了当调度器向共享区加人时的调度器锁--共享区锁的模式,而当需要专程的乘客进入其他共享区时会出现共享区锁--调度器锁的模式,从而导致死锁现象,要不是别人提醒我就寄了。感觉以后写多线程时还是要自己建立更加精密的锁从一开始杜绝这些现象。

posted on 2022-04-28 20:49  计组战力单位  阅读(183)  评论(1编辑  收藏  举报