• 北京航空航天大学2019年OO课程第二次总结


    北京航空航天大学2019年OO课程第二次总结

    ​ 上次作业是大学近三年以来第一次正式地编写java程序,这次是大学近三年以来第一次正式地编写多线程程序,作为一名计算机专业的学生这似乎同样难以启齿。实不相瞒,从操作系统的PV操作开始,到数据库的访问安全性,多线程犹如一把达摩克里斯之剑,一直让我十分痛苦。还好OO课程提供了详实科学的教学,使我有机会系统性的学习和理解多线程问题。

    一、单电梯简单调度程序架构设计

    ​ 第五次作业要求完成一个简单的能运行的单电梯模拟。不难看出主体为请求抓取类(生产者)和电梯类(消费者)。请求抓取类负责从抓取并分析输入,生成PersonRequest类,电梯负责读取解读好的请求信息并运行。

    1.1 请求执行和请求获取的思考

    ​ 对于第一次电梯作业设计,只需要让电梯依次执行每个PersonRequest即可。然而这样的架构呆板且难以扩展——就算有算法可以依情况调整PersonRequest的顺序,电梯一次只能原子地执行一次拉人-运行-放人的任务,运行的前后和运行的过程中电梯无法干任何其他指令。

    ​ 思考一番后,我认为电梯的行为可以分解为更小的粒度。电梯其实只能执行两类指令——上下楼层指令和进出人员指令。这样抽象出来后,电梯的行为就更好管理了,如果中途有其他乘客需要上电梯,只需要插入相应的指令就行了。

    ​ 最终我设计了一个调度器类,这个类提供一个getOrderFafs()的方法,电梯通过这个方法从PersonRequest中抓取请求并翻译为相应的Order指令序列。Order类用于表示电梯一次能够执行的一个原子操作,内部有属性区分移动和开关门两类原子指令。

    ​ 完成之后,呈现的架构如下:

    Elevator类通过Scheduler的方法从请求池Requests类里获取并分析请求,转换为Order序列后再执行;InputPuller类不断获取控制台信息并分析为PersonRequest,装入请求池中。Resource是工具类,负责实现通用的静态方法和静态属性。Order类的设计不是很好,我使用LinkedHashMap来装载成对乘客ID和乘客属性(进or出),后来看来是没有必要的。

    1.2 多线程的同步控制

    ​ 这次作业仅有两个线程(除了主线程),电梯线程和请求拉去线程共同访问请求池中的资源,因此需要在请求池类中做同步处理:

    public class Requests {
        private static ArrayList<PersonRequest> requests
                = new ArrayList<PersonRequest>();
    
        public static ArrayList<PersonRequest> getRequests() {
            return requests;
        }
    
        public void put(PersonRequest pr) {
            synchronized (this.requests) {
                requests.add(pr);
                requests.notifyAll();
            }
        }
    
        public PersonRequest take() {
            synchronized (this.requests) {
                if (requests.size() == 0 && !Resourse.isEnd()) {
                    try {
                        requests.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else if (requests.size() == 0 && Resourse.isEnd()) {
                    return null;
                }
                PersonRequest r = requests.get(0);
                requests.remove(0); 
                return r;
            }
        }
    }
    

    ​ 我选择单例模式,拿取和放置由请求队列上的锁控制:当当前请求队列为空,消费者等待,当生产者生产后唤醒所有在其上休眠的线程。这是一个非常简单的时序关系,在此就不赘述。

    1.3 停机处理

    ​ 停机问题我最初没有仔细思考,后来发现这个问题很值得认真思考。如何优雅的停机是很多多线程程序要面对的——终端发出停止信号后,不能简单的中止所有线程,因为可能有未完成的任务待执行完。这次作业我选择将停机信号做成一个请求(使用的null代表最后一个请求),当电梯收到null时则会退出run()函数。

    二、多电梯捎带调度架构设计

    ​ 第二三次作业分别提出了捎带和多电梯的任务要求,由于我两次作业架构完全一样(说明可扩展性好),故放在一起分析。

    2.1 线程数量与电梯模式的思考

    ​ 在电梯设计问题上,我认为有两条路——拉与推。即请求应该是电梯主动获取还是由某个专用的程序安排给相应的电梯。这里我选择傻瓜式电梯——一个“大脑”管控各个电梯,电梯只需执行自己的执行序列,而不关心任何有关捎带和调度的问题。这样的好处是思路清晰,电梯的行为被统一约束,易于管理。后来发现,这样做也有缺点,因为所有事情都交付调度器会导致调度器类异常复杂。

    ​ 决定好由中央控制电梯后,我将调度器类Scheduler设计为了一个线程,调度器线程负责从请求池中抓取请求,在分析后转化为Order序列插入合适的电梯中,电梯最后只需要执行指令并输出即可。

    ​ 最后呈现的架构如下(只显示主要类):

    ​ 每台电梯都有自己私有的指令序列,这个序列是和调度器共享的,同时调度器和请求拉取器共享了一个请求队列。调度器的getAvailableEle(PersonRequest) 方法实现了电梯的选择;split(PersonRequest) 方法实现了换乘(将一个请求拆分为一个执行和一个挂起的请求);insert(Elevator, PersonRequest)piggyback(Elevator, PersonRequest) 实现了对特定电梯的捎带。可以看出,所有调度和分析的工作都是由单独的调度器线程完成的,理论上提高了程序效率。

    2.2 多线程的同步控制

    ​ 这次我有两个共享队列——独立存在的Requests 和位于每个电梯内的Orders 。后者的同步控制也采用类似的方式:

        public void put(Order or) {
            synchronized (this.orders) {
                orders.add(or);
                orders.notifyAll();
            }
        }
    
        public void awake() {
            synchronized (this.orders) {
                orders.notifyAll();
            }
        }
    
        public Order take() {
            synchronized (this.orders) {
                if (orders.size() == 0) {
                    try {
                        orders.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                Order r = orders.get(0);
                orders.remove(0); 
                return r;
            }
        }
    

    ​ 其中,awake()方法用于在电梯下人之后通知调度器可以加入新的请求了。线程的时序图如下:

    2.3 停机处理

    ​ 这次不能向第一次一样简单的使用标志请求,因为结束时空闲的电梯虽然没有任务,但可能有未来的换乘任务需要执行,如果这时发送停机指令,会导致现在处于换乘第一阶段的乘客无法完成第二阶段。因此调度器需要在收到停机信号后等待,直到所有任务都被执行完毕,才能向电梯发送停机信号。

    三、基于度量的程序结构分析

    3.1 类和方法的分析

    ​ 使用IDEA的UML插件和代码分析工具DesigniteJava分析第三次电梯作业,得到如下结果:

    ​ 所有类的属性不超过8个,方法个数控制在13个以内,除去一些很短的构造、获取方法,总的来说规模控制的不错。从NOM和NOMP可以看出几乎所有的方法都是public方法,这似乎不太好,很多内部使用的方法应该定义为private的,提高程序的封装度。由于本次作业主要是多线程程序,扇入扇出就不予分析了。

    ​ 对于类的加权方法个数WMC (Weighted Methods per Class – Class),可以看到调度器类和工具类异常的高。具有高WMC的类一般可以重构为两个或者更多类。调度器类如前文所述,其在傻瓜电梯设计模式下确实存在过于臃肿的问题。对于工具类,其内都是静态工具方法,讨论WMC没有必要。

    ​ 对于每个类内方法的长度和圈复杂度如下:

    ​ 方法长度均在50行以下。但是调度器类的圈复杂度明显过高,电梯的简单带来的代价就是要求调度器有更复杂的逻辑。调度器每次执行都要考虑每个电梯状态、乘客状态、电梯运行走向等等,难以避免的写的很复杂。现在看来应该让电梯承担一部分调度的任务,电梯与调度器应该互相协作,又泾渭分明。

    3.2 基于SOLID原则的程序评价

    ​ S.O.L.I.D.是指SRP(单一责任原则)、OCP(开放封闭原则)、LSP(里氏替换原则)、ISP(接口分离原则)和DIP(依赖倒置原则)。

    SRP要求当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。 第三次电梯作业中我的调度器履行了过多职责,从捎带到换乘,再到电梯选择和停机,这些工作全部在一个类里完成,的确有些不合理。应当分解调度器类或者让电梯承担更多的责任。

     OCP要求软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的——所有的扩展应该是基于现有代码的基础上进行增加新的方法,而不是直接修改某个方法内部。在第二次到第三次作业的过渡中我很好的做到了这点,除了run方法,所有的修改都是在原程序上新增方法实现的(这也是我不愿意重构架构的原因,原来的架构很好的兼容新的要求)。

     LSP要求当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。这次我没有设计父子类就不予讨论了。

     DIP要求模块之间尽可能依赖于抽象,而不是模块之间的依赖,抽象不能依赖于细节。这次作业我简直是这个原则的反面教材。为了沟通方便,我各个模块之间的依赖非常严重,高层模块和底层模块互相依赖导致代码改动的风险很高(还好没有第四次)。现在想来,应该增加更多的抽象。

    ISP要求使用多个专门的接口比使用单一的总接口总要好。这次作业我也没有设计接口。

    四、BUG的分析与感想

    ​ 为了减少BUG、提高正确性,我从手动和自动两个角度进行调试。手动部分主要针对简单的分类和边界测试。自动测试主要使用python的管道等等工具。虽然随机生成测试点质量可能不高,但是数量足够多时也可以提高 程序的可靠性。

    ​ 电梯的三次强测我都没有BUG,但是线下自己测试时发现了一个线程BUG,这个BUG值得记录。

    ​ BUG的起因是我发现电梯偶尔会在同一层到达两次(输出两次同样的ARRIVE信息),这个BUG咋一看没头没尾,出现的非常奇怪。更可怕的是,该BUG无法稳定的复现,意味着这通常是线程问题。我为此首先花了很多时间尝试找出BUG出现的规律,在能够稳定复现后,我采用插桩法追溯错误原因,最后终于发现了问题所在:在电梯运行时(例如1到2楼),电梯还未更新自己的所在楼层(如还在1楼),这时如果发生线程切换,调度器会错误的判断指令序列里缺少1-2楼的指令从而添上,在电梯执行完一次1-2楼后又会执行一次,导致ARRIVE-2输出了两次。

    ​ 纸上得来终觉浅,绝知此事要躬行。debug时我深深感受到了多线程编程的不易和设计原则的重要性。线程之间永远不应该依照可能过期的数据做出行动,如果我认真履行各个编程模板,就不会出现类似问题。多线程的同步问题极难发现,也同样极难解决,在编程的过程中绝不能走先编程后DEBUG的老路不能先污染后治理),程序从设计伊始,就要认真依据happens-before原则以及check-then-act、read-modify-write等模板,梳理清楚线程的执行关系,设计好可靠不易出错的代码架构。

  • 相关阅读:
    & 【04】 Spring中Xml属性配置的解析过程
    设计模式之模板方法设计模式
    MySQL高性能索引创建策略
    oracle用户创建及权限设置
    【已解决】关于SQL2008 “不允许保存更改。您所做的更改要求删除并重新创建以下表。您对无法重新创建的标进行了更改或者启用了‘阻止保存要求重新创建表的更改’” 解决方案
    ObjectStateManager 不包含具有对“Model”类型的对象的引用的 ObjectStateEntry
    【推荐活动】脚本娃娃同城会——上海站(20130112)
    【原创】对于访问IIS元数据库失败的解决(续)
    【原创】win7 plsql里查询出来的中文信息,复制粘贴的时候出现乱码(以前从没遇到过,第一次啊)
    oracle删除用户命令和部分命令
  • 原文地址:https://www.cnblogs.com/parachutes/p/10763457.html
Copyright © 2020-2023  润新知