2024 BUAA OO Unit2 总结

同步块与锁

同步块的设置和锁的选择:在前两次作业中选择了synchronized 对线程之间共享对象进行加锁,第三次作业选择了读写锁 ReadWriteLock 进行加锁。

锁与同步块中处理语句之间的关系:同步块上加锁使得同步块中处理语句具有原子性,不同线程的运行不会相互影响,保证线程安全。

调度器设计和调度策略

HW5

需求 :

使用固定电梯将乘客送至目标楼层。

电梯捎带策略选择的是"Look" :

过程描述:

1.电梯处于待命状态 (direction == 0):
a.若等待队列为空,则进行wait,电梯待命。等待队列新加入请求时唤醒。
b.若等待队列有人
若请求的起始地址与电梯楼层相等,人向上走则电梯也向上走 direction = 1 人向下走电梯也向下走 direction = -1;
否则电梯朝着该请求的起始方向前进。

2.判断当前楼层是否有人出去

3.判断当前楼层是否有人进

进电梯的条件:

当前楼层与请求起始楼层相等。 若请求要走的方向与电梯当前运行方向相等,则进入电梯。

若没有与电梯同方向的请求,且往这个方向走更远也没有同一方向的请求,则电梯调转方向。

4.若电梯有人进出之后电梯内没有人,则进入待命状态,direction = 0;

采用的和实验类似的“生产者-消费者”模式

类图:
在这里插入图片描述

有InputThread Schedule 和六个Elevator线程 线程之间公共部分RequestQueue用synchronized加锁,当请求队列为空时进行wait,调用其他方法时notifyAll唤醒。

InputThread将请求读入到公共队列WaitQueue,Schedule将WaitQueue中的请求分配到各个电梯的待处理队列ProcessQueue中。每个电梯线程模拟各自的运行。

生产者消费者模式具有较好的可扩展性。

代码规模:
在这里插入图片描述

HW6

新增需求:

实现自己的调度策略,电梯需要重置,每个人在进入电梯前需要被receive。

类图
在这里插入图片描述

新增Passenger类来存储人坐电梯的请求。
在HW6我没有采用特殊的调度策略,直接%6,这导致强测中性能分较差。

对于电梯重置请求,首先将电梯停下,里面的人全部出去,如果没有到达目的地就重新加入公共队列WaitQueue重新进行分配。电梯的待处理队列也重新加入WaitQueue。Schedule继续分配WaitQueue中的请求。

对于Receive操作,我在每个Passenger类中新增了Received标记,如果为0表示没有被任何电梯receive,否则为电梯的id。由于电梯无法在没有receive任何人的情况下移动,因此look算法中,促使电梯运行或者转向的请求需要先被receive。在人进电梯的时候重新判断,如果这个人已经被receive过了就不需要再receive,反之则反。电梯重置时所有和这个电梯有关的Passenger的Receive标记都需要被清空。

InputThread结束条件:输入结束

Schedule结束的条件:InputThread结束,WaitQueue为空且所有电梯的待处理队列为空

Elevator结束的条件:Schedule结束,电梯direcion == 0 即没有运行

代码规模
在这里插入图片描述

HW7

新增需求:

电梯可以被重置为双轿厢电梯(DCReset),换乘和冲突处理

类图
在这里插入图片描述

新增ElevatorController来对电梯进行总控。

对于DCReset请求,首先将原来的电梯stop,然后增加两个新的电梯。原电梯中的人和请求队列中的人重新加入WaitQueue进行分配。
新增电梯:

B电梯 :type == 1  minFloor = transferFloor maxFloor = 11

A电梯 : type == -1 minFloor = 1 maxFloor = transferFloor

Schedule策略:
每个人找到自己当前位置所需时间最短的电梯,加入其待处理队列。对于双轿厢电梯,可能它没办法到人所在的位置,因此我给每个双轿厢电梯新增一个FriendElevator属性保存拆分出的另一个电梯。这样所需时间就是当前电梯移动时间 + FriendElevator移动到人所在楼层的时间。

if (type == 0) {
    floors = calcFloor(fromFloor, floor);
    time = floors * speed;
} else if ((type == 1 && fromFloor >= transferFloor)
        || (type == -1 && fromFloor <= transferFloor)) {
    floors = calcFloor(fromFloor, floor);
    time = floors * speed;
} else {
    floors = calcFloor(floor, transferFloor);
    Elevator friendElevator = this.getFriendElevator();
    time = floors * speed;
    time += friendElevator.calcFloor(transferFloor, fromFloor) * friendElevator.getSpeed();
}

