BUAA-OO-unit2总结

寒月倚南枝 学生 2024-04-19 09:52:25

同步块和锁的选择

同步共享对象

在我的程序中,同步块是以方法为单位的,给方法加锁的本质还是给对应的this对象加锁,按照生产——消费者模型来理解

对于person类请求,流程是 输入线程-总表-调度器,总的请求表是一个共享对象,输入线程是生产者,将请求放入共享盘子当中;调度器是消费者,将盘子里的请求取出。之后经过流程 调度器-各个电梯分表-电梯,此时调度器是生产者,电梯分表是共享对象,电梯是消费者

对于reset请求,流程是 输入线程-各个电梯分表-电梯,输入线程直接充当生产者,各个电梯的请求表是共享对象,电梯是消费者

当涉及轿厢的操作时,请求的流程为 输入线程-总表-调度器-电梯分表-轿厢分表 后一部分流程当中,电梯分表相当于轿厢总表,将每个请求加入对应的轿厢表中,生产-消费者关系与之前类似

锁的获取与释放

锁的获取与释放体现在生产者和消费者如何有序地出入共享空间,主要是通过几个加锁的方法来实现的

addRequest addRequestOfSchedule addRequestOfCar
这三个方法是用来添加请求的,其中addRequestOfSchedule 方法被输入线程调用,没有输出,将请求加入总表;addRequest 是添加请求的主要方法,当该表不是双轿厢时,将请求加入电梯表,已经被重置为双轿厢时,根据请求信息,调用 addRequestOfCar 加入对应的轿厢表,这两种加入都需要输出对应的 RECEIVE 信息

addReset 用来让输入线程直接获取电梯表的锁,没有输出

最重要的是所有的添加方法之后都会有唤醒notifyall操作,来唤醒等待该对象锁的线程,并在方法调用结束之后释放锁

    public synchronized void addRequestOfSchedule(MyRequest myRequest) {
        myRequests.add(myRequest);
        notifyAll();
    }

    public synchronized void addRequest(MyRequest myRequest,
                                        RequestQueue queueA,
                                        RequestQueue queueB, int id) {
        MyPersonRequest myPersonRequest = (MyPersonRequest) myRequest;
        if (!isDoubleCar) {
            myRequests.add(myRequest);
            TimableOutput.println("RECEIVE-" + myPersonRequest.getId()
                    + "-" + id);
        } else {
            if (myPersonRequest.getFrom() < transFloor) {
                if (myPersonRequest.getTo() > transFloor) {
                    sumOfPersonNeedTrans++;
                }
                queueA.addRequestOfCar(myRequest, id, "A");
            } else if (myPersonRequest.getFrom() > transFloor) {
                if (myPersonRequest.getTo() < transFloor) {
                    sumOfPersonNeedTrans++;
                }
                queueB.addRequestOfCar(myRequest, id, "B");
            } else if (myPersonRequest.haveSameDir(true)) {
                queueB.addRequestOfCar(myRequest, id, "B");
            } else {
                queueA.addRequestOfCar(myRequest, id, "A");
            }
        }
        notifyAll();
    }

    public synchronized void addRequestOfCar(MyRequest myRequest, int id, String name) {
        myRequests.add(myRequest);
        TimableOutput.println("RECEIVE-" + ((MyPersonRequest) myRequest).getId()
                + "-" + id + "-" + name);
        notifyAll();
    }
    
    public synchronized void addReset(MyReset myReset) {
        myResets.add(myReset);
        notifyAll();
    }

getOneRequestAndRemove tryGetOneRequestAndRemove carTryGetOneRequestAndRemove这三个get方法是用来获取请求的,依次被调度器、电梯和轿厢所调用,核心逻辑就是当共享对象中没有请求时候,让当前访问共享对象的线程进行等待wait,直接释放共享对象的锁,并等待被notifyall唤醒

        if (myRequests.isEmpty() && !isEnd) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (myRequests.isEmpty()) {
            return null;
        }

而当该线程被唤醒后,会重新从wait处开始执行,当发现共享对象仍然没有请求时,会返回null来让该线程结束

架构迭代分析

第一次作业

万事开头难,第一次作业第一次接触到多线程和电梯算法,一头雾水,进展比较困难那,最后参考了实验课代码,采用了 输入线程-调度器-电梯 的层次化架构设计,并参考往届学长代码采用了Look算法,但是由于对线程安全的知识并不熟悉,认为只读的方法不需要加锁,导致了互测出现了bug,原因读的过程中其他线程修改了共享变量

第一次作业不涉及调度策略,调度器只起到分配作用

第二次作业

第二次作业主要涉及reset的处理以及receive的输出

由于对多线程掌握不足,因此调度策略采用了随机调度,并且重置换乘时,每个电梯会重新receive之前的乘客,闭环处理,性能效果还不错

对于reset,起初是让调度器统一进行分配的,但是为了让电梯快速的响应重置请求,最终选择在输入线程中直接将reset类请求进行分配,这样可以有效增加重置请求的响应效率,并且会减轻调度器的负担,让架构更加清晰

对于receive的输出,选择在addRequest的时候进行输出,需要处理好receive和reset的时间前后关系

第二次作业由于使用了随机,工作量并不大

第三次作业

第三次作业的调整有点难度,关键在于如何定义轿厢,如果只是在电梯当中维护两个轿厢,之后每次只动一个,实现难度虽然大大减小,但是实现方法并不优雅,因此,按照正常的思路,我将两个轿厢定义为两个线程,提高了效率,不过这样就需要处理好多个线程之间的交互,并且还需要考虑轿厢请求的接收,共享楼层的处理以及轿厢之间的冲突问题

