面向对象第二单元总结
〇.单元总览
本单元主要以“电梯问题”为背景熟悉如何处理多线程问题,并着重于对多线程问题中线程安全问题的考察。
在本单元的作业和实验中,我们还学习并使用了生产者-消费者模型、黑板模式、单例模式和流水线模式等新的方法,进一步加深了对面向对象设计模式的理解。
一.线程分析
本单元作业的线程一共可以分为三个类别:
-
输入线程
-
调度器线程
-
电梯线程
输入线程主要负责从标准输入读入输入信息,并对信息进行处理
(第三次作业中输入线程还负责启动新的电梯线程)
调度器线程负责根据调度策略分配乘客到电梯中
电梯线程分为普通电梯和横向电梯两个部分,负责模拟电梯运行
二.同步块与锁
在本单元课程中我们一共学习了synchronize
锁和lock
锁两种不同的方法锁。
在性能上来说,如果资源的竞争不是特别激烈,那么两种锁的 性能是相差无几的,而当资源竞争十分激烈时,此时lock
锁的性能是要远远优于synchronize
锁的。由于课程项目中,资源的竞争并不是十分激烈,而由于对lock
锁并不是特别的熟悉,因此综合来看,在三次作业中,最终选择的都是使用synchronize
锁。
synchronize
锁主要使用在RequestQueue
这一托盘部分,具体来看,对托盘中的addRequest
和getRequests
等方法都使用了synchronize
同步块进行保护,以确保电梯线程和调度器线程不会同时使用托盘从而导致出现线程安全方面的错误。
除此之外,项目中还存在两个单例模式的类:Printer
和Transfer
,前者是为了解决输出线程安全问题,后者则是为了判断是否还存在换乘信息从而判断是否结束调度器线程。对这两个类的方法同样使用了synchronize
同步块进行保护,以确保线程安全。
三.调度器与线程交互
本次的作业采用的是二级托盘模式,主要分为一个大托盘和对应不同电梯的各个不同的小托盘。
在项目中,调度器的作用十分的单一,主要有两个作用:
- 当乘客全部处理完后,给电梯线程对应发送
END
信号 - 根据调度策略将乘客分配到不同的电梯托盘中
调度器线程与其他的线程主要通过一个共享的请求队列即“托盘”进行线程间的交互。
调度器线程与输入线程间的交互:
- 输入线程读入乘客信息,并将信息处理后放入大托盘中
- 调度器从大托盘中获取乘客信息
调度器线程与电梯线程间的交互:
以第三次作业为例,调度器线程与电梯线程有两个交互点
- 调度器获得乘客信息后,将其分配到对应电梯的小托盘中
- 换乘乘客下电梯后,电梯重新将乘客信息放回大托盘中
四.架构模式
由于三次作业属于一个迭代的增量开发,并没有进行太多的重构,因此架构模式部分以最后一次作业为主体进行分析,前两次作业较为简略。
UML图
本次代码主要基于“消费者-生产者”模型进行架构。
以下分析解释一些重要类的作用:
Main类
在Main类中设置了三个变量transfer
、waitQueue
和buildQueue
,这三个变量主要负责在各个类中传递信息。
Transfer
:用于记录换乘人员数量,当程序接收到换乘人员信息时,调用add
方法,当换乘人员结束使用电梯时,调用sub
方法,并提供isZero
方法以判断是否换乘人员全部运送完成。waitQueue
:代码中的大托盘,用于存储输入线程获取到的乘客信息buildQueue
:一个二级列表,用于存储电梯线程
InputThread类
输入线程类,主要分为两个小结构:当读取到乘客信息时,将乘客信息放入大托盘内;当读取到电梯信息时,根据读取信息新建电梯线程。
MyPersonRequest类
乘客信息类,在官方包中PersonRequest
的基础上自己重新修改的一个类。
由于换乘方式选择的时静态分配的方式(即在输入乘客信息时就确定换乘方式),因此我选择在该类的构造函数中就预设好换乘路线,根据乘客的不同类型选择调用findElevator
或者findHoElevator
方法来获得换乘路线。
该类中的myRequestQueue
是一个列表,用来存储该乘客换乘路线上的电梯,当该列表为空时则表示乘客已经到达目的地。
-
选择换乘电梯的策略:
(1)对于纵向电梯即普通电梯,选择最长时间没有使用的一座,如果有多个相同的电梯,则选择运行速度最快的一个,如果有多个速度相同的电梯,则选择第一个找到的电梯。
(2)对于横向电梯,首先找到使|X-m|+|Y-m|最小的m,其中X为起始楼层,Y为目的楼层,m为中转楼层(m优先选择与X、Y相同的楼层),然后在满足要求的楼层的所有电梯中选择最长时间没有使用的一座,如果有多个相同的电梯,则选择运行速度最快的一个,如果有多个速度相同的电梯,则选择第一个找到的电梯
Schedule类
由于在MyPersonRequest
类中就已经给乘客分配好了电梯,因此在该类中只需调用MyPersonRequest
的getMyRequestQueue
方法既可以轻松将乘客加入到电梯外的等待队列中。
Elevator类和HoElevator类
电梯类,主要负责模拟电梯的运行。
电梯的运行主要采用look策略,电梯可分为三个状态:
- 状态0:电梯内外均没有人,保持静止
- 状态1:电梯向上(顺时针)运行
- 状态-1:电梯向下(逆时针)运行
调度策略(普通电梯):
- 当电梯处于状态0时,选择一个距离最远的乘客去接送,并记录主方向
- 当电梯处于状态0,但处于接乘客的途中,若楼层外部有同方向乘客,则捎带
- 当电梯在运行期间,若电梯内部没有乘客,将状态置为0
- 当电梯在运行期间,若内部有乘客且当前楼层外面有同方向乘客,则捎带
调度策略(横向电梯):
横向电梯与普通电梯调度策略基本相同
- 状态0时选择等待列表中第一个乘客,电梯启动去接送该乘客
- 当电梯处于状态0,但处于接乘客的途中,若楼层外部有同方向乘客,则捎带
- 当电梯在运行期间,若电梯内部没有乘客,将状态置为0
- 电梯运行时,每次都选择最近的走法,在运行过程中如果有同方向乘客则捎带。
迭代过程
本次作业基本上没有进行太多的重构,主要迭代如下:
- 第一次作业无横向电梯,相较于第三次作业缺少
HoElevator
类,Transfer
类和MyPersonRequest
类 - 第二次作业无换乘请求,相较于第三次作业缺少
Transfer
类和MyPersonRequest
类
时序图
五.BUG分析
第一次作业出现两处bug:(1)调度策略设计失误,在一些特殊情况下会比基准策略慢,出现超时错误。修改策略后成功修复。(2)输出线程不安全,输出时间非递增,使用单例模式和synchronized
即可确保安全输出。
第二次作业进行了较为完备的测试,无bug。
第三次作业出现一处bug:在RequestQueue
类中增添方法时忘记使用synchronized
保护线程安全,出现线程错误,重新添加synchronized
即可修复错误
互测阶段并未发现他人错误...
六.心得体会
电梯啊电梯,早就听说过你了,没想到有了准备还是被你打的错不及防...
本单元收获满满,学习了“消费者-生产者”模式和单例模式等等东西,迈出了多线程学习的第一步,这是从0到1的突破,略微懂得了如何处理线程,如何确保线程之间的交互安全...
巩固学习了第一单元所讲到的层次化设计模式,更多的练习也让我对面向对象的思想有了更深的体会(虽然还是没怎么太明白...)
只能说多线程这玩意越看头越大,中间想尝试一些别的东西,奈何水平不够,能力太差,时间不足,猝。希望以后有空还能再看看多线程,在多研究一下...