BUAA OO第二单元作业总结

一、作业设计策略

(一)第一次作业设计方案

  • 模型:生产者消费者模型
  • 两个线程:输入线程(生产者)、电梯线程(消费者)
  • 共享对象:请求队列
  • 退出模式:输入线程读到null,退出run,并将null传入请求队列,电梯线程取出null,即退出

第一次作业相对比较基础,我的设计上输入端向请求队列类添加请求、电梯线程从请求队列中依次取出请求,两个动作互斥,因此我对请求队列类方法上锁,解决写写冲突问题,保障线程安全。

关于程序结束的实现,由于这次电梯处理请求遵循FIFO原则,且一个请求仅会被添加到请求队列一次,因此文件结束标志null可以作为线程结束的flag。

(二)第二次作业设计方案

  • 模型:生产者消费者模型
  • 两个线程:输入线程(生产者)、电梯线程(消费者)
  • 共享对象:请求队列(调度器)
  • 退出模式:输入线程退出方法同第一次作业,在请求队列类中设置end标记,电梯使用者队列为空且请求队列到达End状态退出

第二次作业不再是无脑套用生产消费模型,电梯捎带需求对于电梯使用者队列更新与处理提出要求。我在请求队列的getRequest方法中实现电梯捎带,即将电梯当前楼层,电梯运行方向信息作为参数传入该方法,每一次电梯到达一个楼层(完成电梯任务或仅做Arrive动作),getRequest方法都会遍历请求队列,将可捎带请求加入电梯使用者队列。

关于电梯任务完成的优化,电梯的“出人”方法中,每一次要遍历使用者队列,将当前楼层可出的使用者全部送出。我将电梯队列中未离开的首个请求作为主请求,在从当前楼层前去“取人”以及将该人送到目标楼层的过程中,每一层都判断是否要出人是否要进人;在到达主请求楼层时,电梯不立即换向,先遍历使用者队列,将使用者中目标楼层同向的使用者都送走后,再调头。

这次作业的程序退出方式难度较大。基于我的设计,电梯从请求队列中取人不遵循FIFO策略,null不满足任何“捎带条件”因此将null传入使用者队列需要特判,另一方面,如果null传入了使用者队列,电梯完成任务时,每一次遍历使用者的目标楼层时,都要添加使用者不是null这一个条件,使代码十分不美观。因此,在这次设计中我在请求队列类中添加End标记,如果请求队列仅有null一个元素(null不传入电梯,其余需求都会以非捎带或捎带方式进入电梯),end标记为1,当电梯使用者队列为空并且电梯调度器为End状态时,电梯结束运行。

(三)第三次作业设计方案

  • 模型:生产者消费者模型
  • 四个线程:输入(生产者)、三部电梯(消费者、生产者)
  • 共享对象:总请求队列
  • 退出模式:两个队列,一个队列为总请求队列、一个队列为换乘标记队列;总请求队列仅有null且换乘标记队列为空时总调度类end标记为1

第三次作业中电梯增加了限乘人数,限到楼层,三部电梯间换乘。在这个需求下,三部电梯在消费者角色之外,如有使用该电梯的乘客需要换乘,那么这部电梯又承担了生产者角色,将乘客放回总请求队列。

三部电梯的捎带问题仍在调度类getRequest方法中实现,本次作业中向getRequest方法传入参数更复杂,从总请求队列中向电梯使用者加人受电梯承载力的限制。遍历总请求队列时,优先选择这部电梯能直接送到最终目的地的请求,如果需要,应从换乘队列中删掉这个人;如果电梯尚可载人,再将需要换乘的请求加入电梯使用者队列,并将换乘请求加入换乘者队列。

电梯完成任务方式类似于作业2,需要注意的细节是保证每一层电梯要先下后上,避免超载。

线程结束的方式中,调度类End新增条件,换乘队列为空,避免人员尚未送达,电梯结束运行。

二、程序结构分析

(一)第一次作业

1、规模分析

Elevator类:

75行代码

属性:

private OpList elOp;   //共享请求队列
private int floor = 1;  //当前楼层
private static final int door = 250;//开关门时间
private static final int go = 500;//移动时间

