BUAA_OO 第二单元总结 多线程

BUAA_OO 第二单元总结 多线程

一、unit2总览——多线程

本单元的主要内容是简单了解Java多线程设计模式,并且完成三次电梯作业的迭代。

然而实际上我们这个单元作业需要用到的多线程知识很有限(只需要会用synchronized加锁并且了解几个简单的设计模式就能完成)

(一)多线程

为什么多线程

在这里首先谈谈我对多线程的理解:

我认为多线程的本质目的是为了利用空闲的cpu

就拿我们的作业举例,因为电梯移动一层需要0.4s,开关门也需要等待,这个时候cpu是空闲的,因此我们就可以把它利用起来去控制其他的电梯。多线程之所以应用如此广泛是因为我们日常使用的软件大都不是计算密集型的,大多数软件都需要和用户进行交互,而用户的反应速度比较慢,因此CPU就可以趁机去干别的事情。

采用多线程不是因为我们要控制多个电梯,而是因为电梯的移动很慢

我在决定是否要把一个类作为一个线程的时候会判断这个类的方法中有没有需要消耗大量时间等待的,如果有,就可以考虑把它作为一个线程。例如处理输入的线程,电梯线程,如果安排了调度器,调度器也应该作为一个线程,因为调度器要等待获取输入,然后在适宜的时机把请求发送给电梯。那我在第三次作业中的请求分发器为什么不是一个线程呢?因为每获取到一个输入的请求我都会调用一次请求分发器中的方法,把这个请求发出去,这里不涉及等待。

多线程的安全问题

多线程的安全问题通过加锁来解决,就是限制某一段代码作为一个整体不能被打断。

不安全出现的原因是多个线程同时访问一个共享对象,那么我们在写程序的过程中应该弄清楚那些对象是被哪些线程共享的,在可能会同时访问的地方加锁。

(二)关于设计模式

其实就是因为多线程编程太复杂了,因此人们把之前的经验总结为一些设计模式,我觉得这就像是一些模板,以后遇到类似的问题的时候,直接套模板,使用适当的设计模式就有可能设计的更加漂亮优雅。

这一单元我了解到或者使用过的设计模式主要有这几种:

  • 单例模式

    其实我感觉很多情况下,单例模式和写一个静态类效果差不多,那么什么时候用单例模式比较好,什么时候用静态类比较好?

    嗯。。。对这个问题我也没有深刻的理解,网上查到的答案我总感觉没有什么说服力。

    在这几次作业中我需要这种感觉的类有OutputClass ,ReqSender,RequestCounter,其中输出类我直接写了一个静态方法,另外两个写的是单例模式,给我的感觉是静态类写起来比较方便。

  • 工厂模式

    工厂模式就是为了把创建对象的逻辑封装起来,一般的话,如果只有一种对象,直接new就好了啊,但是如果你还得先判断要创建的是什么对象(比如横向电梯还是纵向电梯),然后根据这个不同传入不同的参数,这样光是对象的构造就得写一大坨。使用工厂模式让对象的构造更清晰。

  • 生产者消费者

    感觉这是一个很自然的模式,生产者和消费者共享一个队列,这个生产者向队列中放东西,消费者从队列中取东西,只要保证这个队列的add和get方法互斥就可以了。

  • 观察者模式

    简单来说,就是一个一对多的关系,一个对象的状态改变后通知其他对象。

    训练代码中有出现关于这个设计模式的,但是我没有发现这个在我们的作业中有什么用处,勉强的说,可能就是“输入线程向侯乘表中加入请求后唤醒了对应的电梯”这个逻辑和观察者模式有关吧。

二、问题重述及实现

hw5

(一)问题重述

第五次作业要求比较少:

模拟分布于五个楼座的五个电梯,电梯有一定的运行速度,在每个楼层都可以开关门以及上下乘客,我们的任务是模拟电梯合乎逻辑的运行并且按照时间顺序输出开关门以及上下乘客等信息。

这次作业五个电梯之间其实没有什么关系,设计线程协作的地方仅仅是输入线程和电梯共同对于侯乘表WaitTable的处理。

(二)实现

策略

电梯调度采用LOOK策略,流程图如下:

graph TD e(电梯)---a1 e---a2 e---a3 a1[电梯上行 有乘客] --> b1[ 最远的乘客要去哪里就去哪里] a2[电梯下行 有乘客]-->b1 a3[电梯空-假设电梯在上行]-->b3[向Schedule询问]-->|Schedule给出下一个应该去的楼层|c1 c1(Schedule)-->c2[队列空 输入完毕]-->c3[结束] c1-->c4[队列空 输入未完毕]-->c5[原地等待下一个输入] c1-->c6[队列中有人]-->c7[当前楼层以上有人要上]-->c8[向上走 接上行的人] c6-->d0[当前楼层以上只有想向下的人]-->d1[去接出发点最高的人 然后向下走] c6-->c9[当前楼层以上无人]-->c10[方向变为向下 如果没有人上电梯 再问Schedule]

