第二单元 OO博客

1 同步块的设置和锁的选择

我对同步块的设计是采用synchronized关键字,并使用wait-notifyAll模式。本次作业中的请求队列RequestQueue可以同时被输入线程、调度器线程和电梯线程访问,因此存在线程安全问题。因此在RequestQueue中,我采用了对方法加锁的方式,将其中涉及到成员变量读写的方法全部加了锁,这可以保证每次只有一个线程来访问请求队列。

尽量减少synchronized块的长度,增加线程间的并发程度;在同步块中合理使用waitnotifyAll,使用不当会导致轮询、死锁等现象。

保证锁的一致性,在第三次作业中,对换乘层是否被占领,最初采用的wait-notifyAll由于锁的不正确性,导致错误。所以在wait-notifyAll中一定要先判断清楚锁是什么,再对方法上锁。

其中,请求队列RequestTable可以同时被输入线程和电梯线程访问,因此存在线程安全问题。在本次作业中,我主要是采用了”对方法加锁“的方式,将RequestTable涉及到成员变量读写的方法全部加了锁,这样就可以保证每次只有一个线程来访问请求队列。

2 调度

2.1 调度器设计

我的调度器采用单例模式,在全局中只实例化一个调度器对象,使调度器能够更好地统筹全局,同时,在调度器中新建双轿厢电梯,使用单例模式有利于DCReset的实现。

单例模式采用的是饿汉式单例模式的设计,仅在首次调用时才创建单例实例。这意味着实例只在需要时才被创建,可以节省资源。最初考虑的懒汉式实现需要考虑线程安全问题,因为在多线程环境下,可能出现多个线程同时调用 getInstance() 方法,导致创建多个实例的问题,所以最终采用的是饿汉式单例模式。

public class Controller extends Thread {
        private static volatile Controller instance;
  
        public static Controller getInstance(RequestQueue allRequests,
                                         HashMap<Integer, RequestQueue> eleRequests) {
            if (instance == null) {
                synchronized (Controller.class) {
                    if (instance == null) {
                        instance = new Controller(allRequests, eleRequests);
                    }
                }
            }
            return instance;
        }
}

在调度器中,有以下属性:

private RequestQueue allRequests;
private HashMap<Integer, RequestQueue> eleRequests;
private int curPersonRequestNum = 0;

其中,allRequest是所有电梯的总请求队列,eleRequests是以电梯idKey的电梯各自请求的HashMapcurPersonRequestNum表示当前未完成的请求个数,用于线程的结束。

2.2 调度器如何与程序中的线程进行交互

2.2.1 与输入线程InputThread

  • 关于调度

    输入线程中也有allRequest,用于接收乘客请求,当输入请求为PersonRequest,则加入allRequest,调度器会不断从allRequest中取出请求,并分配电梯给该乘客。

  • 关于线程的结束

    当不再输入请求,InputThread会将allRequestisEnd设为true,提示调度器输入的结束。在调度器中,调度器线程结束的条件是allRequests.isEnd()、所有电梯不在Reset状态, 以及 curPersonRequestNum == 0,即所有乘客请求都结束。

2.2.2 与电梯线程ElevatorThread
  • 关于调度

    调度器不断从allRequest中取出请求,采用平均分配的策略分配给电梯 i ,则从eleRequests取出电梯 i 的RequestQueue,将此请求分配到该电梯的请求队列。

  • 关于线程结束

    当调度器达到本线程的调度条件,就会将所有电梯请求队列的结束标志isEnd设为true,提示电梯请求队列线程的结束。

  • 关于Reset

    第三次作业是在电梯线程中实现DCReset的,当电梯得到DCReset的标志时,会开始时清空电梯、清空RequestQueue、利用单列模式的调度器创建双轿电梯等一系列工作。在调度器中实现双轿电梯的建立,启动,并将其的请求队列加入eleRequests进行统一的调度管理。

    public synchronized void createDcElevator(DoubleCarResetRequest dcRequest) {
            /**********双轿电梯的建立*********/
            FloorManager floorManager = new FloorManager(id,transferFloor);
            ElevatorThread elevatorA = new ElevatorThread();
            ElevatorThread elevatorB = new ElevatorThread();
            /**********加入请求队列**********/
            int num = eleRequests.size();
            eleRequests.put(num + 1, elevatorA.getRequestQueue());
            eleRequests.put(num + 2, elevatorB.getRequestQueue());
            /***********双轿电梯启动*********/
            elevatorA.start();
            elevatorB.start();
    }
