前言

  在继上次表达式求导的三次作业后,我又完成了多线程电梯问题的三次作业,感官上,多线程的作业难度要弱于表达式求导,但是实际上,多线程电梯问题,因为考虑到线程间的通讯,以及线程安全,实际上要困难一些。

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

 

设计策略

  第一次作业——单个无调度策略的目的选层电梯:

  第一次的作业只是做一个没有任何调度策略的目的选层电梯,因此在设计上并没有动脑子(捂脸),采用单例模式建了一个名为User的类用于存储输入的请求队列,建了一个名为Elevator的线程类和名为Main的主函数类,输入是在主函数里完成的,采用的策略就是,在主函数中创建并启动电梯线程,然后主函数不断地往User单例里面写数据,电梯不断获取请求,就是一种非常傻瓜的写法。

 

  第二次作业——单个采用ALS(捎带策略)的目的选层电梯:

  这次作业我采用的策略是让电梯一层层地前进,并且由电梯主动询问调度器是否有用户可以稍带,而在电梯地运行轨迹上我遵循楼道间一般电梯地运行方式,即当电梯上行地时候,不接受任何下行地请求,同理,下行不接受上行地请求,因此电梯将会在楼层间不断地上下,理论上对于一定地用户数量,这个调度方法拥有一个固定值。

  为了实现这个策略,我建立了用户类和楼层类,两个类之间用Arrarylist结构联系在一起,在查询的时候,顺序为该楼房第几层,第几个用户的请求是什么。在此基础上,我在Scheduled类(调度器类)里建立了两个楼房队列,一个表示上行,一个表示下行,同时我把Input线程从Main中摘了出来,Main只承担初始化和启动线程的工作。而在Evelator类里,电梯有一个属于自己的队列,这个队列对应楼层的每一层,存储目标为该层的用户,同时,电梯的行为仅有自己控制,也就是说Input和Scheduled无法直接修改电梯的运行方式。电梯与Input之间通过Scheduled建立联系,Input获取外界请求,Scheduled负责把数据分类处理,Evelator从Scheduled中请求数据,判断线程状态等。

  总体上来说,我通过Scheduled将Input和Evelator基本上完全分隔开,因此在实际操作的过程中,我只保护了Scheduled的读取和修改方法就实现了线程的保护。

 

  第三次作业——多个采用ALS(捎带策略)的带有不同属性的目的选层电梯

  相比于第二次作业,这次作业加入了电梯的限制,包括电梯的停靠楼层限制、人数限制、不同的楼层运转时间。但是本质上,和第二次作业并没有大的差距,第三次作业,我重构了用户类,第二次作业用户类我采用了字符串存储数据,显然,这种数据模式对于使用类的人来说并不友好,因此我在User类的下一层加入了Contaner类,该类用于应对分层时可能出现的多条语句,剩下的架构基本和第二次保持一致,只做了如下的修改,Evelator类在第二次的基础上添加了属性初始化,Main函数增加了一部分初始化数据的生成,Scheduled类里增加了需要换乘乘客的静态处理方法。

  在线程安全的处理上,因为增加了换乘乘客,因此在限定线程结束上修改了一部分,其他并没有变动。

  总结:

  个人感觉这次作业,从第二次开始,架构就设计得比较好了,基本实现了总的设计思路,即电梯负责跑,输入负责输入,调度器负责数据处理,把类的功能基本上实现了独立和分割,这使得我第三次作业较轻松地完成了。而在线程安全上,我使用的是synchronized,先后尝试了锁方法,锁类,锁类的一部分,锁对象的一部分,最后为了保险采用了锁整个类的方法,不得不说这种锁的确很低效。除此以外,除了第一次作业采用了单例模式,我后两次作业均使用了传递参数的形式,我始终觉得单例模式不够直观。

 

基于度量分析代码

ev(G)基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。

Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。该数值越高,说明模块的耦合度高,难以复用。

v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。  

注:上述引用自https://www.cnblogs.com/panxuchen/p/8689287.html

 

第一次作业 :

  类图:

  多线程电梯问题单元总结 随笔 第1张

  度量分析:

       多线程电梯问题单元总结 随笔 第2张

