2021-OO-UNIT2-总结
电梯单元总结
线程安全分析
第一次作业
-
锁的选择
-
第一次作业(单部电梯)所用到的共享对象仅包括电梯控制器 Controller 类。我将所有需要共享的内容集中到 Controller 当中,令两个线程(电梯线程和输入线程)共享它实例化的对象,电梯线程负责读取 Controller 状态,做出相应操作;输入线程负责接受输入,改变 Controller 的状态(如:乘客队列等)。因此,第一次作业中上锁的内容仅为 Controller。
-
本次作业的锁采用 synchronized + 代码块 的设置。例如:
synchronized(Controller) {
//TODO
}之所以采用这样的方式(而非锁住方法等),一是由于在编写过程中查阅了一些关于代码块锁与静态方法锁、非静态方法锁的优劣比较资料后做出的选择(具体内容可以参考:[
-
-
同步块设置
-
本次作业中,锁住Controller对象的代码块分布在电梯线程、输入线程以及Controller类自身。
-
同步块的设置与自己的需求密切相关,同时,另一个重要的考量是判断是否会出现死锁。由于将共享对象集中到 Controller 中,故确保了死锁不会出现。
-
-
锁和同步块中内容关系
-
在电梯线程中,加锁代码块的考量(内容方面)是将判断输入结束与否、判断电梯开门与否、判断电梯移动与否等相结合而化为原子性操作,直接完成电梯行为的抉择。在我看来,这些判断内容必须以原子形式一次性完成,否则将可能出现资源不同步的情况。
-
在输入线程中,加锁代码块中主要完成的是将输入请求添加至共享对象 Controller 中并实现一个NotifyAll操作。这是易于理解的。
-
在Controller对象自身中,主要将出方法和入方法中代码加锁。这是因为出入的判断中必须保证电梯队列的同步,若电梯线程此时正调用该对象的入方法或出方法,则不能容许输入线程对该对象进行修改。
-
第二次作业
-
锁的选择
-
第二次作业(多部相同电梯)所用到的共享对象包括电梯控制器 Controller 类,以及放置请求的 Commands 类。令三个线程共享两类对象。输入线程和调度器线程共享Commands类的实例化对象;调度器线程和电梯线程共享Controller类实例化的对象。与第一次作业相比,上锁的内容有所增加,不过仍然较少。
-
本次作业的锁采用 synchronized + 代码块 的设置。例如:
synchronized(Controller) {
//TODO
}之所以采用这样的方式(而非锁住方法等),延续了上一次作业的思想(具体可以结合:[
synchronized (controllers.get(0)) {
synchronized (controllers.get(1)) {
synchronized (controllers.get(2)) {
synchronized (controllers.get(3)) {
synchronized (controllers.get(4)) {
//TODO
} } } } } }
-
-
同步块设置
-
本次作业中,电梯线程、调度线程包含锁住Controller实例化对象的代码块;调度线程、输入线程包含锁住Commands实例化对象的代码块。
-
我经过仔细思考,认为A-B共享一对象,B-C共享另一对象的同步块设置看似互相依靠,但是并不会带来死锁问题。在实践中也验证了这一点。
-
-
锁和同步块中内容关系
-
在输入线程中,加锁代码块中主要完成的是将输入请求添加至共享对象 Commands 中并实现一个NotifyAll操作。由于它与调度器线程共享该对象,故此时Notify的目标是调度器线程(与第一次架构不同)。
-
在调度器线程中,加锁代码块主要负责接受了输入线程的提醒,并处理输入线程所提供的输入请求(加人或加电梯)。这都是必须保证同步性和安全性的操作。
-
在电梯线程中,加锁代码块的考量(内容方面)与第一次作业无异,主要是将判断输入结束与否、判断电梯开门与否、判断电梯移动与否等相结合而化为原子性操作,直接完成电梯行为的抉择。这一点延续了第一次作业的原子化思想。
-
第三次作业
-
锁的选择
-
第三次作业(A、B、C电梯)基本延续了第二次作业的架构,因此在锁的选择上改动也不大。所用到的共享对象包括存放电梯控制器 Controller 类的一个列表,以及放置请求的 Commands 类。令三个线程共享两类对象。输入线程和调度器线程共享Commands类的实例化对象;调度器线程和电梯线程共享放置5个Controller类实例化对象的列表。
-
本次作业的锁同样采用 synchronized + 代码块 的设置。例如:
synchronized(Controller) {
//TODO
}采用这样的方式的考量与先前作业无异(具体可以参考:[
synchronized (controllers.get(0)) {
synchronized (controllers.get(1)) {
synchronized (controllers.get(2)) {
synchronized (controllers.get(3)) {
synchronized (controllers.get(4)) {
//TODO
} } } } } }而本次作业有所改动,化为:
synchronized(controllers) {
//TODO
}这是为了更好地放置锁中内容,当锁住代码块需要实现wait和notifyAll操作时,可以直接选定该对象进行操作。
-
-
同步块设置
-
详见第二次作业的“同步块设置”相关介绍,与其基本无异。
-
-
锁和同步块中内容关系
-
详见第二次作业的“锁与同步块中内容关系”部分,与其基本无异。
-
调度器设计
第一次作业
-
在本次作业中,由于只有一部电梯,故不包含统筹整体的调度器。故主要介绍电梯内置的控制器:Controller。
![]()
-
控制器如何与线程交互?
-
如图所示,控制器与输入线程的交互主要包括了接受输入的乘客请求;而控制器与电梯线程更是密切相关,它作为一个属性被内置在电梯类中,存储了乘客列表、电梯楼层等信息。它为电梯决定下一步如何行动。
-
第二次作业
-
本次作业中,引入了 调度器(Scheduler)。调度器作为一个线程出现,它结合输入线程,并管理5个电梯线程的5个内置Controller对象,实现增加电梯、往乘客队列中添加乘客等操作。

-
调度器如何与线程交互?
-
首先,调度器自身便是一个线程。将其设置为线程的主要考量是为了第三次作业可能的换乘作准备(个人认为,当线程彼此的交互增加时,用线程类来代替普通类更容易实现调度的实时性)。
-
调度器与输入线程、电梯线程均有交互。与输入线程的交互体现在它查收并处理输入线程接收得到的请求;与电梯线程的交互体现在共享了内置于电梯线程的Controller对象,将乘客请求分配给各个Controller,来运作电梯,同时当需要增加电梯时唤醒对应的电梯线程。
-
第三次作业
-
本次作业中基本延续了第二次作业的 调度器(Scheduler)架构。调度器作为一个线程出现,它结合输入线程,并管理5个电梯线程的5个内置Controller对象,实现增加电梯、往乘客队列中添加乘客等操作。

-
调度器如何与线程交互?
-
首先,调度器自身便是一个线程。
-
调度器与输入线程、电梯线程均有交互。与输入线程的交互体现在它查收并处理输入线程接收得到的请求;与电梯线程的交互体现在共享了内置于电梯线程的Controller对象,将乘客请求分配给各个Controller,来运作电梯,同时当需要增加电梯时唤醒对应的电梯线程。此时,与第二次作业有所不同的是,出于换乘的需要,调度器并不直接分发乘客请求,而是利用乘客信息并结合电梯实际情况,实例化一个Person对象,该对象中包含了换乘路线、乘客请求、当前起始站和终点站等信息,再将Person对象分配给各个电梯。
-
在第三次作业中,电梯线程与电梯线程之间也能实现线程的交互。当需要换乘的乘客在换乘站下车时,它将不再经过调度器,而是直接被另一个电梯内置的Controller对象取走,实现线程交互。
-
第三次作业的架构可扩展性
-
UML类图
![]()
如图所示,该次作业的UML类图较第一单元的UML类图更简洁明了,类之间的相互依赖关系更清楚简单。这在一定程度上体现了面向对象编程思想的提升。以下,更加细致地分段介绍UML类图组成部分。
-
如图所示,表示UML类图中的四个线程类(包括主线程)的相互依存关系。在主线程中创建并运行了另外三个线程。同时,由于题目中新建电梯的需求,在调度器中也有令电梯进入就绪状态的代码,故调度器线程与电梯线程也存在依存关系。
![]()
-
如图所示,为线程与共享对象之间的关系。可见,输入线程与调度器线程共享Commands类对象,调度器线程与电梯线程共享Controller类对象。
![]()
-
另外三个类的具体UML类图如下所示。由于该三类仅在调度器等处简单调用,并没有值得分析的依存关系,因此将他们独立展出。
![]()
-
-
UML协作图
我利用Sequence Diagram工具完成了作业的UML协作图绘制。由于该工具每次仅能绘制一个类的协作关系图,同时每张图中均涉及到一些非线程类。因此以下采用逐一递进的逻辑思想进行分析。
-
MainClass(主线程)
![]()
-
如图所示,左上角以不同颜色标注的黄色方格代表线程。在MainClass中,主线程实现了Elevator线程、Scheduler线程、Input线程的创建。
-
-
Input(输入线程)
![]()
-
在输入线程中,表面上没有表现出与其他线程的交互,但是实际上Commands类的实例化对象为输入线程与Scheduler(调度器线程)的共享对象。因此,图中的setIsEnd(传递结束信号)与addRequests(添加请求)操作实际上是输入线程与调度器线程的交互。
-
-
Scheduler(调度器线程)
![]()
-
如图所示,系统自动生成的协作图中主要体现了与Way对象的协作。此处的Way对象并非线程,因此这不是线程协作关系的重点。值得注意的是在协作图底部,调度器线程通过addWaitQueue(增添电梯等待队列)操作实现了与Controller对象的交互,由于Controller对象由调度器线程与Elevator(电梯线程)共享,因此这本质上是调度器线程和电梯线程的交互行为,调度器线程会通过影响Controller从而影响电梯线程。
-
-
Elevator(电梯线程)
![]()
-
对于电梯线程,值得注意的是它对于Controller对象信息的读取以及对Controller对象的修改操作。因为Controller是它和Scheduler(调度器线程)共享的重要资源,因此这在实质上完成了和调度器线程的交互协作。在之前的分析中,已经介绍过了调度器线程对于电梯线程的影响。现在我们知道了,电梯线程也会通过影响Controller对象从而影响调度器线程。这也是符合调度逻辑的。
-
-
-
综上所述,第三次作业的架构并不复杂,通过UML类图可知类与类之间的联系较为清晰简单;而通过UML协作图可知,线程类和线程类之间的协作通过对共享对象的修改实现,共享对象数量较少,协作是简洁的。因此,个人认为该架构的可扩展性是比较高的,如果题目的要求进一步修改,也能在原有基础上迭代开发。
程序bug分析
本单元作业虽然写起来架构清晰了不少,但是出锅多,与第一单元作业相比,个人认为自己的完成情况非常不理想。不过幸运的是,在线程安全方面没有出现死锁等问题,因此以下分析主要集中在其他方面。
bug-1:运行时间过长
-
在第二次电梯作业的强测当中,有一个点因为超时而出锅(即:著名的180s开始运人的强测点)。这个bug出现在我的Scheduler(即:调度器线程类)中,这是由于Random情况下放人调度策略有误导致的。选择了一个并不成熟的模拟电梯算法进行选人,但是该算法并非完整地模拟过程,过于局部化,因此局限性很强,难以适应情况较复杂、偶然度较大的强测。事实证明,使用“平均分配”的调度策略,虽然难以达到较高的分数,但是对于代码编写能力不够扎实、面向过程思想不够深厚的我来说,是一种比较稳妥的方法。
bug-2:无限进人
-
在第一次作业的强侧中,出现了一个恐怖的bug,令我失分惨重。该bug为:电梯可能在某一次开关门过程中进入过多人,使电梯中的人数超过正常范围。bug出现在Controller对象中,即电梯内置的控制器,该控制器用于决定电梯的行为。当电梯执行进人操作时,我将等待队列中可能进入电梯的乘客放入一个暂时队列中,当队列人数超过正常范围时停止;但是,当我将暂时队列中的乘客转移到电梯队列时,并没有判断电梯是否人满,导致了该bug。
可以看出,我的bug主要分布在策略、行为等方面,而与线程安全无关。这体现了我的面向对象编程思想仍然不够扎实,代码编写能力有待提高。
发现别人bug采用策略
-
采用了自动测评结合代码查阅的方式进行他人bug的查找。该策略起到了一定作用,让我在互测中成功hack到他人。不过,由于在本单元期间,各科课程压力增大,故hack他人的次数与第一单元相比减少较多。
-
关于线程安全问题的查找,同样通过自动测评结合代码查阅的策略进行。值得注意的是,在多线程作业中情况多变,bug复现与否较难把握,故主要是通过自动测评累积信息,再结合代码查阅,构建真正典型的强力数据。
-
与第一单元相比,自动测评机的构建更加具有难度。必须实现定时的投放功能;对于电梯行为正确与否,必须手写判断逻辑。这些都与第一单元的多项式求导测评差异甚远。同时,由于多线程的不确定性较大,自动测评得出的数据也很难保证一定能再次复现他人bug,因此在该单元的互测中,必须加大代码阅读的比例,思考他人同步锁的设置是否合理,以此构造典型情况。
心得体会
-
在线程安全方面,一个较有感触的体会是必须对于代码所采用的同步锁等机制(例如,我采用的Synchronized方法)有透彻的理解和自己的思考,知晓程序运行时的行为。一定不能一知半解便加以尝试,不能简单地看了几个实例便将模板往自己的代码中生搬硬套。个人认为,在没有深入学习该方面知识时,我的代码确实存在线程安全问题;而在花费一定时间仔细研究后,我对于自己程序的线程安全非常有信心,并且也能够对于他人在线程安全中遇到的问题加以分析解答。
-
在层次化设计方面,我想相较于第一单元是有所提升的。在第一单元作业完成过程中,次次重构的经历让我无比难忘;而本单元作业,我通过较理想的层次结构,没有任何一次重构,每次都在前一次作业基础上作轻量迭代。在第一单元博客中,我告诉自己,必须做好层次化设计!!必须在每次作业中思考下一次作业的扩展需求!!,我想,这一次作业应该做到了这点。这样的层次化设计思想,也应该贯彻到接下来的作业当中。
-
在三次作业中,虽然线程安全没有问题;层次化设计也较为良好,但我在强侧中却得到了惨不忍睹的成绩,bug令自己无比懊恼,这应该是几次电梯作业下来最深的体会了。认真分析原因,我认为以下几点导致了我的高频出锅:首先,代码编写能力有待提高。这一点,是在上周打蓝桥杯时幡然醒悟的,我发现自己在写编程题时脑子是浑浊混乱的,像在没有草稿纸的情况下解数学题一样,然后我发现这样的状态正是编写OO代码的常态。这导致了我思路的不够连贯一致,bug接二连三。其次,在电梯单元作业的编写过程中,我是不够专注的,尤其与第一单元相比。在第一单元作业中,每次编写完成代码,我都会认真细致地排查bug,与舍友交流心得,利用自动测评机为自己debug。然而到了这一单元,我并没有这样认真地为自己检查代码,因为自动测评机编写难度的提升,我在一开始也没有热情去为自己测评。同时,由于这一单元迭代开发的工程量降低,我往往过了中测便将OO作业放到了一边。这些都是bug的源头。我想,这是OO博客的意义所在:让我及时反思,为下一单元作业敲响警钟










浙公网安备 33010602011771号