经过第一次作业的训练,我已经明白建立合适的类的重要性。这个单元作业以多线程为核心,用synchornized 块对共享的数据类进行上锁,用wait()和notifyAll()对数据进行释放。本单元的作业迭代非常明显,如果第一次作业结构较好的话第二三次作业会比较简单。
第一次作业
本次作业建立了Main主类,Input乘客读取线程类,Elevator电梯类,Lift电梯运行线程类,Floor单独楼层(乘客队列类),Moring、Night、Random不同策略方法类。 由Main开启Input和Lift线程,Input将读到的乘客信息存放在相应楼层的Floor中。Lift存放着Elevator类,调用三种策略的方法,与Input共享Floor类。 Lift中的run()方法public void run() { while (true) { synchronized (floors) { /*判断楼层剩余人数、电梯 剩余人数、Input是否结束。*/ } switch (arrivePattern) { //morning、night、random三类case } } }关键点是只要读或者写到Floor类就要加锁,比如:run中读取楼层剩余人数。避免数据冲突。 第一次作业一部电梯,没有调度问题或是其他什么问题,Morning策略为等待电梯满员或者Input结束;Night策略为直接跑到有等待队列的最高层,从上往下接人;Random在按照ALS策略的情况下也能运行的比较快。 第二次作业 第二次的类相比于第一次反而减少了。因为我发现在这一过程中比如开关门方法,电梯上下楼方法可以放在Lift里面去调用。
public void open();//开门 public void close();//关门 public static void gogogo(int target, Elevator elevator);//到目标楼层
第二次作业多了添加电梯,就需要在Input里面去生成新的Lift线程
我并没有使用调度器的策略采用的是*竞争,所以实际上处理起来会简单一点点,只需要注意共享类的锁的添加和释放,对Input、Main、Elevator类等进行简单调整即可,基本就在第一次的结构上没什么改动。
*竞争虽然简单,但是我在一开始忽略了一些细节,出现了电梯空跑(到达目标楼层时乘客已被其他电梯借接走)时不停开关门的现象,于是增加了对楼层中是否有等待队列的判定。
然后对于morning,night,random里面的方法也被放在了Lift里面去调用,所以这些类被删除了。
第三次作业
第三次作业我采用的思路和第二次差不多,也是没有使用调度器,所以类与第二次相同。修改的思路就是在第二次的基础上将电梯上下楼方法分类,电梯载人判断分类。
也是属于非常简单的在上一次结构上添加就好了。
但是第三次的BUG是因为CPU超时,由于我没有换乘策略,在乘客输入信息无法被B类型或者C类型电梯使用时,他们的线程会一直执行遍历楼层剩余人数的判断,占用了很多时间。于是我改进了剩余队列的查询算法,并当Input线程结束之时仍未有匹配乘客的电梯线程提前结束;同时还对C类型电梯特殊处理,在输入乘客未满足C类型要求时暂时先不启动C类型电梯线程,这样使得CPU时间大大减少。
在强测中有很多极端案例,比如一直从底层跑到高层,或者顺序非常杂乱,但可能是限制时间较宽,即使用*竞争也能相对顺利地送到,主要考虑的还是在不同状态下有没有Bug。
对于第三次作业如果再添加条件我的可扩展性就相对差了很多,因为方法之间的复杂度很高,毕竟是心里想着最后一次作业,写的时候就没有考虑下一步扩展。
体会
这三次作业让我了解要去建立好的类和方法,虽然三种电梯策略略有不同,但开关门、进出这些都可以在一个方法中去调用,相比于第一次放在三个不同的类里面要方便很多。但是我在实际的操作过程中将方法写得过多,实际上应该可以再为一些方法分出一个类来减少单个文件的代码长度。
多线程的调试是比较困难的,我采用的在特定位置添加println输出,来定位Bug源,当然还有轮询、死锁等问题其实用println的方式并不那么好定位。于是我向讨论区求助,知道了JProFiler工具,这个软件可以很好地查看线程的整个运行过程的状态,还有CPU时间,会列出占用时间较长的方法,在我处理CTLE时非常有帮助。
代码一行长度的问题,因为我们的类名,成员名都会比较长,在引用的时候很容易超出Checkstyle的规定,但是对一些多级的应用,比如引用一个类的Arraylist成员的中的某一个,虽然可以用Arraylist自带方法获取,但是如果自己写一个新的get方法可以短很多。还有就是避免写很长的布尔逻辑判断,很容易看花。
在每次作业的迭代上未采取调度器可能性能会差一点,但是每次的修改都比较简单,不需要大幅度重写。