方法:

 public Elevator(OpList op) {}
 public void Op(PersonRequest pr) {}//电梯完成请求,20行
 private void printOpen(int floor) {}//封装开门输出
 private void printClose(int floor) {}//封装关门输出
 private void printIn(int floor,int id) {}//封装进人输出
 private void printOut(int floor,int id) {}//封装出人输出
 public void run() {}

OPLIST类:

40行代码

属性:

private ArrayList<PersonRequest> wait;//请求队列
private static int limit = 1;

方法:

 public OpList()
 public synchronized PersonRequest getRequest() //取请求,10行
 public synchronized void setRequest(PersonRequest newReq)//增加请求,10行

PRODUCER类:

30行代码

属性:

private OpList rawInput;//共享请求队列
private ElevatorInput elevatorInput = new ElevatorInput(System.in);

方法:

 public Producer(OpList raw) 
 @Override
 public void run() //往请求队列中加入请求

2、OO性能度量

第一次作业是生产消费模型的基础实现,因此代码复杂度低,耦合度低

3、类图

十分基础的架构,Elevator类是消费者,Producer类是生产者,共享对象OPList类。Main类中调用两个线程。

4、UML时序图

5、设计检查

  • 单一责任原则(SRP):输入线程仅负责向共享对象中添加需求、电梯线程仅对使用者队列中的需求依次满足。满足SRP
  • 开放性原则(OCP):第一次作业中不存在电梯调度策略问题,不存在电梯运行限制问题。我的设计通过对Elevator类的operate方法进行改进,并对addRequest类传入电梯运行参数,可以实现调度捎带功能。可扩展性良好。
  • 里氏替换原则(LSP):本次作业中,不存在继承关系。
  • 接口分离原则(ISP):本次作业中没有使用接口

(二)第二次作业

1、规模分析

 Elevator类

230行

属性:

 private ArrayList<Person> users;//使用者队列
 private int floor = 1;//电梯所在楼层
 private Request eleOp;//电梯当前主请求
 private RequestList rl;//共享对象,请求队列
 private static final int move = 400;//电梯移动时间
 private static final int door = 200;//电梯开关门时间

方法:

 public Elevator(RequestList re) {}
 private void printOpen(int floor){}
 private void printClose(int floor) {}
 private void printArrive(int floor) {}
 private int out(int floor,int mark1) {}//在特定楼层情况,25行
 private int operateDown(int from,int to,int count) {}//电梯下行过程处理,30行
 private int operateUp(int from,int to,int count) {}//电梯上行过程处理,30行
 private void operate(Person per,int from,int to,int choice,int count) {}//电梯完成主请求,57行
 public void run() {}//60行

Input类

40行

属性

 private RequestList rawInput;// 共享对象
 private ElevatorInput elevatorInput = new ElevatorInput(System.in);

方法:

 public Input(RequestList req) {}
 @Override
 public void run() {}

RequestList类:

114行

属性

private ArrayList<Person> wait;// 请求队列
private int end = 0;//结束标记为
private int in = 0;//区别第一次取人标记

方法

 public RequestList() {}
 private void printOpen(int floor) {}
 public synchronized int getRequest(ArrayList<Person> ele,int floor,int des,int marker) {}//根据电梯状态从请求队列中取出请求,60行
 private void printIn(Person per,int floor) {}
 public synchronized void setRequest(Person newReq) {}//从输入端向请求队列中增加请求,5行
 public boolean empty() {}//判断当前请求队列是否为空或仅含有null
 public int getEnd() {}//判断输入线程是否结束输入(传入null)

Request类:封装请求

45行

属性

 private int from;//请求中出发楼层
 private int to;//请求中到达楼层
 private String status = new String("");//请求中方向

方法

    public Request(int from,int to) {}//得到请求方向信息
    public boolean carry(Person per) {}//判断是否人的请求同向可携带
    public int getFrom() {}
    public int getTo() {}
    public String getStatus() {}
    public void reset(int newFrom,int newTo) {}//请求重置

Person类:

25行

对PersonRequest进行封装

在PersonRequest基础上增加属性,方便捎带与调度

private boolean state;//是否进入电梯
private boolean out;//是否出电梯

2、oo性能度量