我觉得这是一个简单并且有效的策略,所以后面几次作业都是在LOOK策略的基础上进行了一些改动,并没有探索新的策略。

电梯调度的影响因素太多,没有完全最优的策略,所以。。。感觉挺没劲的。但是要想基于随机数据在统计意义上评价一个策略的优劣这件事还是值得研究的,但是我太菜,不会。

UML类图

本次作业我的设计比较简单,由输入线程获取输入顺便直接将请求分发到每个电梯对应的侯乘表中,电梯线程对侯乘表中的请求进行处理。这是一个简单的“生产者消费者模型“。

线程类只有主线程、输入线程、电梯线程。

这次设计有很多不必要的地方,例如

  • Schedule类里面仅仅存放了一个电梯用于获取运行方向的类,这个东西其实和电梯写在一块比较紧凑,拿出来作为一个类感觉没必要
  • RequestQueue也是鸡肋,这其实就是对于ArrayList进行了一个wrap,把他变成了一个线程安全的类,但是这样的类Java已经提供了(后几次作业我用的是Vector),只是我第一次作业的时候不知道

以上两个问题后两次作业均已修正。

锁与同步块设置

这次作业加锁的地方有两处:

  • 对侯乘表增删请求

  • 电梯开门进入乘客的时候要对这一层中同一方向的请求列表加锁,防止在循环的时候另一个线程(具体来说是输入线程)改动这个列表。

hw6

(一)问题重述及分析

