BUAA_OO_Unit2 总结
电梯单元总结
锁与同步
锁的选择
-
第一次作业中,由于对锁与同步的陌生,我选择了最简单的实现方式:在线程中,只要遇到读写共享对象的情况,就将这段读写代码放在同步块中,比如
public class Elevator extends Thread { ... private void look() { synchronized (shareTable) { PersonRequest pout = findOut(); PersonRequest pin = findIn(); ... } } ... }这样做有一个很大的缺点:使用锁的代码难以统计,东一块西一块,如果出现了应该锁但没有锁的情况,难以被及时察觉。
-
第二次作业中,由于在研讨课中听取了同学的建议,我将原来在线程中使用的同步块转化为共享对象中的同步方法
public class ShareTable { ... public synchronized PersonRequest findIn(int name, int floor, int num) { char name1 = (char) (name + 65); ... } ... }这样一来,不仅线程中对共享对象的调用代码简洁了许多,也更容易分析哪些地方可能出现线程安全的问题。
-
第三次作业中,老师向我们 介绍了更为灵活的自定义锁与java内置的reentrantlock等锁与同步方法,但由于我的怠惰,并没有去尝试他们,而是延续了作业二中的同步方法,只是在此基础上修改或添加了一些方法。
对锁与同步的理解
对于java内置的synchronized来说,一个线程获得一个锁即获得这个锁对应对象的唯一访问权,其他任何尝试访问这个对象的线程如果不做特殊处理,都会进入阻塞态,直到这个锁被所有线程释放,抢夺到这个锁后,才恢复可执行态。而线程也可以在获取锁失败时使用object.wait()进入等待状态,直到被锁的拥有线程将锁释放后,调用obje.notify()方法,才重新被唤醒。
调度器的交互模式
调度器需要同时与输入线程与电梯进行交互:获取输入线程输入的请求,将请求分配到电梯的并决定在电梯抵达目的地后设置它的下一个目的地。交互的方式是通过多线程和共享对象实现异步读写。
架构
hw5
UML图

基本思路:InputClass和Dispatcher之间使用消费-生产者模式,Dispatcher和ELevator之间也使用消费-生产者模式,共享对象为空时消费者处理商品或等待,生产者生产商品,共享对象不为空时,消费者在共享对象不被锁住时获取商品。
细节:
- 调度策略:使用捎带策略,将电梯停止时(或完成上一个请求后)外部请求队列中出发楼层和内部请求队列中目标楼层距离此时最远的楼层作为下一个目标楼层,移动过程中对其他请求进行捎带。该调度策略体现在调度器类中的setTo函数中。
- 输出安全:包装输出类,并使用同步方法。
时序图

hw6
题目要求:新增电梯种类:横向电梯;实现动态添加电梯。
迭代思路:
- 增加一个横向电梯类(由于不可抗力,只是在原有电梯类里添加功能和识别信号)。
- 每个楼层增加一个调度器与电梯间的共享对象。
- 调度器类增加功能:识别不同类型的请求、可以添加电梯、可以为横向电梯设置目的地。
UML图

细节:
-
将线程中的同步块调整为共享对象中的同步方法。
-
电梯name改为int类型,便于计算横向最小路径。
-
直接以hsharetables数组的下标作为key值,对应相应的楼层。
时序图

除调度器类新增增加电梯的功能外,其它保持不变。
hw7
题目要求:请求可能需要换乘;新增电梯可自定义属性。
迭代思路:
-
自定义电梯属性只需要修改一下构造方法即可。
-
换乘请求需要被划分为若干个阶段,而且一个阶段需等到上一个阶段完成后才能开始处理。于是我选择将官方包的请求类进行包装,便于查看当前请求的处理进度,处理到一半的请求则可以返还给调度器再次分析与分配。
-
为了防止有请求未完全处理完而以后的阶段所需电梯进程提前结束,故增加了一个全局计数器,直到所有请求都完成后才可结束全部进程。
细节实现:
-
请求从电梯返还给调度器也是异步的,电梯处理完毕但未完成的请求将传到inputBuffer中,实现返还。
-
包装的请求类增加nowBuilding、nowFloor属性,便于电梯判断当前的请求状态。
-
全局计数器使用单例模式。
UML图

时序图

自己的Bug
hw5
-
判断电梯内人数时出现了线程安全问题,导致有时会判断错误,从而造成超载。
解决方法:增加同步快。
-
输出线程安全。
解决方法:包装安全的输出类。
hw6
-
某个细节导致线程安全
public void run() { while (true) { if (shareTable.isInputEnd() && shareTable.empty() && inside.isEmpty()) { break; } else if (shareTable.empty() && inside.isEmpty()) { synchronized (shareTable) { try { shareTable.wait(); continue; } catch (InterruptedException e) { e.printStackTrace(); } } } ...在6 7行之间,如果调度了调度器线程线程修改了shareTable,则电梯仍会进入等待。
解决方法:在第七行同步块中再判断一次shareTable.size。
hw7
-
无bug,但调度策略太拉了,以至于rtle了一个点。。。
解决方法:原本选择中转电梯是以和出发、目标楼层距离总和作为判断条件,我增加了电梯中人数权重。
别人的Bug
hw5
- 在c房,基本上问题和我一样:超载和输出安全。
hw6
- 堆数据,刀到了线程不安全的程序。
hw7
- 有的代码的横向电梯线程在无法接送等待队列中的请求时仍不进入等待态,可以据此设置数据hack cpu-time。
测试策略
- 将自己coding过程中发现的中测测不出来的问题转化为hack数据。
- 分析代码中可能存在的读写冲突。
- 堆数据。
发现线程安全问题的策略
- 堆数据+多次测试。
测试策略的差异
- 堆数据和分析代码都是最有效的方法,最大的不同就是线程安全问题相同数据可能测多次才会出现bug。
心得体会
这个单元的之所以需要多线程编程,是因为每部电梯都是异步运行的(根据实际),我们需要模拟电梯并行运行并在输出时附上时间戳。除此之外,输入请求与电梯运行之间也是异步的,故我们至少还需要一个线程来分配输入的请求。
总体架构上,我一直以生产者-消费者模式为基础,只有第三次作业需要消费者将未处理完的请求再放回托盘(有点流水线的意思)。这样的设计虽然很方便,理解起来也简单直观,但经过两次迭代后已经稍显臃肿,可拓展性较差。
细节方面,首先是锁的使用上,我也一直使用最简单的synchronized方法,邵同学介绍的reentrantlock与吴际老师上课介绍的自定义锁的方法显然更加灵活高效,但由于我的怠惰失去了尝试他们的大好机会,实在可惜。
至于调度策略,单电梯时我是用的是类似look的策略,而多电梯时则使用自由竞争加上look策略,虽然没在这方面下太多功夫,但测试结果的性能分也差强人意。

浙公网安备 33010602011771号