Synchronized-同步块与锁
1. 对所有涉及共享对象的方法都加锁! 2. 遍历访问共享对象时,还应该对遍历语句块涉及到的实例化共享对象加锁(即设置为同步块)! 3. 如果共享对象被封装成了一个类,可以对该类中的方法加锁 (类比OS中的管程) ,使方法内部的语句运行不会受到其他线程语句的干扰; 如果共享对象不是单独的一个类,就需要对涉及共享对象的操作的语句块加锁 (类比OS中的信号量实现互斥),使不同地方对其的访问是互斥的。 |
- 同步块加锁实例:
InputThread类
synchronized (totalList) {
RequestList requestList = totalList.
get(((NormalResetRequest) request).getElevatorId());
requestList.addRequest((NormalResetRequest) request);
}
Allocate类
synchronized (totalList.get(i)) {
totalList.get(i).setEnd(true);
}
synchronized (totalList) {
totalList.get(id).addRequest(request);
}
调度策略:分级调度算法
1. 多电梯协作:使用自己构造的策略分配请求
在选择最合适的电梯时:若普通电梯正在重置,跳过;若为双轿厢电梯,则选择A或B中的一部进行判断。
设置index为:该部电梯到乘客起始位置的有向长度 / 该电梯的运行速度。
若该电梯接收到该乘客后达到人数上限,则令index为正无穷。
若该电梯的请求列表为空,将系数index减少,增加其优先级。
选择index最小的电梯接受请求。
若当前没有可用的电梯, sleep(50); 后将其重新放入总请求池 WaitListOfAll
若某电梯接受到的乘客 >= 其最大容量,则让其休息2秒后再接受请求。该策略可以有效防止一部电梯接收到过多请求。
对于双轿厢电梯,考虑到其省电的特性与可能增加的多次换乘相互抵消,故在我的架构中没有改变双轿厢电梯的优先级。
- 该自定义策略可以较好地平衡耗电量,乘客等待时间等指标,得到良好的性能分数。
2. 单电梯运行策略:使用LOOK策略处理请求
motion = Drive.askForNextMotion(curFloor, capacity,
passengerNum, directionOfNext, requestList);
switch (motion) {
case MOVE:
move(moveTime);
break;
//other case: ……
}
在 Motion 类中实现单部电梯的 LOOK 运行策略。
3. 调度线程与其他线程的交互
Allocate 调度器线程传入了总请求池(waitListOfAll),分电梯请求表(totalList),总电梯(elevators)三个共享对象。其功能是把 waitListOfAll 中的请求按照分配策略,有序地分配到 totalList 中。同时,需要通过 elevators 得到电梯的一些属性(比如是否在重置,最大容量等),来进行合适的分配。
在电梯线程中,调用 Motion 类的 askForNextMotion 函数,传入电梯当前的参数(属性)以得到下一步的行动。
注意对共享对象的访问是互斥的(借用OS中的概念),需要加锁。
最终主要结构
Main类
构造并启动线程:输入线程,分配器线程,六个电梯线程。
InputThread类
若输入结束,退出线程并将输出信号传给 WaitListOfAll
若获得普通请求,将其加入调度器。
若获得重置请求,立即 将其加入对应电梯的 requestList
Allocate类-分配器
若可以结束( WaitListOfAll 为空且已结束,重置请求全部处理完使用 ResetRequestCount 共享类进行统计
,普通电梯不在重置,双轿厢电梯请求表均为空),向电梯发送结束信号并退出。
若暂时从 WaitListOfAll 中得到的请求为空: sleep(50 后重新判断;
采用分配策略将请求加入电梯的 requestList
Elevator类-电梯
若可以结束(该电梯的请求表为空且已结束且该电梯不处于重置状态,或者该电梯已被重置为双轿厢电梯),直接退出。
优先处理重置请求。
之后,像Drive类(单电梯运行策略)**“询问”**下一步的行动,并执行。
优化:
- 电梯在顶楼或底层开门,自动转变运行方向,避免二次开关门
- 若接收到RESET指令时电梯处于开关门状态,则直接启动重置,避免二次开关门
DcElevator类 extends Elevator-双轿厢电梯
继承电梯类,实现双轿厢独立运行。
在换乘楼层时特判,实现两轿厢互不相撞。
架构与迭代
迭代与扩展
第一次作业实现了单电梯的基本运行,分配策略由输入提供。
第二次作业实现了多电梯分配策略和普通RESET请求
第三次作业实现了双轿厢电梯重置请求
之后,还可以实现开关门防止夹人,可以调整运行模式(高峰模式,深夜模式等)。因为本架构具有优秀的扩展性,实现了层次化设计。
线程协作
通过生产者-消费者模式,实现请求的分配:生产者是输入线程,消费者是电梯线程,中间的托盘是请求表。
稳定与易变内容
调度策略、电梯种类、请求种类等,属于易变的内容,需要根据需求的调整而改变
请求表类(RequestList),电梯开关门、上下乘客的方法等,属于稳定的内容
双轿厢的配合
通过 TransferController 类,使换乘楼层处仅允许一部电梯到达。
到达换乘楼层时,将 共享变量“状态” 设为 OCCUPIED ;离开换乘楼层,将共享变量“状态”设为 FREE
// in Class DcElevator
if (super.getCurFloor() == transferFloor) {
//实现清空轿厢内所有乘客,反转运行方向,接受该层乘客,然后立即离开换乘楼层
clearReverseGetInAndLeave();
}
// in Class TransferController
public synchronized void setOccupied() {
waitRelease();
state = State.OCCUPIED;
notifyAll();
}
public synchronized void setFree() {
state = State.FREE;
notifyAll();
}
private synchronized void waitRelease() {
notifyAll();
while (state == State.OCCUPIED) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
TransferController 类,使用类似**OS中管程**的思想,实现对共享变量的修改,保证两轿厢**互斥地到达**换乘楼层。 |
bug与debug
bug分析
第一次作业中:没有在遍历访问共享对象时加锁。
第二次作业中:
- 没有立即处理RESET请求,导致收到重置信号后可能不会立马处理,电梯继续运行
- 与重置有关的共享变量 isReset 没有加锁,导致多线程冲突
- 修复了乘客进入条件,防止一个乘客多次进入电梯
- 删除不必要的锁,防止误锁导致的某段时间内共享对象无法被访问,因而出现逻辑错误的问题和死锁问题。
第三次作业中:
- 睡0.01秒防止逻辑问题
- 共享对象没有加锁,导致多线程冲突
debug方法
- 通过print打印法,观察线程能否正常结束
- 通过构造高强度或极端数据,检测边界情况
- 通过观察大法,分析加锁的逻辑
心得体会
- 线程安全方面,我基本掌握了多线程编程,线程协作的方法。了解了在合适的地方加锁,既使程序高速运行,又保证多线程不会互相干扰影响。此外,我还知道了读写锁和普通锁的基本使用方法。
- 层次化设计方面,本次电梯作业进一步加深了我对面向对象编程的理解,把“高内聚,低耦合”的原则落实到写代码中。