BUAA-OO-Unit 2 总结

目录

一、同步块&锁

1.对方法加锁

2.对代码块加锁

3.使用原子类对象

二、架构设计&交互策略

1.代码架构变化

(1)第一次作业:

(2)第二次作业:

(3)第三次作业:

2.各进程协作关系

三、保证双轿厢电梯不相撞的实现:

四、Bug&Debug

1.CPU-TLE:

2.Run-TLE:

五、体会与感悟

1.一般体会:

2.线程安全体会 :

3.层次化设计体会:


一、同步块&锁

1.对方法加锁

        总所周知,同步块是用于控制多个线程对共享资源的访问。因此,在我的代码中,我的同步块设置都只有一个目的,那就是为了限制共享资源只能被一个线程所获取。我将共享资源,也就是请求池,设置为一个 rquestPool 类,在类中的所有 Public 方法都被加上了锁,这样就不必由线程考虑线程安全问题,只需要直接调用共享资源类方法即可,如下方的 hasReset() 方法就是一个例子,直接用关键字 synchronized 修饰方法即可:

    public synchronized ResetRequest hasReset() {
        for (Request request : requests) {
            if (request instanceof ResetRequest) {
                notifyAll();
                return (ResetRequest) request;
            }
        }
        notifyAll();
        return null;
    }

2.对代码块加锁

        当然,并不是所有线程对共享资源的操作都可以借助共享资源类里的方法实现,这时我们就需要在线程内自行设置同步块。比如,我需要在线程对电梯专属请求池里的 Arraylist 进行直接读取和放入时,就直接在线程内部实现。

        如下面的接受方法,需要直接将 elevatorPool 里的请求都放入到 receivedPool 里。其中同步块和锁的关系是,我需要保证我在对 elevatorPool 里的列表操作时,不希望其他的线程也能够访问elevatorPool,于是我直接获取了elevatorPool的锁。但是这里也许涉及到锁加的范围比较大的问题,但是其实目前的请求池类里的内容也就等价于其中的请求列表,若是以后增加了请求池的状态的要求,或许会有加锁范围过大的嫌疑,不过这也是后话了:

    private void doReceive() {
        synchronized (elevatorPool) {
            ArrayList<PersonRequest> record = new ArrayList<>();
            ArrayList<Request> requests = elevatorPool.getRequests();
            for (Request request : requests) {
                if (request instanceof PersonRequest) {
                    requestReceived.add((PersonRequest) request);
                    record.add((PersonRequest) request);
                    TimableOutput.println("RECEIVE-" + ((PersonRequest) request).getPersonId()
                            + "-" + elevatorId);//[时间戳]RECEIVE-乘客ID-电梯ID
                }
            }
            for (PersonRequest personRequest : record) {
                requests.remove(personRequest);
            }
        }
    }

3.使用原子类对象

        除此之外,我还使用了直接定义原子类信号量的方法,来保证线程安全。如下:

    private AtomicInteger isReset;

    ........

    this.isReset.set(2);

        我直接定义了一个isReset的信号量,由于其是原子整型类,我直接调用相关方法,比如set()等原子操作方法,就可以直接保证对isReset的访问是上了锁的,也就保证了线程安全。

二、架构设计&交互策略

1.代码架构变化

(1)第一次作业:

在第一次作业中,我一共只设置了上图中的5个类。它的UML类图如下所示:

        简单解读一下,就是Main函数里开启要求数量的ElevatorOperation,并同时开启分配调度函数Assignment和输入函数InputThread。由于第一次作业是制定电梯,Assignment类就比较简单。而电梯的运行策略我是采取了比较好实现的Look算法,它有如下一些基本规则:

  • 如果电梯里没人且各个楼层没有请求,则停止运行,否则运送乘客或者前往请求来的楼层。
  • 如果运行到一层,检查是否有出去的乘客,安排乘客出电梯,接着寻找是否有该层上电梯的且目的楼层复合电梯运行方向的乘客,若有,让乘客进入。
  • 如果到达某一层,比如运行方向是向上,若是上面所有的楼层都没有请求且下面的楼层有请求,则掉头运行。(默认顶楼上面的楼层肯定没有请求,因为都没有楼层了)

        总的来说这个电梯运行策略比较复合我们日常生活的电梯,实现也比较简单。因此,之后的作业我也延续使用了此运行策略,故在之后不再做说明。

