概述:
第二单元的主要内容是通过编写多线程程序来模拟电梯的运行。第一次作业是单傻瓜电梯,不考虑策略,基本是初步了解多线程程序的编写;第二次作业是单ALS电梯,在上一次的基础上要实现捎带,尽可能在最短时间内完成所有请求;第三次作业是多电梯,每个电梯可停的层和容量都不同,需要考虑乘客换乘的情况。在这三次作业中,我们的程序需要使用不同的策略将线程安全贯彻始终,以免出现与期望不符的BUG,我认为这也是多线程程序编程中的重点所在。
一、程序分析
1、第一次作业
(1) 设计策略
第一次作业要实现一个傻瓜电梯,思路很简单,可以看作一个传统的生产者-消费者的问题。考虑一个输入线程作为生产者,电梯线程作为一个消费者,维护一个线程安全的共享队列作为托盘(这里我使用了JDK自带的ConcurrentLinkedQueue)。输入线程每次向队列输入请求,无请求时结束;电梯线程的调度策略就是每次从队列中取出一个请求来完成,当队列为空且输入线程结束时结束。
(2) 度量分析
代码规模
类图
复杂度分析
本次作业需求比较简单,所以方法的复杂度都不高,最高就是5。
协作图
SOLID原则
本次作业输入线程只负责输入,电梯线程负责运行和输出,调度器作为共享对象被前二者共享,符合单一责任原则(SRP)。其他原则个人认为由于本次作业要求都无需考虑。
(3) BUG分析
本次作业在互测和公测中均未出现bug。
2、第二次作业
(1) 设计策略
本次作业在第一次作业的基础上要求尽可能快速的将所有乘客请求完成,即需要使用一些调度策略。为了求稳(懒),我使用的就是指导书上的ALS调度策略:规定一个主请求,在完成主请求的过程中允许捎带相同方向上的请求;在主请求被完成之后,根据电梯中是否还存在其他请求,从电梯或者请求队列中获取下一个主请求。由于本次作业和第一次作业除了调度策略,其他要求基本相同,故本次作业主要是在前一次作业的基础上修改了电梯的调度策略部分。
(2) 度量分析
代码规模
类图
复杂度分析
第二次作业中复杂度比较高的主要是pass方法,这个方法主要完成了在当前层和所有请求的交互,即遍历了请求队列和已在电梯中的请求队列。另外,work方法完成了一次主请求的执行,即从得到第一个请求执行到请求队列和电梯中都没有请求,这个方法可能功能分离实现得不是很好,所以复杂度也比较高。
协作图
SOLID原则
本次作业和上一次作业基本架构相同,符合单一责任原则(SRP),在本次作业中我没有考虑多电梯的可拓展性,所以我认为在开放封闭原则(OCP)上做的还不是很好,其他原则在本次作业中也无需考虑。
(3) BUG分析
第二次作业出现了一个bug,即当前主请求被完成后,若电梯中没有请求且下一个从请求队列中得到的请求起始层为当前层时,直接接入电梯同时设置一个Boolean量为true表明主请求已在电梯中。我忘记将这个标志量设为true,导致主请求被多次接入发生错误。按理说这个bug是非常弱智的,我课下也测试出来了,只是自己没有认真看输出结果,因为这个问题强测爆了5个点,总之就是非常后悔.jpg。
这同时也暴露出了本次电梯作业输出正确性肉眼难以准确判别的问题,于是在下一次作业中我使用了自动评测判断输出的正确性,效率提高了很多。
3、第三次作业
(1) 设计策略
第三次作业要求实现三台运行楼层不同的且载客有限的电梯模拟,由于每台电梯可达楼层不同,有些请求就需要换乘才能完成。本次作业要求的请求都可以通过一次换乘达成,故我在运送某个请求之前先判断它否可以被一个电梯完成,还是需要换乘一次电梯。若需要换乘,设置中间层为当前运行方向上最近的换乘层,若不需要换乘中间层设置为目标层。在乘客出电梯时,判断中间层是否等于目标层,若等于则请求完成,否则构造一个中间层到目标层的请求加入请求队列。对于单个电梯的调度策略仍使用上一次作业的ALS策略。但是由于是多电梯运行,电梯线程的结束条件需要更改为请求队列为空且输入线程结束且当前运行电梯数为0,故需要在调度器中增加一个量cnt表示当前运行电梯数,每个电梯开始运行时cnt加1,结束时cnt减1。
(2) 度量分析
代码规模
类图
复杂度分析
本次作业由于电梯类也需要输入请求队列和判断换乘,导致复杂度增大很多。在getex方法中需要根据当前运行方向判断出最近的换乘层,由于换乘情况较多导致方法复杂度很大。pass方法仍然和上一次作业一样,但是多了需要向请求队列输入的可能性。
协作图
SOLID原则
本次作业输入线程负责向请求队列输入,电梯线程可能会向请求队列输入和输出,我没有专门设置接口管理输入和输出,在单一责任原则(SRP)上可能做的不是很好。在开放封闭原则(OCP)上,我设置了一些参数例如电梯可达列表、载客量等参数,若有新的电梯加入仍可很好进行扩展。
(3) BUG分析
本次作业在互测和公测中均未出现bug。
二、互测策略
进入到第二单元的作业后,感觉互测的难度明显上升了。首先,题目要求的按时间输入就难以实现;其次,对于输出结果的正确性判定也不像第一单元一样,简单地调用某个库就能实现了,需要自己实现判断逻辑;最后,多线程的程序如果存在线程不安全,可能出现不可复现的错误,运行几十次可能错误一两次交上去也是空刀。可能由于以上的种种原因,导致这三次作业的互测,大家积极性都不高。
我在互测中使用随机数据生成器生成一些指令对每个人的数据进行测试,开始由于没有一个有效的评测机,用人眼判断效率极低。后来多亏室友帮助,写出了一个简单的判别输出正确性的辣鸡评测机大大提高了效率。利用正则匹配检测电梯运行状态和指令完成情况,最后通过和输入对拍判断输出是否正确,可能逻辑还不太完善不过基本够用。第一次互测大家都没有bug不必多说。第二次互测和第三次互测发现本地运行和评测机的结果很多时候有不同的地方,很多bug查不出来也有时本地没bug却刀中了人,可能这就是一种玄学吧。
三、收获与感想
通过这三次作业我从一个完全的多线程小白到对多线程有了一些了解,感觉多线程编程最重要的就是一定要注意线程安全!线程安全!线程安全!否则很多不可复现的bug可能会让debug过程变得非常痛苦。这几次作业中我基本都是用synchronized来实现线程安全的,使用时锁住对象比锁住方法能够实现更高的效率。在线程间的通信过程中,使用wait()和notify()比暴力轮询更好,虽然我最后还是用sleep()解决了问题。我还学到了一些多线程测试的方法(玄学),基本就是在代码的关键地方printf加上随机数据轰炸。最后,还是那句话,一个良好的架构是实现高性能的基础,一定要在线程安全的情况下在考虑其他的优化。