OO_Unit2
同步块与锁
同步块的设置和锁的选择:三次作业中均选用了synchronized关键字对共享对象进行加锁。
同步块保证了 其中所有对被锁对象进行的操作 在结果上为合并为一个原子操作。由于同步块获取了当前对象的锁,使得其他线程无法对该对象进行操作,从而保障了线程安全。
调度器设计
第一次作业
RequestQueue中的方法均用synchronized关键字进行修饰,调用其各种方法时对其进行notifyAll(),以唤醒因为其而正在等待的线程。Schedule从InputThread中取得请求,从这些请求中逐个取出并分配给各个电梯实例中的waitingQueue。当电梯中的waitingQueue为空时,电梯会使waitingQueue进行wait(),而Schedule对其分配时会唤醒该waitingQueue,使电梯继续运作。
第二次作业
增加了Schedule和Elevator的交互,电梯也能够影响调度器,主要是在电梯重置时将请求重新放回总请求队列中,再通过Schedule重新分配。
为了让电梯及时响应重置请求,现在让InputThread直接通知电梯。
第三次作业
在程序一开始就创建18台电梯,每个电梯井创建三台电梯,由Schedule记录当前应该运行的电梯类型,当该种电梯不用时,它会保持wait状态,在整个程序应该结束前不会被唤醒。
总体来看,三次作业中,架构中的内容基本是保持增加,没有删减。根据二、三这两次作业中功能的增加,增加Schedule与Elevator之间的共享变量,通过对其的共享,实现拓展的功能。
调度策略的思考
在每次进行请求分配时,对电梯进行深克隆,之后对这些克隆的电梯进行操作。
对克隆的电梯的操作:
对每一部电梯(或每一对电梯中符合的那部)添加这一请求,并按照原有的电梯运行策略进行处理,但不实际消耗开关门、移动等行为的消耗时间,而是设置一个timer变量,加上每次行动消耗的时间和电量。计算完成后,选取消耗电量最小的分配方案。由于CPU运算速度非常快,所以这些计算对CPU使用时间的影响不会很大。
由于互测中许多同学们都大量使用卡时间输入大量请求的方式进行hack,这种情况属于特殊情况,需要特殊考虑。因此对该种情况特判,如果出现6部电梯中有5部电梯都在重置中的情况,则使Schedule实例线程等待一会(例如1600ms),保障至少有一定数量的电梯结束了重置过程。
实现两个轿厢不碰撞
首先,我在A、B类型的电梯中都存储了换乘楼层。对于每一对电梯井中的两台双轿厢电梯,给他们分配了一个共同的锁,当一部电梯到达换乘楼层后,它会占有该锁,此时如果另一部电梯试图进入该楼层,则会陷入等待状态,直到电梯离开换乘楼层,并释放锁后,进行notify,唤醒这部电梯。
以下是伪代码实现
// transferFloor 为换乘楼层
//尝试 进入 换乘楼层
// floorId 为电梯 **试图进入** 的楼层
if (floorId == transferFloor) {
//尝试 获取锁
occupied.setOccupied();
}
//……
//开关门
//……
//尝试 离开 换乘楼层
// oldFloorId 为电梯 **移动前** 所在楼层
if (oldFloorId == trnasferFloor) {
//释放锁
occupied.setRelease();
}
bug以及debug
bug和解决方法
三次作业中出现bug有:Schedule实例线程、电梯实例线程无法正常结束,双轿厢电梯提早运行。三次作业中出现bug的次数很多,但bug种类很少,多次均为同质bug。(互测中考验分配策略的TLE问题不视为bug,我认为这是性能太差的结果)
对于前一个bug,是由两个问题导致的。第一个问题是线程不安全导致的,据我根据结果以及代码的分析来看,应该是在判断电梯是否应该结束的判断条件中的对象保护不充分导致的,对这一部分代码块添加同步块保护后,问题解决,Schedule实例线程正常结束。但这时第三次作业的程序仍会出现部分Elevator实例无法正常结束的情况,在这一部分我发现了第二个问题,即对于我为每个电梯井创建的三部电梯会稳定出现部分电梯不结束的情况。经过分析后,我发现这是因为,对于电梯等待,我是通过对于电梯等待队列waitingQueue使用wait()方法实现的,但是电梯判断结束的条件中有对其他实例的调用,这导致waitingQueue未被唤醒,所以那些电梯仍处于等待,不能结束。于是尝试在所有电梯结束前通知同一个电梯井里的其他电梯,bug被解决。
debug方法
在代码各处添加输出语句,输出需要的相关信息。
例如输出我为当前线程构造的名称,例如“Elevator1”、“Elevator6-A”、“Schedule”等,方便判断可能是哪个类的代码出现了问题,而且相较于输出java的官方线程名称,我的这种输出方式更加清晰,具有很好的辨识度。
又例如关键代码处输出相关信息。
如在while循环中输出任意字符串,以此判断是否会出现轮询问题。
或者输出一些用于条件判断的值,分析是否有条件判断过程中错误判断的情况出现。
心得体会
线程安全:
在处理线程安全时,首要任务是准确判断哪些对象可能会受到多个线程的并发访问。一旦确定了这些对象,就需要采取适当的互斥措施来确保它们的操作在并发环境下是安全的。在本次作业中,我主要选择了使用synchronized关键字来实现互斥,尽管读写锁在某些情况下可能会带来更高的性能,但考虑到当前的设计需求和性能影响并不显著,因此我并未引入读写锁来改变原有的设计。
层次化设计:
一个层次清晰、结构分明的架构设计有助于我们更加清晰地理解每个模块的职责和功能。这种设计方式不仅使得代码更加易于维护,还能够实现高内聚、低耦合的目标。当需要进行系统的迭代或更新时,我们可以方便地将需求拆分成不同的部分,并分配到相应的层次中进行处理,从而提高了开发的效率和系统的可扩展性。