换乘处理:
电梯移动到换乘楼层时,若乘客需要到另一半楼层则下电梯并加入WaitQueue重新分配。

冲突处理:
新增共享楼层SharedFloor类,由两个电梯共享。当一个电梯尝试进入共享楼层时,调用方法尝试获得锁,无论如何只有一个电梯能获得锁,同一时间只有一个电梯能进入共享楼层,另一个电梯会wait直到该电梯离开共享楼层后被唤醒。

public void enterSharedFloor() {
    lock.writeLock().lock();
    if (!free) {
        try {
            waitForFree.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    free = false;
    lock.writeLock().unlock();
}

public void exitSharedFloor() {
    lock.writeLock().lock();
    free = true;
    waitForFree.signalAll();
    lock.writeLock().unlock();
}

在第七次作业过程中由于数个bug无法在本地复现,线程安全始终存在问题,我将synchronized改为了ReadWriteLock,并进行了重构。读写锁更易于理解,在对共享对象有关方法进行加锁时更加精准。synchronized加锁很容易出现盲目加锁的情况,导致方法之间互相被卡死,reset操作很容易无法被及时处理。

InputThread结束条件:输入结束

Schedule结束条件:WaitQueue为空,InputThread结束,各个电梯处理队列为空,电梯中没有人

Elevator结束条件:被DCReset 或者 Schedule结束且当前电梯不在运行(direction == 0)

代码规模:
在这里插入图片描述

Bug

多线程Debug较为困难,很多bug由于线程不安全执行顺序不一定等原因无法在本地复现。一下是在每次作业中出现的bug:

HW5:

出现了ConcurrentModification的报错,分析后发现是以下原因:
我在共享对象RequestQueue中写了这样的方法:

public synchronized ArrayList<PersonRequest> getRequests() {
    return requests;
}

将RequestQueue中的requests返回到Elevator中进行处理。这样的锁加了等于没加。因为后续在Elevator中的操作是对requests本身这个ArrayList进行的,后续的操作没加锁,只是对返回这个ArrayList这个方法进行了加锁。

因此我将Elevator中的操作移动到了RequestQueue类中并对其加锁。遍历移动等操作都在RequestQueue这一共享对象中处理。

还出现了一个低级错误,加的人数超过了电梯容量。

HW6:

在将电梯reset时,先输出OUT再让电梯里面的乘客出去,否则在输出OUT之前其他电梯已经开始抢夺乘客了,造成没有OUT其他电梯就RECEIVE的情况。

HW7:

使用 ReadWriteLock 读写锁代替 synchronized进行加锁,保证线程安全且更加具有灵活性。

电梯进行 DC重置时需要首先将其stop ,否则schedule线程可能会在其重置的过程中给它分配请求,则它永远无法被处理,导致程序停不下来

程序停不下来还可能是因为wait一直没有被唤醒,一种特殊情况是在输入末尾将所有电梯进行DC重置,我将重置后的电梯当做新的电梯加入电梯队列,但在重置时schedule线程已经结束,新的电梯没有被打上结束标记,导致电梯停不下来。

乱加锁导致一个线程之间各个方法相互阻塞,使reset操作无法被及时执行,使用读写锁可以更好地解决该问题,方便理解。

多线程debug方法:

多线程程序debug过程中可能会出现本地正确交上去不对的情况,一般是因为线程不安全,要充分考虑每种情况。

若出现轮询,可以在每个线程while(true)里面输出信息,找到轮询的线程。

若线程没结束,一般是在某个地方wait没有被唤醒。只需要在wait前后都打上标记,找到停止的位置。

代码逻辑较为复杂时使用ReadWriteLock更方便理解,加锁更精准,减少错误。

心得体会

线程安全非常重要,bug出现70%的原因都是线程不安全。使用ReadWriteLock进行加锁可以更好地保证线程安全。CopyOnWriteArrayList可以替代ArrayList,其有关的操作都具有原子性。不能盲目地加锁,否则会导致部分请求无法被及时处理。

我在三次作业中都是用了“生产者-消费者”的模式,它便于理解且具有较好的扩展性。层次化设计可以让代码更加具有可读性和可扩展性。

OO的学习是一个磨炼心智的过程,不能当ddl战士,否则bug真的会de不完。Unit2虽然过程比较曲折但是学到了很多。

  • 28
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值