Elevator类中三个operate方法耦合度高,原因是三个方法实际是电梯完成任务上行、下行、送到目标楼层全过程的分割,operateUp与operateDown都是operate中的一部分,而run方法中operate又是重要的部分。operateUp与operateDown两个方法设计不够巧妙,内容大多为对称映射关系。run方法复杂度高,耦合度高,主要因为run方法中为了实现优化,在实现取request,完成request的基础操作外,用循环遍历电梯使用者队列,调用operate方法,尽可能多的将同向运行的乘客送出,这就导致我的run方法写的庞大不美观。

RequestList类的getRequest方法也是复杂度高,耦合度高的方法。耦合度高是因为电梯的调度在我的设计中是以这个方法为基础,通过电梯类中每一层都调用这个getRequest方法,通过返回参数,判断该层是否可以捎带进人,因此在elevator类中重复调用这个方法。其复杂度高的原因在于,在这个方法中,嵌套了4层if else判断,并在最外层if else的每一种情况下都对请求队列进行遍历。

3、类图

 Person类封装了PersonRequest,Request类对人的请求与电梯运行任务做了统一。Elevator类是消费者,与Input类共享RequestList对象。RequestList类中的请求队列以及Elevator类中的使用者队列都是以Person为单位的ArrayList,Elevator类与PersonRequest类中,对于电梯当前运行情况以及等待着请求用Request类封装,判断是否可以捎带。

4、UML时序图

5、设计检查

  • 单一责任原则(SRP):PersonRequest类负责在具体楼层,针对电梯某一状态,点对点为电梯使用者队列增加人。电梯类的run方法中,同时实现取请求,完成主请求,顺路送人、捎带判断等任务,较为负责,安全性差。
  • 开放性原则(OCP):这次电梯的设计可扩展性较好。对于单个电梯的执行主请求,处理顺路送人,捎带的设计,扩展到有运行条件限制的电梯上也可适用。getRequest方法,只需要增加传入参数,也可以满足有运行条件限制的要求。
  • 里氏替换原则(LSP):本次作业中,Person类封装PersonRequest,变相继承Personrequest,子类可以替代父类。满足LSP
  • 接口分离原则(ISP):本次作业中没有使用接口

(三)第三次作业

1、规模分析

Elevator类

384行

属性

private static final int door = 200;//关门时间
private final int capacity;//承载力
private final  int move;//移动时间
private final String name;//电梯名称
private final ArrayList<Integer> floors = new ArrayList<Integer>();//可达楼层
private ArrayList<Person> users = new ArrayList<Person>();//使用者
private final ArrayList<Integer> setFloors = new ArrayList<Integer>();//与其他电梯交叉楼层
private int floor = 1;//当前所在楼层
private Request eleRep;//电梯当前主要请求
private Channel ch;//共享对象
private int first;//标志接过第一个人

方法:

goUP,goDOWN,operate,run仅在第二次作业基础上稍加改动

 public Elevator(String str,Channel channel) {}
 public int scanFloor(int floor) {}//判断某楼层是否在可达楼层中
 public void addFirst() {}
 public boolean isFull() {}
 public int getNearest(int oto,int afrom) {}//换乘目标楼层,20行
 public int getFirst() {}
 public String Name() {}
 private void printOpen(int flo) {}
 private void printClose(int flo) {}
 private void printArrive(int flo) {  }
 private int out(int mark1) {}
 private boolean findFloor(int floo) {}
 public int goUp(int to) {}
 public int goDown(int to) {}
 private void operate(Person per,int des,int choice) {}
 private boolean isIn(Person person) {}
 public void run() {}

Channel类:

170行

属性:

    private ArrayList<Person> wait;  //请求队列
    private ArrayList<Elevator> eles;  //电梯线程池
    private ArrayList<Integer> ag;    //需要换乘者标记队列
    private boolean end = false;  
    private boolean end1 = false;
    private int carry = 0;

方法:

public Channel() {}
private int fin(int id) {} //遍历换乘者队列
public synchronized void addRequest(Person per) {}//从输入端增加请求
public boolean isEnd() {}  //判断请求队列为空且输入结束且换乘队列为空
private void printOpen(int floor, String str) {}
private void printIn(Person per, int floor, String str) {}
public synchronized int getRequest(Elevator ele, ArrayList<Person> users,int floor, int des, int marker, int ca) {}  //向电梯使用者队列投放请求 ,90行
public boolean getEnd() {}
public void startEle() {}