第三次作业难度小小上升,不过整体思路并未有太大变化

迭代更新

三次迭代中,稳定不变的是整个请求响应流程的层次化设计,其他诸如电梯的运行算法、调度器的调度算法以及请求的种类都是可以随时更改的,相应的接口较为灵活,属于可以增量开发的地方

假如有另一类请求需要响应,首先增加他的响应流程,然后在响应的时候增加对应的响应效果,最后协调好各个层次的关系即可实现拓展开发

线程之间的协作关系

线程协作UML图如下

img

可以看到一共有四种线程,分别为 输入线程 InputThread 调度器线程 Schedule 电梯线程Elevator 以及 轿厢线程 Car

输入线程会接受所有的电梯表和调度器表,将标准输入处理成一个个请求类型添加到相应的请求表中,读取到结束符之后,给调度器表发送结束信号并退出线程

    private final RequestQueue waitQueue;
    private final ArrayList<RequestQueue> elevatorQueues;

调度器线程接受所有的轿厢表和所有的电梯表以及调度器自己的表,与输入线程在调度器表中进行交互,将请求加入到相应的电梯或轿厢表当中,当请求分配结束并且已经结束时,给每个电梯和轿厢表都发送结束信号并退出线程

    private final RequestQueue waitQueue;
    private final ArrayList<RequestQueue> elevatorQueues;
    private final ArrayList<ArrayList<RequestQueue>> carQueues;

电梯线程接受一个属于自己的电梯表,另外两个是对应的轿厢表,电梯线程会和调度器以及输入线程通过电梯表进行交互,不断响应其中的请求。当请求处理完成并且电梯表结束时,退出线程;处理第二类reset请求时会启动相应的A B 轿厢线程,并将请求进行同步,之后特判退出电梯线程

    private RequestQueue elevatorQueue;
    private RequestQueue queueA;
    private RequestQueue queueB;

轿厢线程只有当对应的电梯被第二类reset后才会启动,A B 轿厢会接受二者各自的轿厢表,以及共同隶属的电梯表,当改装成双轿厢之后,仍然需要从调度器中分配请求,此时将会经过电梯表,维护该电梯槽需要换乘的乘客数量,并将对应的请求合理分配给A B轿厢,换乘时,A B 轿厢通过调用电梯表的方法和另一个轿厢进行交互,并在 自身请求结束并且无换乘人员 时结束线程

    private RequestQueue elevatorQueue;
    private RequestQueue anotherQueue;
    private RequestQueue bothQueue;
    //////
    if (!bothQueue.havePersonNeedTrans()) {
                return "over";
            }

只要确保线程之间满足生产消费者模型,基本不会产生线程安全问题

两个轿厢如何不碰撞

不同轿厢之间共享换乘楼层对象

Car轿厢在我的程序中是一个线程,每个电梯线程在响应第二类请求时会开启两个新的轿厢线程,两个轿厢线程都有各自对应的请求表。运行的逻辑将分为两部分 一是处理运行范围可以处理的请求,另一部分是处理换乘的乘客

BetweenCar的意义是两轿厢之间的共同楼层,通过 synchronized 关键字进行加锁,并维护一个 canUse 变量,保证每次只有一个轿厢能够访问该楼层,没有使用权的轿厢需要原地等待另一轿厢使用结束后才能进行使用

public class BetweenCar {
    private int transFloor;
    private boolean canUse;

    public BetweenCar(int transFloor) {
        this.transFloor = transFloor;
        this.canUse = true;
    }

    public synchronized void tryToUse() {
        if (!canUse) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        canUse = false;
    }

    public synchronized void setCanUse(boolean canUse) {
        this.canUse = canUse;
        notifyAll();
    }
}

bug分析

第五次互测中出现了bug,原因是对请求表中一些只读方法没有加锁,让我明白了不只是修改共享变量的方法需要加锁,只读的方法也需要加锁,最基础的要求是要满足读写锁的逻辑定义,在读的时候不能进行写锁的获取,否则就会造成 ConcurrentModificationException错误

第六次第七次作业中都未出现bug,但是在双轿厢电梯的实现过程中,由于线程数量增加以及线程互动更加频繁,出现了一些多线程特性的bug,比如程序无法终止是由于各个线程之间没有满足正确的生产-消费者逻辑关系,线程间形成了闭环,造成了死锁

在线程的结束方面,由于轿厢线程的特殊性,需要特别修改其终止条件,避免其不能正确终止;在电梯第开启轿厢线程之后,电梯线程应当立即结束,需要进行特判

心得体会

第二单元让我体会到了多线程的力量,对线程安全的重要性有了深刻的认识,并且对层次化设计有了更深的理解

在不考虑效率的情况下,线程安全当然可以通过整体加锁来很好地解决,但是对方法的加锁会锁住整个对象,这种牵一发而动全身的方法可能会在安全方面表现出色,但是在性能方面往往乏善可陈,这时就需要深入理解加锁和共享的本质,尝试对特定共享成员变量或者特定代码段进行加锁,争取在保证线程安全的情况下,有效提高共享资源的访问效率,从而合理协调不同线程的工作

层次化设计方面,这次使用了很多生产消费者模型,实现了 输入线程-调度器-电梯 的层次化设计,并且不同生产消费者之间使用请求表完成共享托盘的工作,这样也可以更好地分析不同线程的关系,合理协调线程任务

这一单元相比第一单元进步明显,希望下一单元能够学习到更多东西

...全文
67 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