OOUnit2
OOunit2总结博客
(1)总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句之间的关系
作业中同步块都在共享对象中的方法,共享对象实现如下接口:
public interface Queue { void addRequest(Request request);//添加成员 void setEnd();//传递结束信号 boolean isEnd();//检查是否结束信号 boolean needWait();//利用该方法使得进程判断是否需要等待 boolean isEmpty(); }
一个具体实现类如下:同步块中方法以 synchronized
关键字修饰。
add
和 set
方法在更改类中属性时通过 notifyAll()
语句唤醒同一共享对象的等待线程。
import com.oocourse.elevator3.Request; import java.util.ArrayList; public class QueueReq implements Queue { private ArrayList<Request> requests; private boolean isEnd; public QueueReq() { requests = new ArrayList<>(); isEnd = false; } @Override public synchronized void addRequest(Request request) { requests.add(request); notifyAll(); } @Override public synchronized void setEnd() { isEnd = true; notifyAll(); } @Override public synchronized boolean isEnd() { return isEnd; } @Override public synchronized boolean needWait() { if (requests.isEmpty()) { try { //Output.println(Thread.currentThread().getName()+ wait); wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return requests.isEmpty(); } @Override public synchronized boolean isEmpty() { return requests.isEmpty(); } }
(2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
第二单元第一次作业
第一次作业只有楼座的上下电梯,且每个楼座只有一座。电梯调度器负责将对应楼座的乘客分配到对应电梯候乘表中即可。
第二次作业
第二次作业增加了同一楼座多部电梯和环形电梯,调度器负责将对应楼座的乘客分配到对应电梯候乘表的列表类中,由这个列表类的方法实现将乘客请求添加到电梯候乘表中。(这部分即是调度器的分配策略,为了方便选择了基准策略平均分配。即同一楼座或同一层的电梯候乘表列表中有N个电梯时,这一座的乘客平均分给这些电梯的候乘表)
由于第二次作业添加了增添电梯请求,调度器除了负责电梯线程的结束以外,还添加了增加电梯的方法,负责电梯线程寿命的开始。
第三次作业
第三次作业在第二次的基础上增加了换乘。故调度器在第二次作业功能基础上还实现了乘客请求的拆分和冻结。拆分指的是调度器维护一个楼座间的连通表,当两个楼座存在一个环形电梯可以横向直达时,我把它视作连通的。根据连通情况判断请求的换乘是走连通的环形电梯还是走一楼的全连通电梯。每当添加一个新电梯时会更新一次连通表,每当添加一个新乘客时也会查询一次连通表。当连通表维护好之后,乘客需要换乘的请求就可以进行拆分了。根据目标楼座、目标楼层、当前楼座、当前楼层、连通表这五个因素,把乘客请求拆分成两个或者三个或者一个横向直达的请求。然后把第一个请求当成第二次作业的方式直接处理,即放到候乘表列表里,不同的是,第二次作业的环形电梯是每一层有一个候乘表列表管理N个电梯,第三次作业则考虑连通情况,每一层有10个候乘表列表,代表A-B连通,A-C连通,……D-E连通,10层一共100个候乘表列表。
当新添加一个横向电梯时,举例:例如一个6层可以在ABE座开门的电梯,则它的候乘表需要添加到6层A-B连通、6层A-E连通、6层B-E连通三个候乘表列表中。如果某个乘客请求中间会走6层的A-E,该请求就会被分配6层A-E连通的候乘表列表中,就有可能会按分配策略分配到这个电梯中(依然采取平均分配策略。)
初始化该列表的部分代码如下:
private void init() { /*……*/ for (int i = 0; i < 10; i++) { // init floList // init connectedTable connectedTable.add(i, new ArrayList<>()); floList.add(i, new ArrayList<>()); for (int j = 0; j < 10; j++) { floList.get(i).add(j, new QueueList()); } // 初始横向电梯放入全连通一层。 j = 0 ,floor = 1 floList.get(i).get(0).addQueue(ele.getSubQueue()); } /*……*/ }
第三次作业中的调度设计还有一个重要的地方是拆分请求后要按顺序执行,故之后的后序请求都会加入冻结列表
private final QueueReqFreeze queueReqFreeze;
调度器的运行逻辑和冻结列表里是否有请求,是否需要唤醒密切相关。
queueReqFreeze
:冻结列表
queueMain
:和输入线程共享的列表,主要代表控制台输入
handleFreeze
:处理冻结列表中的请求。调度器和所有电梯共享一个对象messageTray
,当电梯完成一个请求,它会把乘客的id,当前走出电梯的楼层楼座输入到这个共享对象中,每当调度器要处理冻结列表中请求,判断是否要解冻的时候,就会在messageTray
中搜索是否和冻结请求的出发楼座和楼层还有乘客id一一对应。如果有则代表上一阶段的请求已经完成,需要解冻处理下一阶段请求了
handleRequest
:在第二次作业的基础上,除了有已经实现的分辨请求类型、创造电梯开启线程、或者把乘客请求放入对应候乘表列表中以外。还实现了创造电梯后更新连通表、考虑乘客请求是否需要拆分和冻结等。
public void run() { init(); while (true) { if (queueReqFreeze.isEmpty()) { if (queueMain.isEnd() && queueMain.isEmpty()) { // set All sub end for (int i = 0; i < 5; i++) { buiList.get(i).setEnd(); } for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { floList.get(i).get(j).setEnd(); } } return; } else if (queueMain.isEmpty()) { if (queueMain.needWait()) { continue; } } handleRequest(); } else { if (queueMain.isEnd() && queueMain.isEmpty()) { if (messageTray.needWait()) { continue; } handleFreeze(); } else if (queueMain.isEmpty()) { handleFreeze(); if (queueMain.needWait()) { continue; } } else { handleFreeze(); handleRequest(); } } } }
在第三次作业的调度器中,告诉电梯线程该结束的判断条件为,冷冻列表为空,输入线程不再输入且为空:
queueReqFreeze.isEmpty() && queueMain.isEnd() && queueMain.isEmpty()
而开启电梯线程同样在创造电梯线程的方法中实现:private void createEle(Request request) {}
调度器设计总结
总的来说三次作业调度器的设计是在前次作业基础上根据所需新功能不断迭代而需要实现的。从开始只需要处理乘客请求、控制电梯线程的终结;到第二次作业需要处理创造电梯的请求,以及开启电梯线程的生命;到第三次还需要拆分请求,维护连通表,冷冻请求等。根据需求进行设计。
(3)结合线程协同的架构模式(如流水线架构),分析和总结自己
三次作业架构设计的逐步变化和未来扩展能力画UML类图
-
第一次作业UML类图较为简单:架构主要有调度器、输入线程、电梯类,主线程较为简单主要负责顶层连通,这里不展示在类图中。
-
第二次作业添加了工厂模式和环形电梯类,调度器的属性略有改变
-
第三次作业架构在第二次作业基础上更改不大,主要添加了电梯类和调度器的共享对象(图中没有给出),调度器的属性更新了冻结列表(如图),添加了连通表类(为了降低耦合单独成类,可并入调度器,UML图中没有给出)
三次作业的架构是迭代设计的,如类图所示,在不满足新作业功能要求的情况下会引入新的中间类,通过扩展中间类或者接口,然后继承实现的方式进行扩展。
-
画UML协作图(sequence diagram)来展示线程之间的协作关系(别忘记主线程)
三次作业除了第三次电梯会给调度器回传
messageTray
信息以外,前两次都是一级一级自行判断线程结束并向下传递结束命令。(而且第三次作业电梯回传的messageTray
也只是方便调度器清空冻结请求列表freezeReqQueue
,当该冻结请求列表为空且其他结束条件满足后,调度器会结束自己进程,此后电梯线程写入共享对象的信息并无意义。因此,此三次作业的时序图皆可用一张图表示,即线程完成自身任务后就结束并向下传递结束命令,输入线程负责处理输入,调度器负责分配候乘表,电梯线程负责运送乘客,所有线程结束后主线程也结束)。
(4)分析自己程序的bug
公测第二单元第一次作业部分评测卡在waiting没有结果,bug修复直接提交后得到100分。
第二次、三次作业公测互测均没有bug。
分析是由于简单的设计带来更高的容错率,首先是同步块和锁的设计,同步块全部设计在共享对象的方法内,而且有意避免轮询的出现,在共享对象中加入 needwait
方法,在需要获取共享对象时判断是否需要等待,如果需要,该线程立刻释放共享对象锁和CPU资源。
其次是电梯运行逻辑,详细界定了输入线程、调度器、电梯的生命周期开始结束边界判定。避免出现线程结束乘客请求还未处理或者乘客还在电梯内未能送达目的地的情况。
架构设计每个线程对自己生命周期和向下传递结束信号的设计也减少了耦合,避免复杂情况导致的bug。
(5)分析自己发现别人程序bug所采用的策略
列出自己所采取的测试策略及有效性
用测试自己程序的样例测试别人程序。由于对互测参与度低故没什么有效性。
分析自己采用了什么策略来发现线程安全相关的问题
通过构造特殊的案例,比如短时间内出现大量请求。
分析本单元的测试策略与第一单元测试策略的差异之处
多线程协作无法构造单元测试验证一些模块的准确性。
(6) 心得体会
从线程安全和层次化设计两个方面来梳理自己在本单元三次作业中获得的心得体会
- 线程安全:越简单的设计容错率越高,同步块操作全部由共享对象完成,这样保证线程获得共享对象的锁的时候,其他线程调用共享对象的方法需要等待线程的锁的释放。
- 层次化设计:善用设计模式,例如电梯内部类的状态模式、调度器和电梯间的工厂模式。保持低耦合,这样在迭代下一次作业的时候只需要更改一些模块间的参数,以及增添一些类和接口来当中间件实现新的功能,例如新的共享对象,新的共享对象列表。迭代调度器功能时,电梯内部运行逻辑可以几乎不变,只需要保证调度器将正确的请求分配给电梯线程,电梯类要做的修改只是参数上的变化。另外,将需要抽象的东西包装成类或者接口再继承,这样原有已实现的功能类所修改的仅仅是一些继承关系等,新实现的功能类则可以参考原有类做一些属性和方法的修改来区分开来。