Person类

属性:

private final int id;
private boolean stateIn; //电梯内判断位
private boolean stateOut; //出电梯判断位
private final int ofrom; //原始请求出发楼层
private final int oto; //原始请求到达楼层
private int afrom;  //实际出发楼层 
private int ato; //实际到达楼层
private ArrayList<String> inList; //搭乘过电梯标记队列

方法:

public Person(PersonRequest pr) {}
public boolean getInS() {}
public boolean getOutS() {}
public void setStateIn(boolean bo) {}
public void setStateOut(boolean bo) {}
public boolean canIn(Elevator ele) {}  //判断是否可仅当前电梯
public void setTo(int to) {}
public void setaFrom(int from) {}
public int getaFrom() {}
public void setOut(Elevator ele) {}  //根据当前所进电梯判断实际到达楼层为多少
public boolean oCanOut(Elevator ele) {} //判断当前电梯是否可以直接送到原始目标楼层
public int gi() {}
public int getaTo() {}
public int getoTo() {}
public void addIn(String str) {}

Input类

同第二次作业

Request类

同第二次作业

2、oo性能度量

getRequest方法复杂度高,嵌套4层if else判断,且每一个条件分支下都有遍历。对于请求队列与换乘者队列,存在遍历后删除某些项,重建队列的操作。耦合度高,因为该方法在Elevator类的operate方法中被调用频率过高。

Elevator类的goUP,goDOWN,operate方法耦合度高的原因同第二次作业。run方法的复杂度,耦合度圈复杂度高,原因也同第二次作业。

Person类CanIn复杂度高因为其内部有4层if嵌套,且最后一次嵌套中存在对使用过电梯队列的遍历。

3、类图

 Channel线程中的线程池中创建三个Elevator类,Elevator类与Input类共享Channel中的waitList对象,三个Elevator类共享一个Channel总调度器。

Person类与Request类是不可再拆分的原子类。

4、UML时序图

5、设计检查

  • 单一责任原则(SRP):Elevator类存在安全隐患,run方法的责任过多,不仅需要从电梯使用者队列中取出请求,还需要满足与前往请求发出者的方向一致的电梯使用者的送达需求。这个设计使run内部users的增加,删除操作混乱,导致bug出现。
  • 开放性原则(OCP):代码可扩展性一般,因为最核心的addRequest方法已经由于依赖当前电梯信息过多,受到了很大限制。
  • 里氏替换原则(LSP):本次作业中,Person类封装PersonRequest,变相继承Personrequest,子类可以替代父类。满足LSP
  • 接口分离原则(ISP):本次作业中没有使用接口

三、程序bug分析

第一次作业中,公测与互测未发现bug,但是在自己写代码的时候,由于不熟悉锁机制,对OPList方法加物理锁后,未及时notifyAll,出现了死锁。

第二次作业中,刚开始没有处理好Elevator线程终止判断方案,导致程序终止不了,最终RTLE报错。最后发现bug点在于我的Elevator判断终止条件为usersList仅有null一个元素,但是PersonRequestList在一些情况下未将null传入Elevator类。这个问题不算是线程安全问题,主要是设计不够完善。最终,我转变思路,对共享对象类设计End标记位,向Elevator类传递结束信号,保证程序正常结束。

第三次作业中,我仅对addRequest与getRequest两个共享对象中的方法上物理锁,其余对象,均为单一线程享有。这个设计保障了线程安全性。但是单一Elevator线程内部,我对于电梯使用者队列的“取与删除”的安全性设计存在缺陷。导致我在本地测试中出现了“乘客电梯中失踪“,乘客还没有输入“IN”,就被从电梯使用者队列中删去的问题。前者我在本地测试中勉强补救,后者在本地测试中未发现,导致强测中丢失了两个点。

