301
社区成员
发帖
与我相关
我的任务
分享
场景一:线程1执行过程中,因为缺少对象A提供的资源,线程1需要在以对象A为monitor对象的同步块中执行wait方法进入阻塞,避免CPU空转
锁选择:不能及时提供资源的对象A,如下图中的waitingList
同步块设置
锁与同步块中处理语句之间的关系
在以对象A为monitor对象的同步块中执行wait方法,将线程1放入对象A的等待池,需要其他线程调用对象A中含有notify的方法来唤醒线程1
场景二:若线程1在以对象A为monitor对象的同步块中执行wait方法,需要在A对象中设置加有synchronized关键字和notify的方法a,通过其他线程调用方法a以及时将线程1唤醒
锁选择:对象A,如下图中的addRequest方法所在的对象this
同步块设置
锁与同步块中处理语句之间的关系
线程2执行对象A中的addRequest()方法,从而执行notifyAll()方法,将因为执行wait()方法而陷入阻塞的线程1唤醒并等待JVM执行
场景三:将对象A中read-modify-write类型的顺序操作(包括但不限于getAndRemove,遍历数组foreach等)包装为一个原子操作方法a,防止在rmw操作执行过程中有其他方法执行导致输出结果不确定
锁选择:对象A,如下图中的getAndMove方法所在的对象this
同步块设置
锁与同步块中处理语句之间的关系
将对象A的锁交给执行getAndMove方法的线程1,保证线程1执行getAndMove方法过程中不会有其他线程调用对象A的其他读写方法,保证getAndMove方法执行结果的确定性
锁的实现:除了synchronized关键字,锁的另外一种实现:读写锁ReadWriteLock
锁选择:
同步块设置
锁与同步块中处理语句之间的关系
在进入方法时加锁,即执行lock()方法,在退出方法时解锁,即执行unlock()方法,由于多个线程同时调用读方法的顺序变化不会产生结果影响,因此相比使用synchronized关键字使同一时间最多只有一个线程调用对象中的方法,使用读写锁可以在多线程同时读取时提高一定性能
除此之外,如果需要在return时调用访问方法,可以通过try-finally块保证在return语句执行后再解锁,如下图
由于请求输入时已指定电梯,也没有重置请求,不需要额外分配器,由输入线程InputThread即输入即分配
调度策略:采用look策略,总的来说原则如下:
以下内容中的同向和顺路可能有歧义,特此说明:
同向指乘客的请求乘坐方向和电梯运动方向相同,如乘客1:6->10,电梯方向向上,我们就说乘客1和电梯同向
顺路指乘客的出发层位于电梯的运动方向路径上(包括电梯所在层),如乘客1:6->10,电梯位于6层,方向向上,则我们说电梯和乘客1顺路
如:电梯此时在5楼,方向向上,乘客1:6->9,乘客2:8->6,乘客3:10->7,乘客3:4->5,则电梯会先将乘客1带上电梯并送到9楼,再将乘客3带上电梯,方向改为向下,再将乘客2带上电梯,将乘客2和3送到目的地后再去接送乘客4
伪代码如下
// 获取主请求
Person getMainRequest(Action direction) {
// 电梯有人,那么先把电梯上的人全部送到目的地,任意选电梯中的一个乘客为主请求
// ...
// 等待队列有人
// 电梯无方向,选到达时间最早的为主请求
// ...
// 否则,电梯有方向,任意取出同向顺路的人为主请求
// ...
// 否则,电梯有方向,任意取出顺路反向的且上电梯时最晚的人为主请求
// ...
// 否则,没有顺路,掉头寻找
// getMainRequest(!direction);
// 否则电梯内和等待队列都没人,没有主请求
}
// 获取方向
void setDirection() {
// 没有主请求,方向置无
// 有主请求
// 主请求在电梯里,或者主请求在电梯楼层也就是即将上电梯
// 主请求期望向什么方向走,电梯就往什么方向走
// 否则电梯向主请求方向移动
}
// 预估行为
Action getAction() {
// 什么情况下关门
// 什么情况下开门
// 什么情况下移动
}
分析:这样的策略很贴合实际的电梯,既不会因为电梯对同一个方向的乘客接送不充分而导致部分乘客等待时间太长,也不会因为电梯反复掉头而加大耗电量,既易于实现又具有不错的性能
交互方式:电梯线程本身就在不断重复预估行为和执行行为的过程,电梯线程在每一次预估行为前都会更新一次主请求,再根据主请求更新方向,最后根据主请求、运行方向、请求列表等来确定执行何种行为(如开门接客,移动送客,无客等待等),可以说电梯行为的调度和电梯线程本身是串行的
沿用第一次作业
总的来说,请求具体分配给哪个电梯,取决于由耗电量,等待时间等性能综合选出的最优解。
不少同学使用影子电梯对一个乘客的接送行为进行性能预估,所谓影子电梯,就是根据每个电梯的状态分别克隆出一个只包含必要信息(如乘客,所在楼层,运行方向,移动时间等)的电梯对象,再将请求投喂到每个影子电梯的性能预估方法中,通过比较性能分,选出最优解并分配。
这种策略虽然不现实,因为现实中,乘客在请求电梯时只会告诉系统自己想往上走还是往下走,系统直到乘客登上电梯才知道乘客要前往几楼,但是作业要求如此,设计影子电梯策略就没有任何问题。
调度策略:
不同于影子电梯,我的调度策略是比较贴合现实的。从性能分上来看,可能是因为数据量不大,性能分和影子电梯相差无几,由于现实中同时出现几百个请求乘坐电梯的可能性几乎为零,因此,性能差异可以忽略不计,不过实现起来更加自然简单,大致来说实现策略如下:
伪代码如下:
void run() {
while (true) {
// 对循环何时停止的判断
// ...
// 将unallocatedList中的每一个请求取出并尝试分配
while (!unallocatedList.isEmpty()) {
allocate(unallocatedList.getAndRemove());
}
// 休息一段时间
// sleep
// 将unsettledBuffer中的请求再加入unallocatedList中
}
}
void allocate(Person request) {
// 筛选合格电梯:未在重置,不会满,电梯在等待状态或同向顺路
for (Elevator elevator : elevators) {
// 寻找满足要求的距离request楼层最近的电梯
// ...
}
// 电梯不存在,加入unsettledBuffer
// ...
// 电梯存在,分配给电梯
// ...
}
分析:这样的策略和实际的电梯较吻合,只有在特殊情况下会不同,如:
电梯1在5楼,方向向下,但准备掉头,电梯2在1楼,方向向上,此时请求在6楼,方向向上,按照这种策略,请求会被电梯2接收,但现实中,该请求往往会被电梯1接收,因为电梯1实际上赶到6楼更快,因此或许不一定要寻找同向顺路且最近的电梯,寻找到达请求所在楼层最快的电梯也不失为不错的办法,但我的调度策略好处在实现简单
交互方式:我为分配器单独开了一个线程,没有继续直接沿用输入线程InputThread中即接收即分配的策略是因为第二次作业新增了重置,也就是中途可能会下客,这就导致请求的分配和请求的接收不一定是同步的。因此,最好的办法是新增线程,独立地对输入线程InputThread的请求进行分配和对电梯线程Elevator重置的请求进行再分配
沿用第一次作业
和第二次作业唯一的差别在筛选合格电梯时加上请求所在楼层在电梯运送范围内
三次作业中,稳定的内容为:
三次作业中,易变的内容:
实际上双轿厢电梯A和电梯B之间除了在换乘层附近的运行之外,其他的任何行为,如接收乘客请求Receive、上下乘客In和Out等都是完全不相干的,只需要交换是否正在,或者即将进入换乘层的状态信息。因此,我采取Admit-Check的机制来管理AB电梯向换乘层的运行。
起初,我认为只需要在电梯内置布尔变量isTransferring来代表正在换乘,在策略类获取电梯行为时,按照以下逻辑控制运行:
伪代码如下:以上层电梯为例
private boolean isTransferring = false;
protected Action getAction() { // 策略类决定电梯行为的方法
// 一系列开关门的行为判定
// ......
// 移动行为判定
// 该电梯准备进入换乘层
isTransferring = true; // 申请进入换乘层
// check下层电梯是否也正在申请换乘
// 下层已经申请了换乘,那该电梯就等待
// 下层没有申请换乘,那该电梯进入换乘层
// 该电梯在换乘层,且下层电梯在申请换乘或者上楼离开换乘层
isTransferring = false; // 释放申请
// 向上移动,离开换乘层
// 其他移动行为
// ......
}
如上,也就是简单的设置独立的布尔变量表示申请状态的做法,然而这样的实现却会在多线程中有个问题,举一个简单的例子:
Input:
[1.0]RESET-DCElevator-1-6-6-0.4
[4.0]1-FROM-6-TO-11
[4.0]2-FROM-6-TO-1
Output:
[4.00]RECEIVE-1-1-B
[4.00]RECEIVE-1-1-A
(然后就会卡住)
一个电梯刚接受DoubleCarElevatorReset重置完,换乘楼层在6楼,上下两个电梯分别在5,7楼,此时同时来了两个乘客,一个从6楼到11楼,一个从6楼到1楼,显然这两个请求会被上下两个电梯分别接受,然而在运行过程中,经常会看到程序卡死,这是怎么一回事呢?经过检查发现存在这种情况,当两个电梯都运行到isTransferring = true语句之后,因为check始终为true,因此两个电梯都会陷入不断等待的状态,也就是卡死了,这下我们就知道了bug产生的原因————是由设置和查询不是同步执行导致的,也就是说,isTransferring = true和check()应该放在一个代码块中同步执行,且在两句代码连续执行结束前不允许其他电梯执行,这可太耳熟了,这不就是一个加了synchronized关键字的方法吗,由此,我们也得到了一种解决方案,新增一个锁类TransferLock,用来记录和查询上下两个电梯对进入换乘层的申请
public class TransferLock {
private boolean lockA; // 下层电梯的占换乘楼层的锁
private boolean lockB; // 下层电梯的占换乘楼层的锁
public synchronized boolean lockLowerAndCheckUpper() {...}
public synchronized boolean lockUpperAndCheckLower() {...}
public synchronized boolean checkLower() {...}
public synchronized boolean checkUpper() {...}
public synchronized void unlockLower() {...}
public synchronized void unlockUpper() {...}
}
由此,getAction方法就可以修改成:
private TransferLock lock = new TransferLock();
protected Action getAction() {
// 一系列开关门的行为判定
// ......
// 移动行为判定
// 该电梯在换乘层上面一层且运行方向向下,也就是准备进入换乘层
if (lock.lockUpperAndCheckLower()) // 申请换乘并查询另一位电梯是否在换乘
// 下层已经申请了换乘,那该电梯就等待
// 下层没有申请换乘,那该电梯进入换乘层
// 该电梯在换乘层,且下层电梯在申请换乘(lock.checkLower())或者上楼离开换乘层
lock.unlockUpper(); // 释放换乘锁
// 向上移动,离开换乘层
// 其他移动行为
// ......
}
由此,我们可以产生类比,换乘楼层不可能同时出现两个电梯,就像一个锁不能被两个线程同时持有,也就是说双轿厢电梯换乘所产生的冲突,可以用锁的思路来解决,更重要的是,几乎所有的涉及类似一房间一锁问题都可以这样解决。
因为多线程程序不易调试,且bug难以复现,因此,对出错测试点的输入要加以保存,同时,在代码不同段标记System.out.println用来输出文本不失为一个比较好的办法
例如:在part3中提到的双轿厢电梯死锁,由于死锁的出现,导致程序卡死无法继续输出,这时候我们就可以在代码各个地方添加println函数输出信息,多次运行,不断缩小范围,最终确定程序在哪里出现了卡死
仅凭肉眼分析,只有卡死这种程序出错是容易辨别的,如果条件允许,可以编写一个验证器,验证输出序列的合理性,并在输出错误行为的行上弹出错误,当然,没有条件的话依赖课程组的评测机也不失为一点小手段