多线程电梯问题单元总结 随笔 第3张

  自我点评:

  从上图,很清晰,我的第一次作业设计非常简单,就是单纯为了完成作业在main里,而且方法数量非常少,在evelator里面甚至只有两个方法,一个是为了简化sleep的函数,另一个就是run,可以说除了简单明了之外,这个设计一无是处。

 

  根据SOLID原则分析:

  我的三次作业都不会涉及里氏替换原则和接口隔离原则,因为我压根没有使用继承和接口。而仅就这次作业分析,开闭原则几乎没有遵循,如果我想在这次作业基础上实现后几次作业,那么重构是必然结果,而单一功能更不可能,因为我只有三个类却实现了电梯,调度器,还有输入,显然电梯等的功能,根据单一功能原则应该分离开。

 

第二次作业 :

  类图:

  多线程电梯问题单元总结 随笔 第4张

  度量分析:

  多线程电梯问题单元总结 随笔 第5张

  自我点评:

  从度量分析图来看,我的这次设计不考虑类,只看方法,方法的耦合度相当低,预防错误所需要的测试路数也非常少,只有少部分方法在维护的难度上存在问题,从数据来看,这次的设计相当成功。从UML类图可以清晰地看出我的调度器的类的结构,我把调度器,输入,电梯,以及主函数的功能完全地分开了,彼此之间实现了完全的独立功能。但是,这次的设计实际上局限很大,这次作业我当时并没有考虑扩展性,因此在Onewayticket这个类中,把多个用户数据用字符串的形式结合起来,这种设计,理论上适应性非常高,但实际上,对代码的维护和改变影响很恶劣,这是我当时没有考虑的。

 

  根据SOLID原则分析:

  此次作业相比于第一次,在单一功能原则上实现了大的突破,虽然没有达到标准的一个类一个功能,但至少,一个方法一个功能,实际上如果利用类似于宏的结构化,代码的模块化可以进一步增加。关于开闭原则,User涉及的类实现的较为糟糕,主要原因在于字符串的拓展着实麻烦,但是对Evelator类的实现可以说完全符合开闭原则,具体将在第三次作业介绍。里氏替换原则和接口隔离原则,还是没有涉及相关内容。

 

第三次作业 :

  类图:

多线程电梯问题单元总结 随笔 第6张

  度量分析:

  多线程电梯问题单元总结 随笔 第7张

  自我点评:

  第三次作业实际上是第二次作业改的,作业二改(作业三)继承了作业二所有的优点,并且在此基础上,关于User的改进使得User获得了更好地拓展性,同时User的数据形式使数据的获得和分析更加简洁。在设计之初实际上考虑过工厂模式,但是感觉对于只有三个电梯没有必要,实际上如果尝试一下,代码应该会简洁很多。在度量分析里,有个叫做gettranfer的方法,圈复杂度过高,实际上问题并不是处在这个方法上,而是出在schedule这个系列的方法,这个方法的添加主要用于用户的换乘,里面写的是很麻烦的换乘方式,这里我采用的是静态的逻辑,实际上,这种设计无论是性能,还是实现都是下等,唯一的优点就是不怎么费脑子,应该采用带权图之类的东西做更好一点。

 

  根据SOLID原则分析:

  因为第三次作业又可以叫做第二次作业改,所以这部分分析是一样的。我的设计在开闭原则上实现的非常好,对于schedule类,不考虑重构底层数据结构造成的转变,我只添加了一个用于用户换乘的方法,而对于Evelator类,我只添加了一个初始化构造方法,可以说,我认为实现的已经是非常好了。

 

