BUAA_OO_2021_ 第二单元 - 难度巅峰之多线程电梯

BUAA_OO_2021_ 第二单元 - 难度巅峰之多线程电梯

写在前面

早就耳闻了面向对象课程第二单元的难度,在面临一个全新的领域——多线程时,或多或少都会手足无措吧。对于一个普普统统的计算机专业的学生来讲,没有大佬们对于代码强大的理解与拓展能力,只能看着入门教程一点点自学,十分痛苦。多亏了廖雪峰老师网站的java多线程入门,我才对多线程思想有了些许体会。但俗话说“师傅领进门,修行在我的”,对于这个单元的做业,最最重要也是最最基础的就是 wait()notifyAll() 的使用来对线程进行调度;另外一个最最重要也是最最基础的就是 synchronized() ,对于锁的使用仍是十分抽象的。但通过了这三次做业的摸索,我对这两者有了一个较深的理解。java

oo上机时的实验代码也给我带来了非非很是大的帮助(虽而后来被锤有bug)上课时老师给的多线程的例子不免很是简单, 而实验代码清晰的架构以及对多线程的操做的示例都给我前两次做业带来了很大的启示。若是学弟学妹们无心中刷到了这篇博客,听学长一句劝:不要死磕本身丑到爆的架构,在入门的时候借鉴借鉴优秀代码,会事半功倍。算法

1、同步块的设置与锁的选择

三次做业使用的同步锁是synchronized() ,而未使用Lock编程

synchronized()可修饰的对象有:安全

  • 修饰一个代码块:被修饰的代码块称为语句同步块,其做用范围是使用大括号{}括起来的代码,做用的对象是synchronized(object) 中的object。
  • 修饰一个方法:被修饰的方法称为同步方法,其做用范围是整个方法,做用的对象是调用这个方法的对象。
  • 修饰一个静态方法:做用范围是整个静态方法,做用对象是这个类的全部对象。
  • 修饰一个类:做用范围是整个类中全部方法,做用对象是这个类的全部对象。

三次做业的同步块统一采用的是使用 synchronized() 锁住代码块,而非给方法加锁,更没有给类加锁。一是为了性能考虑,毕竟加锁的代码块越小越好,二是我想锻炼一下本身对于synchronized() 的理解,给小块代码加锁,虽然对于互斥问题的解决更加复杂,但这也正是多线程编程的精髓。多线程

整体而言,这三次中都存在的电梯安全问题是输入线程和电梯线程对于等待队列的读写操做。架构

在三次做业的电梯类中,我采用的是Look算法进行调度,所以在每次有新请求进入电梯后,都要对要去的最高(最低楼层)进行更新,但整个过程都是对于本部电梯内部属性的操做,并无致使线程冲突。只有在第七次做业中加入了换乘操做,才致使了不一样电梯之间的线程安全问题。dom

下面介绍具体到每一次做业中的呈现形式:性能

第五次做业

本次做业架构比较简单,只有一部电梯所以共享对象并很少。测试

共享对象有:waitQueue(等待队列)优化