2.3 调度策略

在第二、三次作业中我使用的是平均分配策略,具体实现方式(以第三次作业为例)如下:

其中,lastId作为一个全局变量,记录上次检索到的电梯号,若 lastId++后越界,会更新为1。

若返回的电梯id为0说明没有一辆合适的电梯可以接此乘客,则让线程休眠,防止轮询。

while (true) {
        int elevatorId = elevatorIdForPerson(person);
        if (elevatorId != 0) {
                eleRequests.get(elevatorId).addRequest(fromFloor, person,false);
                break;
        }
        try {
                sleep(200);
        } catch (InterruptedException e) {
                e.printStackTrace();
        }
}
2.4 调度策略是如何适应时间、电量等多个性能指标
  • 出现五个电梯同时Reset的情况

通过限制每个电梯的请求列表个数,防止在五个电梯同时Reset时,调度器将这期间的所有请求都分配给未Reset的电梯。

  • 思考

最开始,采用根据电梯的楼层与方向等属性估算一个时间值,择优选择电梯,具体时间值的实现是在电梯线程中进行,因为加入synchronized上了锁,所以该方法的实现会电梯的其他方法的影响,而电梯中的大量方法涉及开关门、移动等,得到时间值的方法等待的时间长,所以获得每一部电梯的时间值会经历一段相当长的时间,导致我的调度策略实现时间过长,在Controller类中,调度过慢。

所以后来使用了平均分配的方法,平均分配的好处就是防止电梯请求的分配不均,使得有些电梯的运量过大等,在平均的一个状态下完成调度。但是带来的坏处也是很显然,因为平均分配的随机性,可能会错过一更好的分配方式,导致性能分的丢失。

3 架构

3.1 第五次作业