总的来说,我的设计尽量避免了“共享”,上锁局限在对方法上锁,一定程度上保证了线程安全性。然而,抛开线程安全性,我的设计中,在电梯类调度优化时,考虑并不全面,对于电梯的使用者队列的维护不够完善,导致最终电梯使用者队列的增加与删除出现了冲突,出现了bug。

四、发现别人的bug策略

在第一次作业互测阶段,我阅读别人的代码,发现大家实现的思路框架基本一致,应该不存在设计上的bug。

第二次作业互测阶段,由于未搭建评测机,我只有尝试输入自己本地测试的数据,对同屋的人代码随机测试,无奈没有发现bug。

第三次作业互测阶段,已经搭建好评测机,我通过data.py生成随机数据,经过TestClass文件解析时间戳,将输入流定时输入到同屋小伙伴的代码中,并将输出结构递交给check.py。check.py对得到的输出数据按照指导书中的正确性要求进行检查,如果满足全部正确性要求,则循环回到data .py生成随机数据这一步;否则,循环停止,cmd界面显示报错信息。循环生成随机数据测试法亲测有效。

与第一单元互测阶段发现bug相比,第二单元发现别人程序bug难度更大。想要有效发现别人程序的bug,一定要建立起一套完整的,成体系的找bug工程。

首先,第二单元的定时输入是我们无法通过idea输入端手动完成的,我们必须通过.sh文件以及时间戳解析文件,将带时间戳的输入信息保存到文本文档,输入其他人的程序中进行测试。

其次,第一单元的前两次作业,把目标放在检查别人对于输入是否合法的判断是否全面,死抓WRONG FORMAT错误就可以狼到一些人,但是第二单元,输入流是官配的,狼人重点放在了对于合法的输入,其他人的代码输出的正确性问题。

第三,第二单元中,输出正确性判断难度较大。第一单元中,通过MATLAB,计算求导结果尚可以判断输出结果是否正确,到了第二单元,尤其是是第三次作业,三部信息交叉输出,输出正确性判定条件较多,尤其是在输出数据较多时,肉眼几乎无法判断输出是否正确。因此,第二单元判断别的输出正确性一定需要正确性解析代码。

第四,第二单元中,由于多线程输出的不确定性,一些本地输出错误结果的测试样例,提交到评测平台时,得到了正确的输出结果。这种情况在第一单元输出结果确定的情况下是万不会发生的。多线程不安全设计造成的bug的不可复现性要求我们提高测试数据的投入量。

 

五、心得体会

1、线程安全

这三次作业中,为了保障线程安全性,我尽量做到减少共享,防止冲突的发生。对于共享对象的上锁,我局限于共享对象类的方法上锁,保障一个线程调用该方法时,一个共享对象实例全部被锁住。我的实现方法虽说提高了线程安全性,但是灵活性不够,这暴露了我对于Java的线程安全以及锁机制的理解不够透彻。

经过研讨课同学的分享以及互测中阅读其他同学代码,我认为在后续多线程学习中我应该主动尝试对共享类的某个属性上锁,学习并使用可重入锁,使用atomic包的原子性工具。用更丰富,更灵活的手段保障线程安全。

2、设计原则

相比于第一单元每一次作业都重构的状况,本单元我格外注意代码的可扩展性,第一次作业搭建好生产者消费者架构,为后两次作业提供基础,第二次作业的电梯调度策略可直接迁移到第三次作业中单个电梯线程的调度设计中。可以说,三次作业的设计思路是一脉相承的。

本单元我在单一责任原则上做得很不完善。第二次作业开始,电梯调度策略的实现全部装入run方法,使run方法冗长难看不说,还导致了电梯调度过程中,使用者队列的安全性得不到维护,最终潜藏的隐患遗留到了第三次作业中集中爆发,强测挂点。这个问题的根源是我没有对电梯调度问题整理清思路,将“调度”问题剥离出几个“子过程”,分割实现,而是一股脑丢进run方法,套在多层循环中实现。在第三次作业中,本来想在Elevator类之外设计operate方法,在Elevator中直接调用,让代码更加美观,可惜由于我设计电梯处理请求以及调度问题的思路不够清晰,没有实现operate方法从电梯类中分离。

 

posted @ 2019-04-21 15:28  Jessyswing  阅读(358)  评论(0编辑  收藏  举报