面向对象第二单元总结
(1)总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系
三次作业中同步块的设置分为以下几类:
- 共享对象的互斥操作,如
请求队列的增删改查 - 输出函数调用的互斥。我将所有输出函数抽象为一个类,所有的输出操作必须调用这个类来完成。
 
其中第二点使用场景比较单一,仅在电梯改变状态的过程使用,各个电梯直接竞争输出函数的锁,不需要增加wait/notify机制。
对于第一点,输入线程与调度器、调度器与电梯都是生产-消费者模型。
- 首先,消费者需要先拿到共享对象的锁,取出共享对象中的请求并处理,如果没有则等待。因此消费者之中的同步块只包括查询、删除共享对象,以及共享对象为空时的
wait语句。 - 而对于生产者,生产者只有拿到新的请求,才需要申请共享对象的锁,将请求放入共享对象并通知消费者。因此生产者中的同步块只包括向共享对象中放入新请求,以及通知所有消费者。
 
在这三次作业中,每个共享对象只会有一个生产者和一个消费者竞争它的锁,线程极度安全。但是也错失了让多个线程竞争一个锁的训练机会。
(2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
第一次作业中我没有采用调度器,直接输入线程→候乘区→电梯线程输入线程和电梯线程共享候乘区。
第二、三次作业的架构为输入线程→请求队列→调度器线程[→候乘区→电梯线程]+。
调度器线程的主要功能
- 与
输入线程共享请求队列,从中取出请求,随后根据请求创建电梯或将请求分配给电梯们。 - 根据请求内容设置新的
电梯线程的属性,并激活该线程。 - 与每个
电梯线程共享一个候乘区,将取出的请求按一定算法分配给候乘区。 - 关闭线程。
调度器需要根据输入线程的存活状态、请求队列的状态选择是否关闭本线程,关闭时需要向所有电梯线程发送关闭信号。 - 在第三次作业中,
调度器还需要记录是否有人正在换乘的信息,关闭线程时还要判断换乘信息是否为空。 
调度器算法设计
	第二次作业中没有采用高大上的调度算法,仅仅在调度器中新增了一个变量index,作为循环分配请求的指针,调度器每次将新请求放入指针所指候乘区,然后index循环后移即可。(还好强测数据没有特别针对这种傻瓜算法)
 第三次作业也是循环分配任务,只是循环指针变为每种类型电梯共享一个。
(3)从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性
我将从迭代过程来分析这个问题,请看我三次作业的UML类图(因为策略类的缘故导致这玩意比较复杂,俺就直接用idea生成了):

以及我的UML协作图(以一个电梯线程为例)

第一次作业
	第一次作业只需要模拟一个电梯的运行,不存在复杂调度的需求,因此我参考生产消费者模型,将电梯运行过程建模为输入线程→候乘区→电梯线程。
	其中输入线程专注于输入的获取,并及时地将需求按楼层放入候乘区。
	而候乘区是一个以楼层为键值的哈希表,每层存放着从该楼出发的人员队列。
	电梯线程模拟电梯的开关门和上下楼动作。每当电梯到达一层楼时,电梯会向其内置的策略类请求一个动作,策略类维护一个电梯轿仓,并且负责分析此时电梯所处状况,向电梯发送动作指示下一步电梯的行为。
	整体架构上,输入线程和电梯线程被候乘区分隔,同步读写候乘区,逻辑上线程之间的耦合度降低、增强了线程间协同的安全性。
 但同时也碰到了新的问题:
- 其一是线程间通信,由于轮询对CPU消耗太大,
电梯线程遇到候乘区为空时不能采用轮询,而是需要等待输入线程唤醒,这可能会产生唤醒信号丢失。 - 其二是线程关闭的问题。两线程逻辑逻辑上的解耦,使得各线程可以专注于其内部矛盾。但是这会造成
电梯线程只会睡眠而不会主动关闭的尴尬境地。 
	解决方案。在电梯线程的sychornized同步代码块中睡眠可以解决唤醒信号丢失问题,并新增判断条件来主动处理唤醒信号(输入结束或新增请求)。
	对于电梯架构设计,解耦电梯分析过程(策略类负责)和执行过程(电梯线程本身负责),有如下几个好处:
- 逻辑上,电梯相关行为之间耦合度更低。将电梯分析过程抽象为策略类,有效降低了电梯本身代码长度和复杂度,使
电梯线程本身变得易于维护。 - 策略类将不同算法封装为一个类,可以根据情况的变化随时替换(事实上是一个猫宁到底)。
 - 线程相关。解耦这两个过程,大大降低了发生线程不安全的可能。因为复杂的分析被封装到静态的
策略类中,此时电梯享有对共享对象的绝对控制权。而动态的执行过程则与共享对象无关,电梯在执行之前就释放了对象锁,避免了潜在的线程安全问题。 
 但是同时也会引入额外的问题:
- 策略类没有发挥出热插拔的作用。这三次作业都是一个策略干到底。
 - 策略类和电梯之间的通信困难。解耦分析和执行过程,从简化了思考的复杂度,但是由于分析和执行过程分给两个类来实现,电梯的不同状况对应了不同的执行方案,因此两个类间的通信格式就较为复杂,对应的分析代码也会比较复杂。
 - 代码长度增加。由第二点直接导致。
 
第二次作业
	第二次作业中从单一电梯变为多个电梯。因此需要在输入线程和电梯线程之间增加一个调度器线程作为输入需求的缓冲,从全局给各个电梯分配任务,采用一定算法调度电梯的执行。
 整个程序的架构变为
	输入线程→请求队列→调度器线程[→候乘区→电梯线程]+
	其中调度器线程的作用是分析请求队列中的请求,新增电梯或向电梯分配请求。关于调度器的具体设计前面已经详细说过。
第三次作业
 第三次作业只需要在第二次作业的基础上进行一定程度迭代即可。主要有以下几个方面:
- 
电梯线程的升级。前两次作业中,电梯和策略的特征是一致的,而此次作业中对电梯的特性提出了不同要求,这就需要电梯和策略具有指定特性的能力。我引入setType(String type)方法,在电梯初始化时调用该方法,用以设置电梯和策略的基本属性。 - 
对换乘的支持。我换乘算法设计的过于弱智,(令人气愤的是)还不如不换乘。但我认为无论调度器是否采用换乘算法,本次作业的架构都需要支持换乘。基于开闭原则,我在第二次作业架构上主要做出如下补充:
- 
调度器内置变量transNum变量,用以统计已经过调度器、进入电梯候乘区的、正在换乘途中的人数。 - 
电梯可以将人员重新放回
请求队列。基于高内聚低耦合的原则,调度器只负责调度请求队列里的请求,而电梯负责生成新的请求送入队列。同时,我将原始请求
Request和电梯请求Person设置为不同类型,方便调度器快速识别来自请求队列的请求的种类。 
这样补充的好处是:
- 符合开闭原则,实现简单。由于没有重构,只是在原有基础上增添新的代码,并且新增部分逻辑简单、代码量也比较小。
 - 线程流畅关闭,提高线程安全性。设置换乘记录变量
transNum,有效掌握整体架构中需求流动的状况,对线程是否关闭了如指掌,免于线程空等而无法关闭的风险。 
 - 
 
(4)分析自己程序的bug
• 分析未通过的公测用例和被互测发现的bug:特征、问题所在的类和方法
• 特别注意分析那些与线程安全相关的问题(特别要注意死锁的分析)
 这三次作业出现bug较少。
 非线程相关、可稳定复现的bug:
- 第一次课下:LOOKUP算法中需要一个DIR变量记录当前电梯运行的方向,需将DIR在电梯到达顶层和底层时转向。
 - 第三次课下:策略类未初始化数组。
 
	线程相关bug:第三次课下中未成功关闭调度器,原因在于真正需要换乘时才能改变换乘记录变量transNum,而不是在判断阶段就修改,这有可能会产生错误记录,导致调度器无法关闭,程序运行超时。
(5)分析自己发现别人程序bug所采用的策略
嘿嘿,俺这次一个bug也没找到,嘿嘿。
• 列出自己所采取的测试策略及有效性
 我的测试策略:
- 构造功能性测试数据,比如关注承载人数上限、可达楼层等等基本性质的测试。
 - 针对多线程测试。主要就是数据的性质和投放特征。数据性质比如两个楼层来回个十次八次的,专找那种特殊楼层的;投放时间就结合着代码内容,根据潜在存在的线程安全设置投放数据的频率和时间。
 - 有效性嘛,只用此法找到了我的一处bug。
 
• 分析自己采用了什么策略来发现线程安全相关的问题
 对于线程安全的测试没有什么太好的方法,除了多跑几轮数据之外,就是采用工具模拟压力测试环境。除此之外就是阅读代码,重点关注同步代码块之间的关系。
• 分析本单元的测试策略与第一单元测试策略的差异之处
本单元除了基本功能测试之外,和第一单元测试的差异主要在于:
- 第一单元是静态测试,而本单元是动态测试,需要构造带有时间戳的数据,并且测试时需要具备定点投放数据的能力。
 - 第二单元测试涉及到多线程问题,诶诶同样是数据,人家第一单元就能稳定复现,第二单元就不行,真丢人真丢人!这还叫测试吗!
 
(6)从线程安全和设计原则两个方面来梳理自己在本单元三次作业中获得的心得体会
好的架构是一切的基础,而好的架构源自于优雅的设计。
软件工程的一般流程是需求分析→软件设计→伪代码实现→具体实现(具体术语俺忘了,大概就这意思)。
作业中大部分需求分析已经体现在指导书上了,这一步我们只需要读懂指导书。我们核心的目的是通过对具体任务的软件设计,锻炼自己的系统能力(高小鹏)。在纠结与权衡中召唤出一个既能完成任务又能支持后续开发、既实现高内聚又体现低耦合、既支持SOLID原则还能开闭原则等等一大波乱起八糟原则的架构大邪神。
然后还不能直接写代码,你还得把脑子里的架构转化成伪代码架构,也就是整个工程要先搭个雏形出来,至少让那帮外行觉得看上去能跑。
最后菜是具体实现,具体实现之前害得要先沐浴焚香,斋戒三日,理顺气息,才能在伪代码架构的基础上一气呵成,犹如(此处打一形容一气呵成的诗句)。
问题是优雅的设计也不是想召唤就能召唤的,除了练习之外,提升设计能力最有效的办法就是
线程安全是效率和风险间权衡的结果
具体思路和方法前面说的够多辣。下面就是心法时间,悟到多少全看你造化。
 线程越安全当然越好,老子恨不得全给他同步了,但是强制给每块代码加上锁嘛又显得水平太次,所以优雅的架构就显得十分必要了。好的架构当然不是第一时间解决线程不安全,而是直接回避出现不安全的可能——就像这次作业给每个电梯加一个请求队列——效率差就差点,谁让程序员都是懒狗呢。
 如果嘛,真到回避不了的时候,这才轮到计算机历史上那些闪着光辉的名字登场——让这帮天才提出解决方案来解决这些潜在的线程不安全问题——看来这是下下策啊。
	写到这我都快感动哭了,维特根斯坦说天才不济之处你看见才能,原来费劲巴列要解决线程不安全的这帮天才只能算扁鹊神医,全上sychornized的人才是防患于未然的扁鹊他哥啊!
                    
                
                
            
        
浙公网安备 33010602011771号