BUAA_OO_第二单元作业总结
第二单元作业总结
BUAA OO 2021面向对象作业
19373469 陈纪源
一、同步块和锁的选择
(一)第一次作业
在本次作业中,由于是刚刚了解的多线程的内容,因此参考了实验课的框架。使用syncronized
语句对于共享数据waitQueue(等待队列)和processQueue(局部队列)所包含的代码段进行互斥共享。下面分别介绍各个线程的同步控制块。
输入线程:
while (true) {
PersonRequest request = elevatorInput.nextPersonRequest();
if (request == null) {
synchronized (waitQueue) {
//close
}
break;
} else {
synchronized (waitQueue) {
//add person
}
}
}
调度器线程:
while (true) {
synchronized (waitQueue) {
if (/*judge something*/) {
for (int i = 1; i <= elevatorNum; ++i) {
synchronized (elevatorQueues[i]) {
//elevatorQueues[i].dosomething
}
}
}
/*do something else*/
}
}
电梯线程:考虑到实验课架构是有死锁的可能性,我在这里修改了一下,没有再抓取waitQueue的锁
while (true) {
synchronized (elevatorQueue) {
try {
elevatorQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (/*judge something*/) {
//work
}
move();
elevatorQueue.notifyAll();
}
}
(二)第二次作业
第二次作业虽然加入了多部电梯,但是由于我在上一次作业中写好了调度器,因此在架构上没有太大区别(依然使用了同步控制块)。唯一有区别的是我发现我上一次的架构,电梯和调度器实际上是串行的。
原因在于,我的电梯线程每动一层,都会wait()
等待调度器线程通过notifyAll()
唤醒电梯线程,同时move函数中电梯线程并没有释放出elevatorQueue的锁,因此电梯只能白白等待。
因此我对电梯线程的锁进行了修改:
while (true) {
synchronized (elevatorQueue) {
if (/*judge something*/) {
//work
}
elevatorQueue.notifyAll();
}
move();
}
这样调度器和所有电梯线程就可以做到高并发了。
同时这次由于是有多电梯,因此输出需要加锁,我写了一个Output
类来处理输出,当需要输出时会调用其中的互斥方法printString()
:
public class Output {
synchronized void printString(String str) {
TimableOutput.println(str);
}
}
(三)第三次作业
第三次作业中由于需要换乘,因此增加了一个新的共享数据类OutPerson
,表示从电梯出来需要换乘的人的集合,因此调度器线程需要进行修改。
while (true) {
synchronized (waitQueue) {
if (first) {
for (int i = 1; i <= 5; ++i) {
synchronized (elevatorQueues[i]) {
//elevatorQueues[i].dosomething()
}
}
}
else if (waiting) {
if (exchange.size() > 0) {
synchronized (outQueue) {
try {
outQueue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//deal exchange
}
}
} else (/*judge something*/) {
/*do something*/
}
}
else (/*judge something*/) {
/*do something*/
}
}
}
二、调度器设计
(一)第一次作业
本次作业中调度器只起到了传输的作用,把数据请求从waitQueue传输到processQueue,并没有做真正的调度工作。
而且由于自己理解不够深刻,导致调度器和电梯的交互变成了串行模式,性能很低。
(二)第二次作业
这次电梯调度中调度器发挥了分配的作用,分下面两种情况讨论:
-
新加一个电梯:将所有processQueue中的人全部取出放回waitQueue,再根据当前电梯情况进行重新分配(跳转到2)。
-
新加一堆人:跑模拟电梯,贪心求出将人放入哪个电梯所有电梯的最大结束时间会比较早(如果有多个则将其加入等待人数比较少的电梯中)。
调度器根据以上策略进行请求分配。
调度器和输入线程(Main函数)的交互主要是获得输入请求。
调度器和电梯线程的交互是每次调度会去获取电梯的状态,分析得到新的请求的分配策略。
(三)第三次作业
这次调度器在上一次的基础上,加入了换乘的功能。
类似于第二次作业,对于每个请求的人会获得一个放入电梯的编号。
当电梯是A电梯时,且该电梯的等待人数\(A_w\),B类电梯最大的等待人数\(max B_w\),C类电梯最大的等待人数\(max C_w\)满足:
就认为是不平衡的,此时强制电梯换乘,换乘表是一个静态策略表。
这次调度器和电梯的交互在上一次的基础上,还增加了换乘人员的交互,相当于从电梯线程出来的人,如果它需要继续换乘,那就需要通过outQueue传回waitQueue当做一个新的请求。
三、第三次作业架构分析
1. UML类图
2. UML协作图
该图只画出了最开始前三个电梯对应的UML协作图(后面的电梯加入是类似的)。
3. 功能和性能分析
分析本次电梯的功能:从输入线程获得请求,将请求交给等待队列中。调度器根据等待队列,将请求交给模拟电梯,返回建议放置的位置。当调度器发现电梯内部不够平衡时,强制人员换乘,从List中获得换乘(静态)表,将请求分给对应的电梯等待队列,否则就直接将人的请求给对应的电梯等待队列。电梯线程获取电梯等待队列,使用look算法进行上下移动,当发现一名用户属于换乘用户时,需要加入OutPerson中的队列。OutPerson中的序列会将人重新加入等待队列进行重新分配。
分析本次电梯的性能:模拟电梯在第二单元的作业中取得了一定的效果,但是在没有换乘的情况下,有可能会使所有的乘客都去A电梯(因为对于B/C电梯,有些乘客请求是无法到的),因此采用了之前所说的动态强制换乘操作。但是由于时间仓促,测试并没有太及时,这个换乘的参数选择其实并没有得到特别好的效果。
平衡性分析:为了追求换乘的效果,我在之前第二次作业中的框架下修改了大量东西,增加了大量的共享数据(还是在waitQueue和ElevatorQueue中增加的),这导致我后来调试中有大量的问题。而且这种换乘策略我觉得自己也没有想太清楚,甚至都没有做太多和不换乘比较的策略。因此在功能和性能的平衡性把握中,我还需要再反思反思。
4. 架构可扩展性分析
对于我的架构来说,扩展性非常一般。对于我的电梯线程,并没有使用状态模式建模,还是基于很多的if判断语句(非常面向过程!)。个人认为电梯线程还是应该分解为状态转化类和操作类,状态转化类负责修改电梯的状态(对于不同的模式就可以仅仅修改这个部分),操作类负责电梯的开门、关门、上下移动。调度器线程理论上来说扩展性还可以,但是其实调度器线程其实最好将调度和策略分成两个不同的类,分别管理自己的事情,这样如果有新的调度策略时可以仅仅修改策略类。
5. 反思与总结
-
将策略和动作分成不同的两个类,而不是柔和在一起
-
应该考虑将Person类重新封装,以更好的进行换乘
-
性能优化应该建立在稳定的架构上的,而不是舍本逐末
四、分析自己程序的bug
(一)第一次作业
在第一次作业中,所有公测数据和互测数据都已通过。
(二)第一次作业
在第二次作业中,所有公测数据和互测数据都已通过。
(三)第一次作业
在第三次作业中,最后一个公测数据点出现了bug。
经过分析发现是自己在给新电梯分配可到达楼层数组时错误赋值了。
出现这个bug的一个关键性原因是,调度器类的功能实在是太多了,行数有341行。
这么多的行数情况下非常容易写出这样的bug。
线程安全问题做了很多测试,没有问题。
这提醒我一定要去做类功能的拆分,既方面自己理解,又可以更好的进行代码调试。
五、分析其他同学程序的bug
(一)第一次作业
第一次作业当时就采用了一些随机数据(30条指令)对一部分同学的代码进行测试,结果没有测出问题,就没有在上面提交数据。
(二)第二次作业
第二次作业依然使用了随机数据(50条指令)对一部分同学的代码进行了测试,发现其中一名同学的输出没有进行同步互斥,会出现输出逆流的问题(时间戳并没有按顺序),将数据提交到评测机后发现并没有复现bug(本地基本上每次都有问题)。
(三)第三次作业
第三次作业还是使用了随机数据(50条指令),这次由于一些原因是和我的两位队友一块测试的,对它们的代码进行了测试发现并没有问题,也尝试在上面提交了一次数据,发现没有bug。
六、心得体会与反思
6.1 线程安全
- 通过第二次作业的磨练,对多线程的内容有了更加深刻的理解。
- 认识到了多线程的各种架构(任务分配、观察者模式等),认识到各种架构的优势和劣势。
- 认识到锁和同步控制块的重要性,控制块的大家既不可以过大也不可以过小。
- 在测试多线程线程安全时需要在高并发的环境中模拟进行,且不可以仅仅单一进程测试,这样效果不好。
- 认识到线程安全类的重要性。
- 认识到线程安全问题的调试方法(断点调试和printf输出中间结果),还可以对功能进行拆分测试。
- 还需要思考如何进行科学的多线程测试,主要是覆盖所有的分支和情况。
6.2 层次化设计
- 架构设计非常重要!!!这次的架构问题就是在设计时没有进行策略和行为分开,导致可扩展性差。
- 学会平衡功能性和性能,性能的提高应该是在一个好的功能性基础上进行的。
- 认识到状态类建模和策略类建模的方式。
6.3 其他
- 继续提高自己工程开发的能力,也进一步锻炼了自己的代码能力。
- 对于自动评测机,还需要了解如何扩展和改进。
- 更需要和同学们、老师们交流架构模型,从其他同学那里学习到新的思路。
- 对于更多架构和模式,还需要进行更深入的学习。