作业6和作业5相比主要有下面这几个变化

  • 加入了横向电梯,但是仍然不需要考虑换乘(输入要求一个乘客的请求必须可以直达
  • 可以输入指令向系统中动态添加电梯
  • 一个楼层(楼座)可以有多个电梯

在hw6中,横向电梯和纵向电梯其实差不多,只不过方向不一样。

如果采用自由竞争的策略,要求多个电梯共享一个侯乘表,因此电梯对于侯乘表的读取就需要保护。

(二)实现

UML图

这次作业的实现和hw5差不多,由InputHandler获取请求并且把请求分发给对应的侯乘表(每个楼层、每个楼座都对应一个侯乘表),电梯从侯乘表中获取请求,思路示意图如下。

本次作业的共享对象仍然只有侯乘表,电梯和输入线程共享侯乘表,电梯和电梯之间也共享侯乘表。

锁与同步块设置

和上一次作业相比,多了多个电梯对于同一个侯乘表的访问冲突,但是要加锁的位置和上一次差不多。

对于电梯为空时获取接下来要走的方向的getTarget方法我没有加锁,因此这个方法可以说是线程不安全,但是为什么没有出问题呢?因为这个函数的返回值仅仅起一个参考作用,电梯要是这一步走错了,下一步再去正确的地方就可以。

hw7

(一)问题重述及分析

第七次作业与之前的主要区别如下:

  • 电梯可定制。可定制信息包括电梯的速度、载客量,还有横向电梯的可到达楼座。
  • 乘客的请求没有楼层相同或者楼座相同的限制(可能需要换乘

电梯工厂

针对第一个问题,其实很好处理,可以在电梯的构造函数里面传入一些参数,但是这样的话reqSender就会变得乱七八糟,所以我决定把电梯的构造逻辑分离出来——这就是工厂模式。直接给电梯工厂传入一个ElevatorRequest,电梯工厂就会造出来一个电梯,我认为工厂模式就是为了对构造方法的封装。

请求的拆分-伪装

针对第二个问题,我采用静态拆分的方法,收到请求的时候直接由reqSender把请求拆分为1到3个简单请求(简单请求指的是不需要换乘的请求),构造为一个MyRequest。MyRequest的方法其实和PersonRequest很像,都有getFromFloor,getTofloor等等,不过MyRequest是根据当前这个已经经历过的换乘,从被拆分出来的简单request中选择一个,伪装成它,这就是对象的属性与状态的区别。

这样,电梯就不用管这一堆关于换乘的逻辑,因此从hw6到hw7我的纵向电梯代码几乎没有改动。由ReqSender发出的请求都是MyRequest,即使是不需要换乘的请求也被ReqSender构造成一个MyRequest再发送出去,因此可以做到新请求和经过换乘的旧请求的统一

为了把换乘的请求发送给侯乘表,我在输入线程和侯乘表之间又抽象出了一层ReqSender用于分发请求(采用单例模式),电梯和输入线程都可以直接获取ReqSender然后通过这个对象分发请求。

关于我为什么不选择动态拆分:

其实在我这种拆分策略下,动态拆分和静态拆分的效果其实是一样的,因为第一步拆分的时候已经选择了一个换乘楼层,换乘楼层一经确定拆分方法就确定了,如果在到达换乘楼层之后再拆分,肯定会优先选择把当前楼层作为换乘楼层。因此动态拆分和静态拆分效果是一样的,为简单我选择了静态拆分。

但是如果要考虑电梯速度,侯乘人数等等这些因素,或者如果电梯有可能中途退出系统,静态拆分肯定会有很多局限性。

示意图如下:

线程的结束

参考了实验的代码

这个功能是由RequestCounter来完成的,RequestCounter是一个单例模式的类,其中有一个变量counter,类似于OS中学到的“信号量”,它的含义是已经完成但是未查收(acquire)的PersonRequest的数量。

电梯每送达一个人,会调用RequestCounter中的release函数,将counter+1。

RequestCounter中还有一个函数叫做acquire,含义是查收一个请求,如果当前count>0,给count -1;否则等待电梯完成请求。在主线程中setEnd之前会调用requestNum(PersonRequest的总数量)次acquire,requestNum次调用之后就可以保证所有的请求已经完成。这之后就可以给侯乘表setEnd了。

(二)实现

UML图

锁与同步块设置

hw7中分发器对电梯列表进行了访问,其实只需要在判断某一楼层是否有横向电梯可以满足需求的时候需要对这一层的横向电梯列表加锁。

其他部分和hw6没有太大区别。

(三)sequence diagram

这是本单元第三次作业的协作图,下图描述了一个两段请求被完成的过程、一个ElevatorRequest的完成和线程的结束。

三、bug分析与互测

强测及互测

hw5,hw7均无bug,hw6出现了ctle问题,最后发现是因为频繁的notifyAll,强测了一个点没过

具体来说是,我在Waittable的isEnd()和isEmpty()方法中也加入了notifyAll,因此,当这个楼座中一直没有乘客并且还没有被setEnd的时候,如果这个时候有电梯调用isEnd(),就会唤醒其他等待在这个楼座中的电梯,但是这个时候侯乘表中仍然没有乘客,其他的电梯又互相频繁唤醒。。。这就是轮询。

注释掉这两个notifyAll之后问题解决

强测成绩两次均在98以上,另一次是93,挺好的,满意了。


这一单元没有参与太多的互测,大概是因为没有时间。我认为互测的初衷是想让我们互相读代码。

但是好多人写了评测机测bug,这其实也挺好,同学之间互相帮助找bug嘛。

常见bug总结

看了看别人的bug和自己的bug,我觉得这一单元主要可能出问题的地方在于线程安全,有下面这几个容易出现的bug:

  • 轮询

    轮询发生的原因很有可能是在一个循环中啥都没干(没有改变任何对象的状态)然后继续执行


    我在hw6出现的这个bug也属于这一类,不过是涉及了多个电梯之间的互相唤醒:

    电梯被唤醒->发现没有乘客(这个同时唤醒了其他电梯)->wait->被其他电梯唤醒->回到开始

    在hw7中,写出来的最初版本也出现了轮询问题,原因是我最开始在判断横向电梯wait的条件是对应侯乘表整个为空。但是如果这一层有两个电梯e1,e2,侯乘表中的乘客只能被e1送达,这个时候e2不能接到乘客,也不能wait,而我在获取目标楼座的时候如果当前没有可以接到的乘客就会返回当前楼座,表示让电梯在下个循环中wait,但是电梯他不wait啊,最后就一直在这个循环里面啥也不干。

    出现这些问题的原因本质上是逻辑考虑不周全。

  • ConcurrentModificationException

    当我们在一个循环中增删迭代器中的元素的时候,JVM就会报这个错,这个问题在单线程里面很容易发现,例如:

    for(PersonRequest pr : persons)
    {
        if(...)
            persons.remove(pr);
    }
    

    但是在多线程中,常常是在类似的循环中对迭代器什么都没干,就发生了这种诡异的现象,这个问题出现的原因是另一个线程改变了迭代器中的元素。解决方案就是对循环加锁。(即使这个对象是线程安全的也可能会出错,因为线程安全对象不能保证这个循环是原子的)

    在多线程编程中要小心使用这样的循环!

  • 线程不结束

    线程不结束的常见原因可能有这几种:陷入死循环;死锁(罢工)

    判断是哪一种也很简单,就是在没有输出的时候看一下java有没有占用过多的CPU资源

    • 陷入死循环可以用检查轮询的方法检查,就是在可能出问题的地方输出一些东西,运行,看有没有频繁输出。

    • 死锁的检查可以在wait方法前后输出一个标志,就可以直到是那些线程处于等待状态了。

    当然这只是比较简单的检查方法,IDEA的多线程调试我也尝试过,但是总感觉没有输出好用(print大法好!)

  • 电梯行为诡异

    emmm我在整个单元的过程中还没有遇到这种问题,没什么经验。

四、心得体会

  1. 重构能让思路变得清晰

  2. 当你加锁的时候,一定要清楚是为什么而加锁


('A')/

posted @ 2022-04-29 16:15  Banana889  阅读(63)  评论(2编辑  收藏  举报