同步块的设置和锁的选择
在三次的作业中,我都是选择共享变量waitQueue作为锁。对于需要读取或写waitQueue的语句块,我们需要在其外面加锁。以下以第一次作业为例进行分析。
例1:
synchronized (waitQueue) {
if (end && passenger.isEmpty() && waitQueue.isEmpty()) {
break;
} else if (passenger.isEmpty() && waitQueue.isEmpty()) {
try {
waitQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在上面的代码中,由于需要判断waitQueue是否为空,所以需要加锁避免出错。
例2:
PersonRequest request = elevatorInput.nextPersonRequest();
if (request == null) {
break;
} else {
synchronized (waitQueue) {
waitQueue.add(request);
waitQueue.notifyAll();
}
}
在上面的代码中,由于需要向waitQueue中添加请求所以需要加锁,且由于例1waitQueue在为空时wait,所以在此还需要对waitQueue进行notifyAll以唤醒锁。
调度器设计
第一次作业只有一个电梯,故未设计调度器。第二次与第三次作业的调度器结构类似,故以第三次作业为例。
由于程序体量较小,故调度器即为数据输入线程,由数据输入线程对电梯线程进行调度。电梯调度时使用共有的等待队列waitQueue,由电梯线程进行竞争性运行。
交互过程:调度器一开始调用初始的三部电梯,之后在处理请求时,若为乘客请求,则将其加至waitQueue中,若为电梯请求,则新实例化一个电梯线程并启动。
第三次作业架构设计的可拓展性分析
在第三次作业中,依旧使用和前两次类似的电梯运行策略(竞争性运行),功能与性能取得了较好的平衡。
基于度量分析程序设计结构
Complexity Metrics
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.run() | 6.0 | 19.0 | 21.0 |
Strategy.moveTo(ArrayList,ArrayList,String,String) | 2.0 | 11.0 | 15.0 |
Elevator.getIn(Ask) | 6.0 | 15.0 | 17.0 |
Elevator.move(Ask,int) | 7.0 | 7.0 | 9.0 |
Strategy.hasAskWait(ArrayList,String) | 5.0 | 8.0 | 13.0 |
WaitQueue.run() | 3.0 | 9.0 | 10.0 |
Strategy.fit(Ask,String) | 4.0 | 7.0 | 12.0 |
Elevator.getOff(ArrayList) | 1.0 | 10.0 | 11.0 |
Elevator.down() | 1.0 | 5.0 | 5.0 |
Elevator.floorOutAndIn(Ask) | 1.0 | 6.0 | 6.0 |
Elevator.outAndIn(Ask) | 1.0 | 5.0 | 6.0 |
Elevator.up() | 1.0 | 5.0 | 5.0 |
DebugInput.run() | 1.0 | 2.0 | 4.0 |
Elevator.Elevator(WaitQueue,ArrayList,ArrayList,int,String) | 1.0 | 2.0 | 3.0 |
WaitQueue.isEmptyPassenger() | 3.0 | 2.0 | 3.0 |
Elevator.close() | 1.0 | 2.0 | 2.0 |
Elevator.open() | 1.0 | 2.0 | 2.0 |
Ask.Ask(int,int,int) | 1.0 | 1.0 | 1.0 |
Ask.getFromFloor() | 1.0 | 1.0 | 1.0 |
Ask.getId() | 1.0 | 1.0 | 1.0 |
Ask.getToFloor() | 1.0 | 1.0 | 1.0 |
Ask.setFromFloor(int) | 1.0 | 1.0 | 1.0 |
DebugInput.DebugInput(OutputStream) | 1.0 | 1.0 | 1.0 |
Elevator.getPassenger() | 1.0 | 1.0 | 1.0 |
Elevator.setEnd(boolean) | 1.0 | 1.0 | 1.0 |
Elevator.setPattern(String) | 1.0 | 1.0 | 1.0 |
Main.main(String[]) | 1.0 | 1.0 | 1.0 |
WaitQueue.WaitQueue(ArrayList) | 1.0 | 1.0 | 1.0 |
class | OCavg | OCmax | WMC |
---|---|---|---|
Strategy | 9.33 | 13.0 | 28.0 |
Elevator | 4.07 | 11.0 | 57.0 |
WaitQueue | 4.0 | 8.0 | 12.0 |
DebugInput | 1.25 | 1.0 | 5.0 |
Ask | 1.0 | 1.0 | 5.0 |
Main | 1.0 | 1.0 | 1.0 |
分析上面数据,可知复杂度较大的方法为Elevator.run()、Elevator.getIn(PersonRequest)、Strategy.moveTo(ArrayList,ArrayList,String)。run()方法时由于需要调用大量elevator的其他方法,getIn()和moveTo()是因为判断嵌套多且条件复杂。
Dependency
Class | Cyclic | Dcy | Dcy* | Dpt | Dpt* |
---|---|---|---|---|---|
Ask | 0.0 | 0.0 | 0.0 | 4.0 | 4.0 |
DebugInput | 0.0 | 0.0 | 0.0 | 1.0 | 3.0 |
Elevator | 1.0 | 3.0 | 4.0 | 1.0 | 2.0 |
Main | 0.0 | 2.0 | 5.0 | 0.0 | 0.0 |
Strategy | 0.0 | 1.0 | 1.0 | 1.0 | 3.0 |
WaitQueue | 1.0 | 3.0 | 4.0 | 2.0 | 2.0 |
第三次作业出现了相互依赖的情况,为Elevator类与WaitQueue类。
可拓展性
SRP--单一职责原则
- 内容:就一个类而言,应该仅有一个引起它变化的原因。
- 分析:主要的类是WaitQueue,Elevator,Strategy。其中,WaitQueue类负责读取输入请求和调度,Elevator负责电梯的运行,Strategy负责电梯的运行策略。总体上看,每个类的职责分明,相互之间没有重合的职责。
OCP--开放-封闭原则
- 内容:软件实体(类,模块,函数等)应该可以扩展的,但不可修改。
- 分析:我的代码中,主要的类中的主要方法可以通过继承来实现扩展。比如修改新的电梯运行策略,或者增加新的电梯种类、行为等。在两次迭代作业中,对代码的修改并不大。
LSP--替换原则
- 内容:子类型必须能够替换它们的基类型。
- 分析:三次作业中均未用到继承。三种类型的电梯,也只是通过构造方法的参数来实现。
DIP--依赖倒置原则
- 内容:抽象不应该依赖于细节,细节应该依赖于抽象。
- 分析:电梯类和输入类需要相互依赖。
ISP--接口隔离原则
- 内容:类进行必要的交流,没有多余的接口。
- 分析:代码中未使用多余接口。
类图
时序图
分析自己程序的bug
第一次和第二次作业都没有出现bug,第三次作业出现了bug。
第三次作业强测中有两个相同类型的点没过,是功能性错误。
-
错误样例:
-
[1.0]Random [3.1]82-FROM-11-TO-20 [4.0]308-FROM-8-TO-14 …… …… [35.7]16-FROM-10-TO-9 [36.0]254-FROM-7-TO-18 [42.4]123-FROM-9-TO-2 [42.4]52-FROM-15-TO-12 [43.1]94-FROM-12-TO-3 ……
-
错误信息:Passenger 123 cannot enter the elevator twice at floor 3
-
错误原因:问题出现在Elevation类中换乘乘客的输出语句身上。对于乘客123(123-FROM-9-TO-2)来说,在运行时乘客123被B类型电梯运载,运送到3楼,再由A类型电梯运送至2楼。错误代码中是先将下电梯且换乘的乘客加入到等待队列waitQueue中的,再和进入电梯的乘客信息一起输出。这会导致本bug的产生:换乘乘客下电梯后立马上了另外的电梯,导致进入另外电梯的语句比下了该电梯的语句先输出,产生了乘客123还没下电梯就进入了另外一个电梯的表象。
-
bug修复:对于下电梯且要换乘的乘客而言,不在下电梯的时候立马加入waitQueue中,而是等到其下电梯的语句输出后再加入waitQueue中。
如何有效发现别人的bug
在互测环节,由于时间关系和多线程的不可复现性,并没有发现别人的bug。寻找bug的策略如下。
- 构造功能测试集:对于每一个可能的存在的问题,都进行测试。
- 构造极限测试集:构造极端数据进行测试。
- 压力测试:在符合条件的情况下请求尽可能多且复杂。
心得体会
通过本单元的三次作业,我对Java多线程编程有了一个初步的认识,并了解了一些简单的互斥访问与同步控制的方法。
在多线程编程中,通过线程安全的共享对象来完成线程间交互是十分清晰而简洁的方式。通过对象锁,可以使该对象在同一同步块内只能被一个线程访问,且不会被打断。再结合wait和notifyAll方法,可以避免轮询,高效利用CPU资源。
在设计中遵循SOLID原则及一些其他重要的设计原则也是十分重要的,这些原则保证了程序结构的清晰性和良好的可扩展性。在本单元作业中,部分设计原则未能体现甚至有所违背,在今后的编程中会多加注意。
期待下一单元的主题。