(2)第二次作业:

        第二次作业和第一次作业相比,我多增加了一个ElevatorStatus类,也就是同步记录电梯运行状态的类。这个类的设置是和我的调度策略有关的——影子电梯。

        影子电梯是相当于模拟电梯之后运行的情况,总的来说,就是模拟ElevatorOperation的运行,直到电梯完成使命,计算所花费的时间。

        而我的策略就是借助影子电梯实现的,比如新来了一个PersonRequest请求,我会尝试将其放入每个电梯中(当然是深克隆的电梯,以免对原电梯造成影响),得出其运行时间,选择出花费时间最小的电梯,该电梯就是最终拿走这个请求的电梯。其在assignment中的具体实现如下:

    private int choseElevator(PersonRequest personRequest) {
        ArrayList<Double> sumTimes = new ArrayList<>();
        for (int i = 0;i < 6;i++) {
            if (isRests.get(i).get() != 0) {
                sumTimes.add(10000.0);
                continue;
            }
            sumTimes.add(elevatorStatuses.get(i).cloneOne().simulationRun(personRequest));
            //System.out.println(i + " " + sumTimes.get(i));
        }
        int minId = 0;
        double minTime = sumTimes.get(0);
        for (int i = 1;i < sumTimes.size();i++) {
            if (minTime > sumTimes.get(i)) {
                minTime = sumTimes.get(i);
                minId = i;
            }
        }
        //System.out.println("give "+personRequest.getPersonId() +"to " + minId);
        return minId;
    }

        其他的架构改变就没什么特别大的了,其UML类图如下图,可以看出确实是只添加了一个ElevatorStatus类,用于“投影”电梯的状态:

(3)第三次作业:

        从上图可以看出,我新增了OccupiedKey类以及DouElevatorA和B两个双箱电梯类。其实这三个类都与双轿厢电梯的实现有关。那么事实上架构也是直接在原来的架构上新增了随时可以替换普通电梯的双轿厢系统类而已,其UML类图如下所示:

2.各进程协作关系

        各进程的协作关系我就用自己画的图来表示。如下:

        从数据流通图中可以看出,我的分配策略是“分配了就全权交给此电梯负责”,这样实现的目的是保证不会发生数据的循环流通,否则一旦操作不当会导致死锁,线程无法正常结束。这种实现方法也是属于“偷懒”了,但事实上这其实是我无法解决死锁问题的无奈之举(debug了一天还是无法保证不发生死锁)。

        其次,可以看出我的设计中比较稳定的内容就是数据流通在进入电梯之前的数据流通的单向性。易变的内容就有很多了,包括各种电梯都可以实现,什么三轿厢、四轿厢、各轿厢协作都可以实现,还有多目的地乘客也可以实现,以及水平移动也是需要设计不相撞即可……

        最后,介绍一下我的双轿厢分配的调度策略,我的调度策略是影子电梯+轮流分配。其实我考虑过对双轿厢的模拟运行,但是由于双轿厢的运行是需要两个轿厢协作的,那么就不知道某个请求什么时候能够到达转移层,这样就难以模拟,同时考虑我的分配策略一旦分配就无法回退,我觉得影子电+轮流分配是最合适的策略。简单介绍一下就是设置一个count数,对于一个请求,查找电梯号为count%6 + 1的电梯类型,如果是双轿厢,直接分配给它,如果是单轿厢电梯,则对剩下的所有电梯进行模拟,找出运行结束时间最短的电梯。

三、保证双轿厢电梯不相撞的实现:

        我的实现是基于上面新增的OccupiedKey类,其中的内容如下:

public class OccupiedKey {
    private int occupied;
    private int aeleEnd = 0;
    private int beleEnd = 0;

        这个类中只有occpied是用于保证不相撞的,aeleEnd和beleEnd都是用来实现电梯正常结束的,并且类中所有方法都上了锁,且对这个类的访问都保证只经过其内实现的方法。那么保证不相撞的逻辑代码如下:

    //询问并获取锁
    private void doMove() {
        long waitTime = 0;
        if (nowPosition == transferFloor - 1 && runWay == 1) {
            while (occupiedKey.checkIsOccupied()) {
                try {
                    sleep(10);
                    waitTime = waitTime + 10;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            occupiedKey.setOccupied(1);
        }
    //完成任务后释放锁
    if (nowPosition == transferFloor && runWay == -1) {
            try {
                sleep((long) (runTime * 1000));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            nowPosition--;
            eleDown();
            occupiedKey.setOccupied(0);
            return;
        }

        在电梯执行move行动前,若是在换层层下一层(对于A类电梯来说是这样的,B类则是换层层上一层),则询问换层层是否被占领,若是,则睡眠10时间单位,再次询问,直到未被占领,然后宣布我已经占领,当然需要在执行完成后释放锁。从中可以看出我每次都计时了我询问等待的时间,这实际上是为了实现讨论区提出的——当我们发现B类电梯离开时,事实上B电梯已经走了它换层的时间,若是我已经询问超过了我需要换层的时间,那么在B离开时,我可以立即设置A电梯到达换层层,实现了效率最大化。

四、Bug&Debug

        在实现的时候确实bug和debug是一件让人十分头疼的问题。而我遇到的bug有很多中,首先是实现时的逻辑实现错误,这个就没什么必要展开说了。

1.CPU-TLE:

        接着是“CPU-TLE”问题,有时候这种bug真的是防不胜防,因为你如果没有考虑到的话,你做的所有本地测试都是难以发现的,而我的第三次作业就是因为没有考虑这个,导致强测错了许多点。

        这种bug其实也是比较好发现的,那就是“print”大法,把线程的每一次询问都打印出来,如果发现出现一长串的输出信号,那么就可以肯定发生了轮询,此时针对性去修补即可(一般是使用wait()和sleep()即可)。

2.Run-TLE:

        然后另一个比较头疼的就是RTLE了。我遇见的RTLE一般都是分配策略出错或者分配策略不够高效。这个一般就需要去查找分配的策略是否符合自己的设想,以及去优化分配策略了。这个一般也是需要把相关信息打印出来。如下:

    private int choseElevator(PersonRequest personRequest) {
        int eleId = count % 6;
        count++;
        if (isRests.get(eleId).get() < 2) {
            ArrayList<Double> sumTimes = new ArrayList<>();
            for (int i = 0;i < 6;i++) {
                if (isRests.get(i).get() == 0) {
                    sumTimes.add(elevatorStatuses.get(i).cloneOne().simulationRun(personRequest));
                } else if (isRests.get(i).get() == 1) {
                    sumTimes.add(elevatorStatuses.get(i).cloneOne().
                            simulationRun(personRequest) + 1.2);
                } else {
                    sumTimes.add(100000.0);
                }
                //System.out.println(i + " " + sumTimes.get(i));
            }
            int minId = 0;
            double minTime = sumTimes.get(0);
            for (int i = 1;i < sumTimes.size();i++) {
                if (minTime > sumTimes.get(i)) {
                    minTime = sumTimes.get(i);
                    minId = i;
                }
            }
            eleId = minId;

        }
        //System.out.println("give "+personRequest.getPersonId() +"to " + eleId);
        return eleId;
    }

        其中的注释代码就是我需要打印出的东西的代码实现。我把每次影子电梯分配时,每个电梯的运行结束时间打印出,就可以检查是否实现成功。

五、体会与感悟

1.一般体会:

        这次单元总体来说是比较难的,因为是多进程设计,那么调试和保证进程安全就是非常需要花费心思的了。但是我觉得我还是收获了很多东西的。

        首先我认为多进程是非常高效的,也是非常有熟练掌握的必要的。它可以让多个进程同时进行,可以加快代码实现,也可以非常高效地模拟多个事物基于时间的交互情况。

        其次,这部分内容和OS的进程同步与互斥高度重合,因此在两个课程的学习过程中可以优势互补,相互补充和帮助理解(当然,也会受到影响,比如之前的很多地方我都用了线程二字,因为我的理解是,IDEA里应该是用户级进程,所以应该是多个线程交互)

        最后,事实上这部分内容的学习,也让我更好更全面地理解了面向对象编程的含义。所谓的“面向进程编程”难道不也是特殊的“面向对象编程”吗,只不过我的对象的“move”是真的在“move”罢了。进程,也是一个在“运动”的,行为和“时间高度相关”的对象而已。     

2.线程安全体会 :

        关于线程安全的体会,我的第一感觉是它是实现多线程程序的重中之重。因为没有线程安全的保证。那么你的多线程程序可以说是飘摇动荡的楼阁,随时会倒塌。

        当然,实现保证线程安全有非常多的方式,可以说大家也是为了保证线程安全问题而给出了许多高效的工具。除了我最开始提出的那些,还有包括读写锁等待高效有用的方式,当然由于我的进程交互多是二者之间,且多是写操作,故没有采纳读写锁等其他方法。

3.层次化设计体会:

        关于层次化设计,我觉得在我的代码中的体现是比较明显的。观察我给出的进程交互图也可以看出些许端倪。我的层次化设计高度概括就是三类:处理输入层、请求分配层、处理请求层。对应具体进程就是:InputThread、Assignment、Elevator(包括各种各个电梯进程)。层次化设计可以保证我们的代码结构清晰,这样实现起来逐层实现即可,比较方便。同时也利于Debug时,可以把问题聚焦到具体的层次进行修改。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值