自我BUG分析

  本次电梯系列作业,我遇到的BUG种类非常少,三次作业有两次进入互测没有被HACK,还有一次因为重大逻辑错误,导致没有进入互测。BUG分为两类,一类是线程安全及协助BUG,还有一类是逻辑BUG。

 

  线程安全及协助BUG:

    public boolean endthread() {
        synchronized (this) {
            if (waitforup == 0 && waitfordown == 0) {
                if (sign && waitfortranfers == 0) {
                    return true;
                }
                try {
                    wait();
                }
                catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (waitfortranfers == 0
                    && waitfordown == 0 && waitforup == 0 && sign) {
                return true;
            }
        }
        return false;
    }

  这个方法位于第三次作业的Scheduled类,从第二次作业,所有的安全控制都在Scheduled类中,上面的代码作用是判断当前的电梯线程是否需要挂起或者结束,上面是最终的正确版本,这个BUG我改了两次,一次是没考虑中转的用户进入电梯后还有可能出来的问题,这使得用户还没出电梯,换乘电梯的线程就已经结束了;第二次是,改了第一个BUG后,电梯线程没法正确结束。

 

  逻辑BUG

    private User schedule(int id,int from,int to) {
        ArrayList<Container> temp0 = new ArrayList<Container>();
        int fromele = whereyouare(from);
        int toele = whereyouare(to);
        int tranfers = 3;
        Boolean reverse = false;
        if ((fromele & toele) == 0) {
            tranfers = gettranfers(from,to,fromele | toele);
            if (from < tranfers && to < tranfers) {
                if (from > to) {
                    tranfers = 3;
                }
                reverse = true;
            }
            Container container = new Container(tranfers,ele(fromele));
            temp0.add(container);
            Container container1 = new Container(to,ele(toele));
            temp0.add(container1);
            waitfortranfers += 1;
        }
        else {
            Container container = new Container(to,ele(fromele & toele));
            temp0.add(container);
        }
        User temp = new User(id,temp0,reverse);
        return temp;
    }

 

  这部分代码只是从Scheduled类里的schedule系列方法的一部分,这里出现了一个重大的逻辑BUG。我采用的是二进制编码来判断人员的出入电梯和换乘思路,然后我从逻辑上忽略了一种可能,这使得我再过了中测了,强测直接GG,只得了4分。

 

如何发现别人的BUG

  实际上,这部分我并没有合理地策略,我没有像那些大佬一样搞了自动评测机,而且因为忙于OS和自己的私事,实际上并没有多久的时间搞互测,互测采用的还是以前的老思路,先拿出自己的测试集,然后再编写一些逻辑上可能出错的测试集,测试逻辑和线程安全问题。由于这次时间不足,我甚至没有细看代码,针对性地编写测试集。下一次作业可以考虑编写一个自动评测机来实现自动化评测了。

 

心得体会

  这三次作业个人感觉,操作简单,理论复杂,设计苦难。多线程问题最突出的问题是线程安全问题。对于这个问题我体会良多。第一次作业,本质上是用多线程完成单线程任务,这里线程安全问题并不明显;第二次作业,线程之间共享资源的互斥是个巨大地问题,我从这里才开始学习synchronized的用法,我先后尝试了在不同的类里面锁相同的对象,但是,这种模式会使得检查安全问题的时候非常混乱,几乎不具有逻辑性,所以后来我把所有的锁都放到了方法里。最初的设计,为了减少锁造成的性能损失,因此我尽量锁了对象,而且锁的对象都非常细,这在结束线程的判断上出现了语法问题,因为结束线程理论上需要获取一整个完整对象的锁,结果锁的对象不一致,造成了矛盾,实际上这个问题,我觉得可以用一个锁的对象的记录解决,但是当时为了方便和安全,改成了锁一整个对象,这部分仍然可以得到改进。

  其次,大概就是设计原则上的问题了。我认为一个类的所有行为应该都算它的方法,而且,对于这系列作业,并没有其他的类具有相同的电梯的功能,因此我并没有完全按照单一功能原则实现,但对于大的类,类的功能是完全不重叠的,而对于类的内部,针对这一系列作业,采用了低耦合的做法,而且遵循了开闭原则,这使得我第三作业的完成相当轻松,基本上就是copy。

  再次,关于程序正确性的检测,这次我感慨良多。第三次作业得益于第二次作业的完备设计轻松地完成,这使得我并没有针对第三次作业进行覆盖性测试,只进行了部分逻辑的弱测和推演,结果导致出现重大逻辑失误,使得强测几乎没有分数,而实际上的改正只用了6个字符,这使我在难过之余也开始正视覆盖性测试的必要性,还有,一个人最大的敌人就是懒惰和傲慢,只有谦虚才能前行。

 

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