BUAA-2024年春-OO第二单元总结

目录

前言

同步块的设置和锁的选择

调度器设计

UML协作图

 双轿厢电梯的两个电梯碰撞问题

多线程程序的debug方法

心得体会


前言

OO第二单元作业主要是多线程电梯系统,第一次作业中直接对每位乘客指定一部电梯运送;第二次作业中不再指定电梯,需要自己构思分配策略,而且加入了电梯Reset请求;第三次作业加入了DoubleCarReset请求。

下面是我在第三次作业完成之后各个类以及代码规模。

其中Advice类是枚举类,用于呈现Strategy提供给Elevator的下一步行动,具体大致有Move、OPEN、REVERSE和NORMAL_RESET、DOUBLE_RESET,在每次电梯采取行动之前,都会通过电梯内置的Strategy得到当前周期该如何运行。

InputThread类是我为输入单独开的一个线程,如果读到null就结束线程,读到Request则进行判断,将Request细分到WaitTable的不同类Request的等待队列中。WaitTable是用于暂存InputThread线程输入内容的一个类,Schedule类会从WaitTable中读取不同的Request然后按一定的分发策略将请求分给不同的RequestTable。RequestTable与WaitTalbe有些类似,但是RequestTable是每个电梯的请求等待队列,而WaitTable是所有未分发请求的集合。具体架构大致如下图所示。

同步块的设置和锁的选择

我在最开始的时候并没有很好地理解同步块和锁,只是无脑地在方法前加synchronized关键字,这里警示后面的同学在上手之前一定要多查阅java中关于多线程安全问题的知识,避免像我一样在这方面吃亏。

首先,我只是在共享类的方法上加了限制,这样并不能完全解决多线程冲突的问题。比如通过requestTable.getRequestTable()方法可以将该requestTable对象取出,这个方法用synchronized关键字限制是线程安全的,但是将requestTable对象取出之后再对其进行的操作就不一定是线程安全的了,所以使用synchronized()修饰代码块的方式可以更好地保证线程安全。

在hw5中,同步块的设置较少,我将输入的请求直接加入到各个电梯的RequestTable中,ELevator在接送乘客时,会删除RequestTable中已经进入电梯的乘客请求,所以只需要对这一部分加以限制即可。

hw6中,由于Reset请求的存在,导致Elevator的请求可能被重新返回到WaitTable中进行重新分配,所以共享块不仅局限在WaitTable这一个类中。这时更加显示出只在方法前加synchronized关键字限制的方法行不通,必须使用synchronized对代码块进行限制才可以最大可能地避免多线程冲突问题。

hw7中,贡献资源进一步扩大,特别是DoubleCarElevator的加入,使得程序需要考虑的可能性显著增大。当电梯受到DCReset请求的时候,我是先新建两个电梯线程再删除旧的电梯线程,这时候如果不加锁,很有可能发生旧的电梯被删除了,但是乘客请求还是分配给旧的电梯的情况。

我在这几次的作业中只使用了synchronized关键字,这也是在第一次课上老师讲的内容。而且相较于lock()、unlock()或者读写锁,synchronized关键字不需要手动释放锁,在一定程度上比较方便。

调度器设计

我在hw5中并没有设计调度器,因为hw5中直接将乘客指定一部电梯来运送,所以我在InputThread中直接将该请求加入到Elevator的RequestTable中。

然而在hw6中,由于乘客请求不再直接指定电梯来完成,而且加入了ResetRequest,此时调度器Schedule就不可或缺了。于是我设计了Schedule类,将WaitTable中的请求按一定的策略进行分发。分发策略有很多,诸如影子电梯、随机、模六等,我采用的是调参方法,粗略地计算了一下各个电梯接到即将分配的PersonRequest所需要的时间。此外要注意ResetRequest的优先性,因为作业要求接收到Reset请求的电梯必须在输出两个ARRIVE之内开始Reset,所以我在Schedule中优先判断WaitTable中有无ResetRequest,如果有,则先分发ResetRequest,直到分发完毕再进行PersonRequest请求。同时在hw6中要注意Schedule线程结束的条件,如果有电梯在进行Reset,这时不可以结束Schedule,因为电梯中的乘客会被返回到WaitTable中,此时还需要Schedule重新对其分配。

    public boolean ifHasReset() {
        boolean resetFlag = false;
        for (Integer key : processingTablesMap.keySet()) {
            //说明有电梯正在reset,所以此时不能结束schedule线程
            if (processingTablesMap.get(key).getNormalResetRequest() != null
                    || processingTablesMap.get(key).getDoubleCarResetRequest() != null) {
                resetFlag = true;
                //System.out.println(key +" is reseting");
                break;
            }
        }
        return resetFlag;
    }

在hw7中,我延续了hw6的分发策略,对于双轿厢电梯的乘客分配,我设置如果一个乘客的出发楼层不在双轿厢电梯的接受区间内,则将该电梯的权重设为MAX_VALUE,所以不会将这个请求分配给该电梯。对于双轿厢电梯中想要跨过换乘楼层的乘客请求,需要在换乘楼层此离开电梯,这时需要将其返回到WaitTable进行重新分配,所以只要有DoubleCarElevator在运行,Schedule线程就不可以结束,否则将无法进行再分配。