在第五次作业中,我的代码架构基本形成与定型,其中包括输入线程、调度线程、请求线程、电梯线程、策略类等。

  • RequestQueue请求线程

    • 在本次作业中,我实例化了七个RequestQueue对象。其中allRequests是总的请求队列,HashMap<Integer, RequestQueue> eleRequests是以电梯 idkey 的请求列表的HashMap

    • 此线程有属性:

       private HashMap<Integer,ArrayList<Request>> requests = new HashMap<>();//<出发地,此出发地的所有请求>

      其为以出发地为Key的乘客请求列表的HashMap,存储该请求队列的请求个体。

    • 对于allRequestQueue,设置了两个方法popRequest()addRequest()

    • popRequest()目的是遍历 personRequests 集合中的每一个元素,找到第一个非空的 ArrayList<PersonRequest> 集合,并删除其中的第一个元素,然后返回这个元素。同时,如果删除元素后集合为空,还会将该键值对从 personRequests 中删除。(迭代器)

  • InputThread输入线程

    • 该线程中有属性:

      private RequestQueue allRequests;
    • 在本线程中,不断读取输入的请求,加入总的请求队列,等待调度器给电梯分配请求。

    • 当没有新的请求,将总请求队列的结束标志isEnd设为true,提示输入线程结束。

  • Controller调度线程

    • 该线程有属性:

      private RequestQueue allRequests;//所有的请求
      private HashMap<Integer, RequestQueue> eleRequests;//<电梯号,电梯的对应请求>
    • allRequests.isEmpty() && allRequests.isEnd(),总请求列表没有请求且输入结束,说明进程的结束,此时要将所有电梯的请求列表的结束标志isEnd设为true,提示电梯线程结束。

    • 按照PersonRequest的要求分配电梯,将此请求加入对应电梯的请求列表。

      int eleId = request.getElevatorId();
      eleRequests.get(eleId).addRequest(request);
  • elevator电梯线程

    • 在Main类中实例化六部电梯,各自独立运行。

    • 该线程有属性:

      private Strategy strategy = new Strategy();//电梯的决策者
      private int id;
      private int curNum = 0;//当前人数
      private int curFloor = 1;//当前楼层
      private boolean direction = true;//方向
      private RequestQueue requestQueue;//电梯自己的requestTable
      private HashMap<Integer, HashSet<Request>> destMap = new HashMap<>();//电梯内乘客的目的地Map

      可以看出,每一个电梯都有自己的RequestQueue请求队列、strategy决策类,还有一个以目的地为Key的电梯内乘客的HashMap

    • 电梯在while(true)中不断使用策略类来做下一步的决定:

      public void run() {
              while (true) {
                  Advice advice = strategy.getAdvice(curFloor, curNum,
                                                     direction, destMap, requestQueue);
                  if (advice == Advice.OVER) {
                      break;                              //电梯线程结束
                  }
                  else if (advice == Advice.MOVE) {
                      move();                             //电梯沿着原方向移动一层
                  }
                  else if (advice == Advice.REVERSE) {
                      direction = !direction;             //电梯转向
                  }
                  else if (advice == Advice.WAIT) {
                      requestQueue.waitRequest();         //电梯等待
                  }
                  else if (advice == Advice.OPEN) {
                      openAndClose();                     //电梯开门
                  }
              }
          }
    • move操作
      • 电梯沿原方向移动一层,若direction == true,则curFloor++,否则,curFloor--

      • 移动后,若curFloor == 11或者curFloor == 1,则直接转向,这样可以节省一步判断。

    • openAndClose操作
      • 遵循先出后进,根据destMap将以本层为目的地的乘客全部送出,再次利用Advice advice = strategy.getAdvice(curFloor, curNum,direction, destMap, requestQueue);判断电梯是否转向,若需要转向,则直接转向。最后在没有超载的情况下,根据eleRequestQueue加入以本层为出发地且同向的乘客。

      • 在进出乘客之间插入一次策略的询问,在出乘客后电梯没人,但是电梯外乘客的到达楼层不在电梯运行方向的情况下,可以减少一次电梯的开关门。

    • waitRequest()操作

      采用wait-notifyAll模式,在RequestQueue中,增加waitRequest()方法:

      public synchronized void waitRequest() {
              try {
                  wait();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      }

      在addRequest()操作后增加notifyAll(),来唤醒等待线程。

  • Strategy类

    每个电梯都有自己的策略类,即策略与运行分离。它可以根据目前电梯的状态给出策略,我采用的是LOOK算法。

    • 首先为电梯规定一个初始方向,然后电梯开始沿着该方向运动。

    • 到达某楼层时,首先判断是否需要开门

      • 如果发现电梯里有人可以出电梯(到达目的地),则开门让乘客出去;

      • 如果发现该楼层中有人想上电梯,并且目的地方向和电梯方向相同,则开门让这个乘客进入。

    • 接下来,进一步判断电梯里是否有人。如果电梯里还有人,则沿着当前方向移动到下一层。否则,检查请求队列中是否还有请求(目前其他楼层是否有乘客想要进电梯)——

      • 如果请求队列不为空,且某请求的发出地是电梯"前方"的某楼层,则电梯继续沿着原来的方向运动。

      • 如果请求队列不为空,且所有请求的发出地都在电梯"后方"的楼层上,或者是在该楼层有请求但是这个请求的目的地在电梯后方(因为电梯不会开门接反方向的请求),则电梯掉头并进入"判断是否需要开门"的步骤(循环实现)。

      • 如果请求队列为空,且输入线程没有结束(即没有输入文件结束符),则电梯停在该楼层等待请求输入(wait)。

      • 如果请求队列为空,且输入线程已经结束,则电梯线程结束。

3.2 第六次作业

本次作业的框架与上次作业一致,以下根据新增加的Reset请求,说明架构的迭代部分:

  • RequestQueue请求队列线程
    • 由于电梯线程的不安全性,由最初的在电梯线程中加入Reset标志改为在RequestQueue中加入Reset标志。

    • 增加属性:

      private ResetRequest resetRequest = null;

      resetRequest的最初值是null,当电梯被reset时,此ResetRequest会更改为相应的请求,在Reset结束之后,会重新更改为null,所以可以根据resetRequest == null来判断电梯是否在Reset阶段。

  • InputThread输入线程

    因为前期通过InputThread调用调度器来对电梯请求队列进行setResetRequest()操作,出现了线程不安全的问题(可能与当时其他部分代码的错误有关),所以我将这一步直接让InputThread线程来操作,减少对调度器的调用:

    if (request instanceof ResetRequest){ //ResetRequest
            int elevatorId = ((ResetRequest) request).getElevatorId();
            eleRequests.get(elevatorId).setResetRequest((ResetRequest) request);
    }
  • Elevator电梯线程
    • getAdvice()中增加参数resetRequest,若resetRequest不为null,则返回AdviceRESET,电梯进入Reset阶段,否则与第五次作业一样。

    • 增添Reset()方法实现电梯的Reset:

      public synchronized void reset() {
              //清空电梯,电梯内人出去
              clearElevator();
              //清空requestQueue
              allRequestQueue.addRequestQueue(requestQueue);
              //RESET
              this.capacity = newCapacity;
              this.speed = newSpeed;
              //stop
              this.requestQueue.setResetRequest(null);
      }
      • clearElevator();

        经历了"开门 - 乘客出去 - 关门"阶段,其中已到达目的地的乘客直接清除,若有未到达的乘客,将其的出发楼层改为先楼层,加入allRequestQueue总请求队列,等待调度器继续为他分配电梯。

      • 清空requestQueue

        将本电梯的请求队列全部加入allResetQueue,等待调度器继续为未完成的请求分配电梯。

  • 关于线程的结束
    • 当输入结束,输入线程InputThread会将allRequest的结束标志设为true

    • 在调度器线程中,当满足allRequests.isAllEmpty() && allRequests.isEnd()即所有请求分配结束,且不再有输入,不一定意味着结束,因为在满足此条件后,有可能发生电梯在Reset阶段将自身的一些请求仍回总表的事情,所以要添加一次判断是否所有的电梯都不在Reset阶段。

      while (true) {
          if (allRequests.isAllEmpty() && allRequests.isEnd()) {
              if (isAllNotReset()) {
                  for (int i = 1; i <= 6; i++) {
                      eleRequests.get(i).setEnd(true);
                  }
                  return;
              } else {
                  try {
                      sleep(500);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      }

      可以注意到,若还有电梯在Reset阶段,我添加了一个sleep环节,是因为,若还有电梯在Reset阶段,在while(true)中会一直连续不断地判断,导致轮询,根据电梯Reset的时间1200ms,取了一个接近的中间值,让线程sleep500ms,解决了轮询的问题。

3.3 第七次作业

本次作业的框架与上次作业一致,以下根据新增加的DCReset请求,说明架构的迭代部分:

  • RequestQueue请求队列线程
    • 增加属性:

      private DoubleCarResetRequest doubleCarResetRequest = null;

      doubleCarResetRequest的最初值是null,当电梯被DCReset时,此doubleCarResetRequest会更改为相应的请求,在DCReset结束之后,会重新更改为null,所以可以根据doubleCarResetRequest == null来判断电梯是否在DCReset阶段。

  • FloorManager类

    这是用于管理换乘层的类,一对双轿电梯对应一个FloorManager。

  • Controller调度器线程
    • 由于DCReset需要创建双轿电梯,并且考虑到Controller是统筹全局的一个单例,所以在本次作业中,我将Controller设为饿汉式单例模式。

    • 调度策略的调整(见上文)。

    • 在Main主类中第一次创建,在之后的其他地方就无需创建新的Controller线程,保证了线程的安全性。

      Controller controller = Controller.getInstance(allRequests, eleRequests);
    • 增加了创建双轿厢电梯的方法:

      public synchronized void createDcElevator(DoubleCarResetRequest dcRequest) {
              //双轿厢电梯以及相应的FloorManager的建立
              FloorManager floorManager = new FloorManager(id,transferFloor);
              ElevatorThread elevatorA = new ElevatorThread();
              ElevatorThread elevatorB = new ElevatorThread();
              //将两部双轿厢电梯的请求列表加入eleRequests的HashMap中
              int num = eleRequests.size();
              eleRequests.put(num + 1, elevatorA.getRequestQueue());
              eleRequests.put(num + 2, elevatorB.getRequestQueue());
              //电梯启动
              elevatorA.start();
              elevatorB.start();
      }
  • ElevatorThread电梯线程
    • 增加了双轿厢电梯的相关属性:

      private boolean isDoubleCar = false;//是否为双轿电梯
      private int transferFloor = 0;//换乘楼层
      private FloorManager floorManager;
      private char dcAorB;//是双轿电梯的A梯还是B梯
    • 增加了dcReset方法,完成DoubleCarResetRequest:

      其中步骤基本和NormalReset相似,增加了引入Controller的单例模式来创建双轿厢电梯。

      public synchronized void dcReset() {
              //清空电梯,电梯内人出去
              clearElevator();
              //清空requestQueue
              allRequestQueue.addRequestQueue(requestQueue);
              //创建双轿电梯 
              Controller controller = Controller.getInstance(null,null);
              controller.createDcElevator(dcRequest);
              //stop
              this.requestQueue.setDoubleCarResetRequest(null);
              this.isEnd = true; //本电梯线程结束
              this.requestQueue.setEnd(true); //本电梯的请求列表结束
          }

      创建完双轿电梯,我就将原电梯的结束标志设为true,将原电梯的请求列表线程的结束标志也设为true,终结原电梯的运行。

    • 双轿厢电梯的换乘处理——move()操作(以A梯为例)

      如果下一层的楼层不是换乘层,则执行原有的move操作;

      如果下一层的楼层是换乘层,首先判断换乘层是否被占,其中运用了floorManager对换乘层的管理:

      while (floorManager.isOccupied()) { //换乘层被占
            try {
                 sleep(200);
            } catch (InterruptedException e) {
                 e.printStackTrace();
            }
      }

      当换乘层不被占,则跳出循环,来到接下来的操作:

      //占领换乘层
      floorManager.setOccupiedState(true);
      curFloor++;
      direction = !direction;

      到达换乘层后,有本层的请求要进,或者目的地在换乘层及换乘层上的要出,需要实现换乘操作:

      if(this.requestQueue.getThisFloorRequest(curFloor)!= null||!isDestMapEmpty()) { 
           transferA();
      }

      换乘操作包括"开门 - 出客 - 进人 - 关门",其中,出客指的是换乘层及换乘层以上的乘客全部出去,若已经到达目的地,说明请求已经完成,若未到达目的地,修改该乘客的出发楼层,将其重新加入allRequest,等待其他电梯的分配。

      换乘结束后,需要立即离开换乘层,不占用换乘层,这也是我的解决相撞的一个方法。

      curFloor--;
      floorManager.setOccupiedState(false);       

3.3.2 双轿厢不相撞处理

首先,我设置了一个floorManager类来管理换乘楼层:

public class FloorManager extends Thread {
    private boolean occupied = false;
​
    public synchronized boolean isOccupied() {
        return this.occupied;
    }
    public synchronized void setOccupiedState(boolean occupied) {
        this.occupied = occupied;
    }
}

每一对双轿电梯有同一个floorManager,对于双轿厢电梯的运行,我的解决方法如下:当下一个楼层是换乘层的话,若换乘层被占,则等待对方电梯离开后占领换乘层,在完成换乘操作后,直接离开换乘层,不长时间占用换乘层,大致步骤如下:

while (floorManager.isOccupied()) { //换乘层被占
       try {
            sleep(200);
       } catch (InterruptedException e) {
            e.printStackTrace();
       }
}
//不被占
//占领换乘层
floorManager.setOccupiedState(true);
curFloor++;
direction = !direction;
//换乘
transfer();
//离开换乘层
curFloor--;
floorManager.setOccupiedState(false);

其中对换乘层被占的解决,我没有采用wait-notifyAll()模式,是因为由于我的不当使用,造成死锁、轮询,心在细想,是由于我锁住的对象不统一造成的,在最开始的代码中,我在电梯线程中使用synchronized块,实际上,这个锁是电梯线程本身,并非floorMagager,所以在floorManager中无法唤醒线程。所以,在最终的版本,我采用了sleep()

3.4 可扩展性、稳定和易变
3.4.1 可拓展性

如果要更改楼层限制,只需修改常数MAX_FLOOR,MIN_FLOOR,增加电梯总数可以修改MAX_NUM,修改初始电梯数量修改INITIAL_NUM,容量CAPACITY等等这些常数。

如果要更换调度策略或者电梯运行策略只要新加调度函数即可,换成策略改变也可以新增换乘函数等等。

3.4.2 稳定与易变
  • 电梯的运行LOOK策略、线程的交互都是稳定的,在三次作业中基本变化不大。

  • 易变的是调度策略,若需要性能更好的调度策略,我们需要在调度器类重新编写。

  • 同时根据两次不同的迭代,需要在不同的类增添不同的属性与方法来适应题目新的要求。包括一些电梯的具体运行虽然在总体上基本不变,但是还需要针对不同的要求进行适当的修改。

4 bug & debug

4.1 第五次作业

在电梯openAndClose()开关门的方法中,我使用了一个循环内删除的方法:

for (Request r : curIn) {
      destMap.get(destination).add(r);
      requestQueue.deleteRequest(r);
}   

RequestQueuedeleteRequest()方法:

public synchronized void deleteRequest(Request r) {
    requests.get(fromFloor).remove(r);
}

可以看出这是一个跨类的删除操作,由于没有加入迭代器,出现了ArrayListConcurrentModificationException异常。最后放弃跨类删除,直接在Reuest类中完成迭代器删除操作。

4.2 第六次作业
4.2.1 Reset不及时

最初我将电梯Reset的标志属性存储在电梯线程中,由于电梯线程的复杂性与不安全性,经常出现Reset不及时的问题,即RESET-ACCEPTRESET-BEGIN这段时间过于长,所以我将电梯的Reset标志属性以resetRequest的形式存储在RequestQueue中。

4.2.2 关于何时reset

最初我是在Controller即调度器中实现Reset操作,即Input输入线程读入ResetQuest会调用调度器对该电梯的RequestQueue中的ResetQuest作出设置。

但是,那个时候,我写的调度策略是采用根据电梯的楼层与方向得到一个时间值,择优选择电梯,具体时间值的实现是在电梯线程中进行,因为加入synchronized上了锁,所以该方法的实现会电梯的其他方法的影响,而电梯中的大量方法涉及开关门、移动等,得到时间值的方法等待的时间长,所以获得每一部电梯的时间值会经历一段相当长的时间,导致我的调度策略实现时间过长,在Controller类中,调度过慢,导致调度器对该电梯的RequestQueue中的ResetQuest作出设置的反应时间慢。

当时没有意识到这个问题,以为是Input线程调度Controller线程有时间差,没有进行及时的Reset,所以,我选择在Input线程中一读到ResetRequest请求就设置Reset标志。

4.2.3 调度策略

正如上文所说,最开始的调度策略,我是根据电梯的楼层与方向估算一个相应的时间值,选择用时少的电梯,具体得到时间值的方法由于要用到电梯的一系列性质,我选择在电梯线程中实现,因为加入synchronized上了锁,所以该方法的实现会电梯的其他方法的影响,而电梯中的大量方法涉及开关门、移动等,得到时间值的方法等待的时间长,所以获得每一部电梯的时间值会经历一段相当长的时间,导致我的调度策略实现时间过长。

最初没有意识是电梯线程的问题,所以放弃了这个方法,直接在Controller调度类中实现平均分配策略。

4.2.4 轮询问题

以下是Controller调度器类中的结束代码:

while (true) {
    if (allRequests.isAllEmpty() && allRequests.isEnd()) {
        if (isAllNotReset()) {
            for (int i = 1; i <= 6; i++) {
                eleRequests.get(i).setEnd(true);
            }
            return;
        } 
    }
}    

allRequests.isAllEmpty() && allRequests.isEnd(),但是还有电梯还在Reset状态,则会不断进行判断,造成轮询状态,所以根据电梯Reset的时间为1200ms,取了相对平均的中间值500ms,让线程休眠一会儿,再进行判断,避免轮询。

while (true) {
            if (allRequests.isAllEmpty() && allRequests.isEnd()) {
                if (isAllNotReset()) {
                    for (int i = 1; i <= 6; i++) {
                        eleRequests.get(i).setEnd(true);
                    }
                    return;
                } else {
                    try {
                        sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
}
4.2.5 五个电梯同时reset问题

当五辆电梯同时在Reset状态时,如果在同一时间有大量的请求,调度器会把这些请求分配给同一部电梯造成运行超时。所以为了避免这种情况发生,我限制了每辆电梯的请求个数,当超过这个限制,则不会再给这部电梯分配新的请求,只好等下一次的调度。

同时可能发生轮询情况,因此我同样利用sleep,让电梯休眠一段时间。

while (true) {
      int elevatorId = elevatorIdForPerson(person);
      if (elevatorId != 0) {
             eleRequests.get(elevatorId).addRequest(fromFloor, person);
             break;
      }
      try {
             sleep(200);
      } catch (InterruptedException e) {
             e.printStackTrace();
      }
}
4.2.6 电梯参数修改未使用

因为这个bug我强测的分数低的可怕,没有进入互测,在判断乘客进电梯的时候,还是沿用第五次作业capacity为6的情况,导致错误,细节没有认真检查。

4.3 第七次作业
4.3.1 双轿厢电梯相撞问题

在上文说过,我是在电梯线程中wait,在floorManager中notifyAll,两个方法的锁不一样,自然无法唤醒。最初没有深入思考,所以选择投机取巧,选择了让线程sleep。

while (floorManager.isOccupied()) { //换乘层被占
       try {
              sleep(200);//本来是wait()
       } catch (InterruptedException e) {
              e.printStackTrace();
       }
}
4.3.2 乘客请求未完成线程就结束问题

由于所有线程的结束条件是输入结束且分配完毕且没有电梯在reset阶段且电梯所有请求完成,由于加入双轿电梯,当最后一个请求是需要换乘的双轿厢电梯,换乘之前,总请求队列分配完毕且结束,换乘后,未完成的请求加入总请求无意义,造成乘客请求未完成。

所以在总请求队列中我加入了未完成情求个数的统计,只有乘客请求到达指定的目的地,未完成情求个数才能减一,在判断线程是否结束的过程中,我们需要追加判断未完成请求个数是否为0,即可解决。

4.4 debug方法
  • 主要采用的是官方包的Timable.println()方法,对线程每一个步骤进行实施跟踪,分析时序。

  • 面对轮询问题,在每一个while循环的开头打印相关标志,准确定位轮询位置。

  • 对乘客进行跟踪,利用测评机的分析功能,有些乘客进出电梯后不换乘或者进入电梯后不出电梯,都可以通过测评机检测出来

5 心得体会

  • 线程安全

    三次作业由于线程安全的问题产生了很多bug,我梳理了一下几点:

    • 加锁要慎重

      对共享对象做同步处理不意味之要将所有的方法都加上锁,会极大影响并发效率。要尽量减少synchronized块的长度,增加线程间的并发程度;在同步块中合理使用waitnotifyAll,使用不当会导致轮询、死锁等现象。

    • 保证锁的一致性

      在第三次作业中,对换乘层是否被占领,最初采用的wait-notifyAll由于锁的不正确性,导致错误。所以在wait-notifyAll中一定要先判断清楚锁是什么,再对方法上锁。

  • 层次化设计

    在最初接触电梯作业时,没有形成完整的流程和功能模块的分析,导致最初的代码写得磕磕绊绊。在面对多线程编程时,要首先根据需要实现的功能,分成多个不同的功能模块,利用前驱图,调度关系,梳理各个线程之间的同步与互斥关系,避免死锁、无人唤醒等异常情况。并规定好不同模块之间的接口,利用共享对象传递信息,然后再自上而下依次实现。

    第一次接触多线程编程,面对第五次作业有点无从下手,实验代码的代码架构为我提供了思路。在实验架构的基础上,我增添了调度策略算法、电梯运行策略分析,实现一定的层次化设计。

    层次化设计有利于迭代开发,三次作业的架构基本没有变化,只需根据新的要求,分析新的流程,增添新的功能模块。所以,在最初对作业进行层次化设计是很有必要的,不仅使思路更加清晰,提高debug的效率,对后续的迭代开发也是很有帮助的。

  • 体会

    继上次第三次作业求导细节错误导致没有进互测屋,第六次作业也发生了相似的情况,即电梯进客时采用的容量仍为6,导致超载,细节方面还是没有认真检查,让我非常懊悔。希望在之后的单元中,一定要注意审题,检查细节,提高自己的架构设计能力,养成封装,层次化设计的习惯与能力。

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值