waitQueue的同步问题主要涉及如下几点:

  • inputThread线程在输入后须要将需求 写入 waitQueue,并唤醒电梯

    synchronized (waitQueue) {
                    if (request == null) {
                        waitQueue.close();
                        waitQueue.notifyAll();
                        try {
                            elevatorInput.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        return;
                    } else {
                        waitQueue.addRequest(request);
                        waitQueue.notifyAll();
                    }
                }
  • elevator线程须要读取 waitQueue中是否有人,若没人则须要wait()

    synchronized (waitQueue) {
                    if (elevatorRequests.isEmpty() && waitQueue.noWaiting() && waitQueue.isEnd()) {
                        return;
                    }
                    if (waitQueue.noWaiting()) {
                        try {
                            waitQueue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
  • elevator线程须要读取 waitQueue在某一层的人,若是有知足上电梯条件的请求,则还须要将请求写出 waitQueue

    因为对于waitQueue的读写操做涉及算法核心,所以不附代码

第六次做业

本次做业增长了多部电梯,所以增长了Scheduler调度器类,来对各个请求进行分配,架构与前一次单部电梯相比复杂了很多,也形成了更多的线程安全问题。

共享对象有:totalQueue(请求总队列),waitQueue(每部电梯单独的等待队列)

全部同步问题如图:

image

从图中可知各个线程对于总等待队列和各个电梯的等待队列的线程安全冲突问题,所以在每次读写操做时,都要使用synchronized() 解决同步问题。如:

//Scheduler调度器线程中:
synchronized (totalQueue) {
                if (totalQueue.isEnd() && totalQueue.noWaiting()) {
                    for (Elevator elevator : elevators) {
                        WaitQueue waitQueue = elevator.getWaitQueue();
                        synchronized (waitQueue) {
                            waitQueue.notifyAll();
                        }
                    }
                    return;
                }
                if (totalQueue.noWaiting()) {
                    try {
                        totalQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } //解除总等待队列的锁,能够继续进行输入
//Elevator电梯线程中:
synchronized (waitQueue) {
                if (elevatorRequests.isEmpty() && waitQueue.noWaiting()
                        && totalQueue.isEnd() && totalQueue.noWaiting()) {
                    return;
                }
                if (waitQueue.noWaiting() || (type == 1 && !totalQueue.isEnd())) {
                    try {
                        direction = 0;
                        waitQueue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                direction = waitQueue.getRequest(0).getToFloor()
                        - waitQueue.getRequest(0).getFromFloor();
            }//解除电梯等待队列的锁,调度器能够继续进行分配

第七次做业

本次做业增长了电梯的种类属性,为了优化性能,所以增长了换乘策略,形成了不一样电梯间互相改变等待队列的线程安全问题。

共享对象有:totalQueue(请求总队列),waitQueue(每部电梯单独的等待队列)

全部同步问题如图:

image

从图中能够看出,因为增长了A类电梯和B类电梯之间的换乘策略,致使了在不一样电梯之间也会产生线程安全问题,所以除了上文给出的第六次做业中的代码外,还会出现如下形式的同步块与锁的设置:

//换乘操做
int toFloor = request.getToFloor();
int fromFloor = floor;
int id = request.getPersonId();
PersonRequest newRequest = new PersonRequest(fromFloor, toFloor, id);
elevatorRequests.remove(request);
TimableOutput.println("OUT-" + request.getPersonId() + "-" + floor + "-" + idNumber);
i--;
synchronized (totalQueue) {
	totalQueue.getRequests().add(0, newRequest);
	totalQueue.notifyAll();
}

2、调度器设计

第五次做业

“就一部电梯,调度器有啥用呢?”

开始作第五次做业以前,我就是这样想的。因而把已经建好的Scheduler类默默删掉了。

事实也是这样,一部电梯的确不须要调度,而此次做业的核心应当是对于单部电梯调度算法的设计,好比指导书给出的als算法,以及最经常使用,性能也不错,容易实现的look算法(这三次做业的Random模式使用的都是look算法,真香!)而调度器这东西,虽然知道在后面必定会再加上,也知道在第一次添上调度器的话有利于以后做业的拓展,但俗话说:“一口吃不成胖子。”我知道本身水平咋样,就不如刚刚接触多线程的时候,整个简单点的架构吧~

因而第五次做业并无调度器,全部输入的请求直接进入等待队列,等待电梯的临幸。

下面介绍一下三种模式下电梯的运行策略:

  • Night

Night 模式的两大特色:

  • 目的楼层肯定(都是一楼)
  • 全部乘客同时到达

所以能够将其看做静态请求。对于处理静态请求,总会有一个最优解,而我采起的策略是

电梯从高处往低处接,每次接六个,若是不足六个则接完就结束

这个策略十分简单,性能也还不错,然而对于Night 模式仍是有别的最优解的,只是代码会复杂不少,而在我能力范围以内,这种策略的性价比很高。

  • Morning

Morning模式的最大问题是,人并非一口气都到一楼,而是动态添加的。

因为是动态模型,不一样人的算法可能会形成很大的性能差别,而我采起了一种比较稳妥地方法:

等人

只有当来了六我的,电梯满员时,才会发车,不然就一直等着,除非识别到已经中止输入了。这种方法带来的一个玄学问题是:没法保证在等人的过程当中是否是有时间可以把电梯上的人先送上去。但若是要实现这样的机制的话,代码量应该是十分惊人的,我也就没有再深究。

  • Random

我采用了传统电梯搭载的look 算法,并在其基础上进行了些许优化。其运行策略以下:

一、获取等待队列中全部上行请求的最低层 和全部下行请求的最高层 ,并判断电梯当前所处位置离谁更近,而后去往更近的那个楼层,并改变电梯的运行方向次序。

二、电梯按照选择好的运行次序运行,并把目标楼层设定为电梯中全部请求的目的楼层的最高层。

三、电梯在每一层进行遍历:首先遍历电梯中的请求有没有目的楼层是该楼层的,并处理出电梯的请求;而后遍历等待队列中有没有前往楼层方向与电梯运行方向相同的,并在该楼层上电梯的请求,如有,则上电梯;若请求的目的楼层要高于目前电梯运行要去的最高楼层,则将电梯的最高楼层属性更新。

四、当电梯到达目标楼层(即全部请求的最高楼层或最低楼层)后,电梯掉头,重复上述行为。

这种算法与平常生活中的电梯十分类似,也很好理解,通过实践,性能也说得过去。

第六次做业

第五次做业欠下的债,迟早都要还的。

第六次做业要把请求们按照必定的策略分给多部电梯了,所以须要设计调度器以及调度器算法,来实现请求的调度。

调度器的遇到的难点以下:

​ 一、如何制定合理的分人策略,是影响电梯性能的最关键因素

​ 二、如何处理好调度器线程与电梯线程、调度器线程与输入线程之间的矛盾,是电梯可否顺利运行的关键因素

制定的分人策略以下:

  • Night

Night模式的分配策略比较容易想到,毕竟是静态请求。

​ 一、将总请求队列中的人从高往低排序

​ 二、按照每六个为一组,轮流分给各部电梯

这样作也会遇到些许问题,好比没法统一处理高层请求,对于人数较少的状况没法调动全部电梯。但我认为这些问题致使的性能差别微乎其微,便没有再进行优化。

  • Morning

实在没有想出完美的解决办法,因而采起将请求均分给全部电梯的调度策略。

​ 一、每次提取出总等待队列的第一个请求,进行处理。

​ 二、遍历全部的电梯的等待队列,选择等待请求最少的电梯

​ 三、将提取出的请求分配给所选择的电梯

​ 四、重复上述操做,直到总请求队列为空

这样的策略有显而易见的性能问题,好比每部电梯为了等够6我的,等待的时间特别长。

  • Random

Random模式的调度策略比较复杂,我整体按照的是“顺路原则”进行分配。具体分配过程如图:

image

虽然并不能保证这样的算法是最优的,但能够保证顺路原则的的确确提升了不少性能。

第七次做业

换乘!换乘!换乘!

随着换乘算法的加入,第二单元做业难度达到了高潮。

难道非换乘不可吗?固然不是,最简单的调度方法即是:

​ 一、全部符合高楼层条件的请求一概给C类电梯

​ 二、全部奇数层到奇数层的请求一概给B类电梯

​ 三、其它全部请求一概给A类电梯

这样,第七次做业的调度器就更改完毕啦!

我按照这样的调度策略,写了第七次做业的原始版本,并获得了不换乘的性能时间,而后便开始设计换乘调度策略。

​ 一、全部符合C类电梯的请求,直接分配给请求最少的C类电梯,不换乘

​ 二、全部奇数层到奇数层的请求,直接分配给请求最少的B类电梯,不换乘

​ 三、全部偶数层到偶数层的请求,直接分配给请求最少的A类电梯,不换乘

​ 四、全部偶数层到奇数层的请求,先分配给A类电梯,运送到最邻近的奇数楼层后,换乘给B类电梯

​ 五、全部奇数层到偶数层的请求,先分配给B类电梯,运送到最邻近目的地的偶数楼层后,换乘给A类电梯

哈哈,换乘也不过如此嘛!

真的不过如此吗?

不是!且不说这种换成策略的性能问题,单看整个换乘过程,明明是为了换乘而换乘啊!我提交了“优化后”的这个版本,果真不出我所料,这个版本的性能比不优化的原始版本还要差...没错,个人换乘策略是负优化。又考虑到第一单元就是因为各类优化算法才致使了强测出锅,我获得了一个结论:若是能力不足,谨慎优化!

3、从功能设计与性能设计的平衡方面,分析和总结本身第三次做业架构设计的可扩展性

第七次做业UML图

image

第七次做业UML协做图

image

本次的架构设计为“消费者——生产者模型”,生产者为InputThread 线程,消费者为Elevator 线程,并不复杂。

关于可扩展性,在这样的架构之下,分配调度算法与运行算法分离,必定程度上提高了可扩展性,但总的来讲,可扩展性仍是不高的,毕竟在写程序的过程当中有着“反正写完这三次就没了”的内心,没有太多考虑程序的可扩展性。

4、分析本身程序的BUG

第五次做业

未发现本身程序bug,也未被hack,而且代码提交一次经过,没进行debug。(因此下次做业就飘了

第六次做业

BUG警告! 本次做业强测稍炸,出现的锅是因为morning算法的进程结束问题,致使4个morning模式的测试样例线程没能正常结束,从而致使rtle。这个bug是我测试的时候样例过弱致使的,很明显也很容易de的一个bug,居然被放生到强测中,真的不该该。

第七次做业

未发现本身程序bug,也未被hack,但在写程序中出现过轮询致使的tle的bug,缘由是wait() 方法调用条件有误形成程序没有正常进行等待。

在debug过程当中,的确出现过死锁的问题。出现了锁的嵌套,而且同步块过大,就十分有可能出现死锁。并且有的死锁居然很难复现,这给个人debug形成了必定的困扰。好在我及时定位到了bug,并绞尽脑汁地把嵌套的锁拆开,不惜牺牲一些性能,也要保证线程安全。

5、分析发现别人程序bug所采用的策略

因为本单元做业的输入是随机投放的,这就致使只要不是C组,想要hack别人,就只能借助评测机了。多是因为评测机生成的样例太弱,这三次做业都没能成功hack别人。

我也曾经尝试过肉眼debug,但因为别人的代码架构和个人代码或多或少有些区别,并且也没有注释,致使肉眼debug变得十分困难乃至不可能完成。我也着重查看了锁嵌套的状况,并思考有没有死锁状况的发生,但同窗们的代码都很强,并无发现有什么异常。

遥想上个单元,三次做业都没使用评测机,只是经过本身的火眼金睛,即便在A组,都能成功hack别人,但在多线程这个单元,肉眼不太管用了,甚至评测机的效率都不过高了...

6、心得体会

  • 线程安全问题是多线程编程最重要的问题之一,线程不安全的多线程是没有意义的。固然,若是把全部的类都用synchronized() 锁起来,线程也就百分百安全了,但也就由多线程退化成单线程了...如何在尽量优化性能的前提下保证线程安全,是一件值得琢磨的事,这也许就是多线程的魅力吧。
  • 层次化设计在多线程中是必须的,毕竟要把每个层次抽象出来,每个线程也都分工明确。一个好的架构对于多线程而言是前提,多学几个架构也没啥坏处。但把模板架构与具体任务结合起来的时候,就要花一点心思了。
  • 本单元做业又跟上一单元同样,在第二次做业翻车,确定是有点遗憾的,不过还有两个单元呢,但愿以后的单元别再翻车了呜呜呜。
相关文章
相关标签/搜索