if (de.getType() == 'A') {
    //说明这个人是在该电梯的接受区间的
    if (trfloor >= fromFloor) {
        //这个人在转换楼层,而且要往上走
        if (fromFloor == trfloor && person.getToFloor() > fromFloor) {
            score = Double.MAX_VALUE;
        } else {
            score += scoreA(curFloor, direction, speed, trfloor, fromFloor,
                                capacity, de);
        }
    } else { //如果该人不在电梯的可接受区间,则直接打最大分
        score = Double.MAX_VALUE;
    }
}

为了避免RTLE的问题,我在写程序的时候优先考虑了总体运行时间,而不是耗电量。所以我将乘客数与满载数的差的权重设为较大,这样可以在一些情况下调用其他的电梯分担一部分请求。在hw7中由于DoubleCarElevator的耗电量只有原电梯的四分之一,所以我将DoubleCarElevator的权重*4,在相同情况下,将乘客请求优先分配给DoubleCarElevator。

UML协作图

下面是我在不同作业中增加的线程,可以较为清晰地看到不同线程之间的协作关系,其中Schedule线程几乎与其他所有线程都有交互,所以在写程序时,Schedule线程与其他线程的冲突一定要考虑清楚。

在三次作业中,Main、InputThread线程变化较小,变化主要集中在Schedule、Elevator和DoubleCarElevator线程,因为这些类直接关系到具体的请求。未来如果要继续扩展的话,修改仍然会集中在这几个类。但是我在hw7中加Elevator变成了接口,使得程序具有更好的扩展性。

 双轿厢电梯的两个电梯碰撞问题

双轿厢电梯的一个重要问题是同一电梯井的A电梯和B电梯可能在共享楼层发生碰撞问题,这一点我对同一电梯井的两个电梯设置了一个共享类TransFloorFlag,在双轿厢电梯即将进入换乘楼层的之前,需要检查换乘楼层是否被占用,而在双轿厢电梯离开换乘楼层之后,需要将共享类中换乘楼层被占用的标志设为false。

public class TransfloorFlag {
    private boolean occupied;

    public TransfloorFlag() {
        this.occupied = false; //初始时默认两部电梯的transfloor没有被占用
    }

    public synchronized void setOccupied(boolean occupied) {
        this.occupied = occupied;
        notifyAll();
    }

    public synchronized boolean getOccupied() {
        return this.occupied;
    }
}

这里需要特别注意什么时候该检查换乘楼层被占用,什么时候该将换乘楼层设置为未占用。我在设计中当DoubleCarElevator到达transFloor+1并且要往下走的或者transFloor-1的楼层并且要往上走的时候,判断换乘楼层是否被占用,如果换乘楼层被占用,则向该电梯发送wait命令;若没有被占用,则向电梯发送move命令。如果该电梯当前在换乘楼层,当电梯已经ARRIVE到另一个楼层之后,才会将换乘楼层释放。

此外,我在设计中还对位于换乘楼层的电梯进行特判,如果该电梯停留在换乘楼层没有请求,即该电梯会在换乘楼层wait,这样显然会造成对共享资源的一直占有。所以我将其设计为如果电梯停留在换乘楼层没有请求需要去做,则电梯将会移动一层,主动让出对换乘楼层的占有。

......
//当电梯里没有乘客的时候         
if (requestMap.isEmpty()) { //如果请求队列里没有人,不要让A和B电梯在transfloor占位
    if (type == 'A' && curFloor == tfFloor) { //如果是A电梯,并且在transFloor
        if (direction) { //调头
            return Advice.REVERSE;
        } else {
            return Advice.MOVE;
        }
    } else if (type == 'B' && curFloor == tfFloor) { //如果是B电梯
        if (direction) {
            return Advice.MOVE;
        } else {
            return Advice.REVERSE;
        }
    }
    if (requestMap.isEndTag()) {
        return Advice.OVER; //如果输入结束了,则电梯线程结束
    } else {
        return Advice.WAIT; //如果输入没结束,则电梯线程等待
    }
}

多线程程序的debug方法

多线程的debug真的太痛了。我在最开始没有注意到多线程的线程安全问题,而且由于多线程的不确定性,很多bug在中测是无法测出的。我自己总结的debug方法大概有两点。一是肉眼debug,这个确实是无奈之举,但是当你潜下心来面对自己的代码时,可以非常有效地发现bug并且理清自己的思路。二是输入高并发性的数据,多线程的冲突发生在同一时间共享资源被不同线程又读又写,从而导致共享资源被错误地读或写。所以在自测或者复现bug的时候,可以采用同样的方法。

还有就是,最好的debug方法就是不要出现bug。在写程序的时候,把自己的想法写下来,仔细思考哪些情形下可能出现共享资源的冲突,多思考一下极端情况,比如某个线程的任务做到一半就停住,其他线程是否会对其任务涉及的数据进行修改。如果某些操作需要是原子操作,则要想清楚需要对哪些共享资源加锁保护。

心得体会

第一次接触多线程是痛苦的,但也是收获满满的。

多线程编程对程序员提出了更高的要求,由于不能面向评测机编程,编写程序时务必要想清楚所有的可能性,特别是在设计对共享资源的操作时是否确保了线程安全。而且多线程在某些方面打破了之前单线程的思路,由于无法确定线程的先后顺序,所以在设计时需要特别考虑清楚这一点。

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值