OO第二单元
1. 同步块的设置和锁的选择
对于同步块和锁的设置,一言以蔽之就是 对共享对象 加锁。也就是两个或多个线程同时需要读写的对象需要进行同步。
-
hw5
本次作业涉及到共享的对象仅有
RequestTable
,因为输入线程和电梯线程会同时读写该对象。只需在读写方法上锁即可。同时,由输入线程放入请求,电梯线程处理请求的模型可以抽象成一个对生产者无约束的生产者-消费者模型。因此我们只需要在消费者拿取的时候加入同步控制逻辑即可,代码如下:
public synchronized HashMap<Integer, ArrayList<PersonRequest>> getwaitRequest(RequestTable processRequest) throws InterruptedException { while (this.isRequestEmpty() && !this.isOver() && processRequest.isRequestEmpty()) { this.wait(); } if (this.isRequestEmpty() && this.isOver() && processRequest.isRequestEmpty()) { return null; } return this.requestMap; }
-
hw6
由于使用 影子电梯 的调度策略,除了hw5完成的对
RequestTable
的共享对象处理之外,还需要额外处理Elevator
的共享。由于每次调度一条请求的时候,就需要复制当前6个电梯的状态,复制过程中电梯不能改变状态,于是我们需要在ElevatorM
的构造方式中上锁,同时对Elevator
中的读写方法上锁。如下是
ElevatorM
的构造方法public ElevatorM(Elevator elevator) throws InterruptedException { synchronized (elevator) { elevatorId = elevator.getElevatorId(); curFloor = elevator.getCurFloor(); curNum = elevator.getCurNum(); direction = elevator.getDirection(); maxPerson = elevator.getMaxPerson(); moveTime = elevator.getMoveTime(); isReset = elevator.isReset(); cost = 0; waitRequestTable = new ElevatorRequestTable(elevatorId); processRequestTable = new ElevatorRequestTable(elevatorId); for (Map.Entry<Integer, ArrayList<PersonRequest>> entry : elevator.getWaitRequestTable().getprocessRequest().entrySet()) { for (PersonRequest request : entry.getValue()) { waitRequestTable.addRequest(request.getFromFloor(), request); } } for (Map.Entry<Integer, ArrayList<PersonRequest>> entry : elevator.getProcessRequestTable().getprocessRequest().entrySet()) { for (PersonRequest request : entry.getValue()) { processRequestTable.addRequest(request.getFromFloor(), request); } } } }
我之前在考虑 影子电梯 的时候,认为就算不加锁,电梯状态改变了,结果也仅仅是不能完全模拟,不会导致程序结果错误。但是事实上,模拟错误是一件很危险的事情,在某些参数被错误的改变之后,可能导致模拟线程永远无法结束!例如当
curFloor
和curNum
错位,也就是模拟电梯接收到了之前的当前楼层和现在的人数,在一些电梯策略中就会导致不停止地移动。 -
hw7
hw7中的 请求分离 和 中间层 在我的处理中均涉及到同步控制
-
请求分离
我新建一个
DepartRequest
类public DepartRequest(int fromFloor, int toFloor, int personId, int elevatorId, int transferFloor) { super(fromFloor, toFloor, personId); firstStepOver = false; this.elevatorId = elevatorId; this.transferFloor = transferFloor; }
-
用一个变量标记他的第一段是否完成。
-
一旦一个跨越中间层的请求被调度到一个双轿厢电梯,那么我将这个请求发送到先处理的轿厢的
waitRequestTable
,同时把这个请求返回调度队列。 -
这个请求会在放入的轿厢被处理,处理完毕后,将他的
firstStepOver
设置为True
,此时我们允许调度器对这个请求的后半部分进行调度。
那么怎么实现调度器对该请求的阻塞呢?
我们会发现,调度器阻塞的情况是:当前没有可处理的请求,当然队列为空的情况在hw56中已经处理完毕。我们现在只需要处理调度器不为空但是里面只有第一阶段尚未完成的分离请求的情况。
现在取出一条分离请求,使用条件锁,当该请求第一阶段未完成,且队列里没有其他的就绪请求的时候,陷入等待。
private final Condition completeOrArrived = lock.newCondition(); ... else if (request instanceof DepartRequest) { lock.lock(); try { while (!((DepartRequest) request).isFirstStepOver() && !(requestTable.NoDepartSize() >= 1)) { completeOrArrived.await(); // 等待分离请求完成 // 等待新请求到来 } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } }
当等待结束,如果是由于新请求到来被唤醒,那么把该条请求重新加入到队列末尾。否则处理该请求。
if (request instanceof DepartRequest) { if (!((DepartRequest) request).isFirstStepOver()) { requestTable.remove(); requestTable.addRequest(request); continue; } else { // 调度该请求 } }
唤醒逻辑也很简单,只需在新请求到来或者旧请求处理完毕后,调用唤醒
public void notifyGo() { lock.lock(); try { // 更新状态或执行必要的操作 completeOrArrived.signalAll(); } finally { lock.unlock(); } }
-
-
中间层处理
新建一个类来管理中间层
该类中同样设计一个条件锁,当一个电梯要访问中间层的时候,通过
tryAccess
方法访问,若中间层被另一部电梯挤占,那么陷入等待。同时在设计中为了方便,电梯在中间层执行完开关门后立刻调用leave()
离开。public class TransferFloorStatus { private final Lock lock = new ReentrantLock(true); private final Condition condition = lock.newCondition(); private Occupy occupy; public TransferFloorStatus(Occupy occupy) { this.occupy = occupy; } public void tryAccess(Occupy newOccupy) throws InterruptedException { lock.lock(); try { while (this.occupy != Occupy.EMPTY) { condition.await(); } this.occupy = newOccupy; } finally { lock.unlock(); } } public void leave() { lock.lock(); try { this.occupy = Occupy.EMPTY; condition.signalAll(); } finally { lock.unlock(); } } }
-
2. 三次作业中的调度器涉及以及交互
- hw5:不存在调度器设计
由于在输入的时候已经完成分配,故在输入的同时进行调度。
架构如下:
UML协作图
-
hw6
相较于简单的hw5,增加了调度器的hw6难度指数级增长。
如何结束
本人在第六次作业首次加入调度器,并将其作为一个线程单独运行。调度器实际上就是输入线程和电梯线程之间的桥梁,在线程交互层面上的作用是接受输入,并且输出到选择的电梯。个人认为比较复杂的是如何实现整体程序的结束。在第五次作业的基础上,输出结束之后发送信号,逐层传递,并在电梯队列空的时候结束是行不通的。因为此时请求的来源具有多样性,请求不仅可以来自输入,还可以来自电梯的返回。所以我在每个电梯的
ElevatorRequestTable
添加了一个Over
属性,并且增添了一个Reset
全局变量。当此时输入结束之后,我们设定输入结束标志,此时要判断Scheduler
是否结束,我们还需要查看Reset
,如果这个时候并没有线程在Reset
,那么此时可以让调度线程结束。调度线程结束之后,再设定ElevatorRequestTable
结束,在允许结束之后,才可以像hw5一样正常结束了。考虑性能
影子电梯使用模拟的办法,对每个模拟电梯计算cost。选择cost最小的电梯进行调度。
UML协作图
-
hw7
第七次作业由于时间不够,采用随机调度。
我对于双轿厢的处理是采用挂载的方式。将两个双轿厢实例,挂载在电梯里面,这样的话在调度器仍然可以访问先前的电梯队列,不需要改变接口,这样的话就可以非常方便的解决问题。
UML协作图
3. 分析和总结
1.稳定和易变的内容
在三次作业中,输入进程的处理,单电梯内部的策略是很稳定的。
易变的部分主要体现在调度的策略上面。
从第五次作业到第六次作业,需要实现一个调度策略。如果是使用影子电梯的话,在第六次作业到第七次作业的迭代中,会展现出很大的变化。因为电梯种类的增加和分离请求的出现,会导致困难的同步问题。由于本人时间不足,最终没有得以实现。
2.bug的出现以及debug方法
本人在hw6中出现了三个bug,主要是在调度过程中逻辑处理除了一些问题
-
清空线程池的顺序出现错误,当所有的线程都Reset之后,优先continue,那么导致线程池没有被清空,使得电梯队列index超出范围。
-
没有控制一个电梯能够接受的最大请求数:
这个只需要在Reset逻辑上加一个判断,如果超过最大数目,那么重新调度即可
-
由于Reset整个过程需要从输入进程不断传递信号,不断输出,所以很难作为一个原子操作。本人也不是很明白如何修改完美。于是只能多加几个判断,保证当前电梯不在Reset。但是实际上在大量随机数据的攻击下,每一千个数据之下可能会出现一个bug。
3.心得体会
线程安全:多线程这个单元的确是一个非常具有挑战的单元。众多遗漏的线程安全问题,看起来合理但是容易造成死锁的处理等都给初次设计多线程程序的我带来了很大的挑战。但随着一次次作业的巩固,我发现线程安全的遗漏问题已经不会再出现了。因为只要多线程共享对象,我们就需要判断是否会产生线程安全问题。但是线程安全的解决问题还是有待以后进一步探索和学习。
层次化设计:本单元中,清晰的层次化设计给我的迭代带来了很多帮助。输入-调度-处理,三层相互作用,但又互不干扰。例如我在hw7中由于时间不足,紧急修改成随机调度,只需修改调度层中的代码即可,从起意修改到修改结束,不超过五分钟。这种低耦合,高内聚的设计在迭代中带来了很大的帮助。是我在以后的设计中需要贯彻的。