2019_BUAAOO_第二单元总结

一、从多线程的协同和同步控制方面、分析和总结自己三次的设计策略。

  在第一次和第二次作业中,由于电梯只有一部,所以我的程序只开了两个线程,类似于生产者-消费者模型。一个用来处理输入(生产者),另一个是电梯运行的线程(消费者),而串联二者的容器就是requestList,请求队列。在第三次的作业中,三部电梯一起运行,所以我又新开了一个调度器线程,是用于根据三部电梯可达楼层的不同来分配请求。

  在处理同步控制方面,在对请求队列等arraylist数组进行add, remove等操作的时候,为了避免线程不安全的现象发生,我使用了Collections类里面的synchronizedCollection方法。阅读这个方法,利用装饰模式根据传入的Collection生成特定同步的SynchronizedCollection,生成的集合每个同步操作都是持有mutex这个锁,所以再进行操作时就是线程安全的集合了。

 

二、基于度量来分析自己的程序结构。

第一次作业

 

  第一次的作业由于采用的是傻瓜电梯调度,即按照请求的先后顺序一个人一个人的接送,所以从逻辑上和实际设计上都是十分简单的,复杂度很低。由于是第一次多线程作业,所以在设计时只需要考虑基本的线程安全问题即可。  

 

第二次作业


 

  第二次作业加入了稍带人的机制,即ALS电梯,所以为了考虑稍带人、电梯进人的情况,需要在每一个楼层对请求队列遍历一遍,同时也要确定在该楼层是否有乘客到达目的地,需要出电梯。在本次作业中,我设置了两个List,一个是请求队列(可以看成电梯外的乘客),一个是电梯运转LIst(可以看成电梯内的乘客)。我的电梯算法并不是主请求算法,而是一种和我们日常生活中最常见的电梯类似。当有请求到来时,首先遍历请求队列,判断当前楼层是否有人按电梯,根据这些人的目的楼层来判断电梯的状态"up"or"down";在up的过程中,如果有人在当前楼层currentFloor以上,待定的运载方向是向上(tofloor > fromfloor),那么电梯在执行到这个楼层的时候回拉上这个人。如果电梯里此时没人,电梯也会延续up状态去该楼层接人。down状态同理。

 

第三次作业


  第三次作业涉及了三个电梯,难点在于电梯的可达楼层不同,有些请求需要换乘。我延续了第二次作业的电梯调度算法。如果输入线程接收到输入请求,那么就会notify正处于waiting状态的调度器线程,调度器线程开始分配请求给三个电梯,如果一个请求是一个电梯可以直达的,就直接分配给该电梯。在分配完直达请求之后,开始分配需要换乘的请求,我的思路是,同第二次作业的调度方法,如果该电梯顺路且可达起始楼层,则让该电梯去接这个请求,然后到达换乘的边界楼层就判断一下电梯内的人是否有该电梯不能直达的人,将其放下,重新设定该乘客的请求(只改变FromFloor即可)添加至requestList中,然后等待其他电梯来接该乘客。由于电梯线程的运行是延续了第二次作业的算法,遍历的次数多,所以复杂度仍然偏高;同时对于调度器线程来说,由于需要不停的运转来分配请求,所以复杂度更高。

  在这里简单介绍一下solid:

  1. 单一责任原则(SRP)

  当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。 类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题,非常耗时耗力。

  2. 开放封闭原则(OCP)

  软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。

  (1) 通过增加代码来扩展功能,而不是修改已经存在的代码。
  (2) 若客户模块和服务模块遵循同一个接口来设计,则客户模块可以不关心服务模块的类型,服务模块可以方便扩展服务(代码)。
  (3) OCP支持替换的服务,而不用修改客户模块。

  3. 里氏替换原则(LSP)

  当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系

  客户模块不应关心服务模块的是如何工作的;同样的接口模块之间,可以在不知道服务模块代码的情况下,进行替换。即接口或父类出现的地方,实现接口的类或子类可以代入。

  4. 接口分离原则(ISP)

  不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。 

  客户模块不应该依赖大的接口,应该裁减为小的接口给客户模块使用,以减少依赖性。如Java中一个类实现多个接口,不同的接口给不用的客户模块使用,而不是提供给客户模块一个大的接口。

  5. 依赖注入或倒置原则(DIP)

  (1) 高层模块不应该依赖于低层模块,二者都应该依赖于抽象 
  (2) 抽象不应该依赖于细节,细节应该依赖于抽象

  (以上资料来源于 https://blog.csdn.net/vichou_fa/article/details/52523617 )

  

  从SOLID原则来分析,我的程序在SRP(单一责任原则)方面做的比较妥当,由于采用类似于生产者-消费者的模型,所以每一个类关注的重点只有自己应该做的,类和类之间的关联并不是很紧密,是调用了中间量(requestList)来产生相互联系;在OCP(开放封闭原则)上,我也有做考虑,比如说电梯的状态(up,down,waiting),在处理电梯运行以及载人的方法中,我更多的关注于方法拓展的可能性,而不是重新修改,所以只需要根据电梯当前运行状态的不同来决定此时电梯应该做什么样的操作,而不是写很多方法来对不同状态的电梯作分别处理;因为没有涉及到继承和接口的问题,所以剩下的三个原则暂不讨论。

   

  在第一次作业的设计中,我觉得问题不是很大。在第二次的作业中,我在电梯运行的线程中的某些方法的设计上还是有一定的缺陷,也有一些冗余的判断以及不恰当的循环嵌套,这可能也是导致该方法复杂度偏高的一个原因。第三次作业由于延续了第二次作业的调度方法,所以复杂度偏高是一个缺点,另外还有一个缺点则是我建立了三个电梯的类,但其实完全可以只设计一个电梯类,构造三个不同的电梯对象,这样可以增强代码的可读性以及减少空间上的开销。如果使用这种方法设计,则换乘方法的设计上也需要重新考虑,比如当一个请求无法通过一个电梯来直达完成任务时,可以在调度器线程对其直接进行split分割操作,将其拆分成两个请求,然后可以设置一个标志位或遍历操作等手段来确保前者请求(换乘的乘客乘坐的第一个电梯)完成后,再执行后者请求(换乘后的电梯)。同时,在线程安全上也需要多考虑,比如对每一个方法用synchonized修饰等等。

 

三、分析自己程序的bug

  在三次作业中,我没有在强测或者互测中被找出bug。所以在这里我简单谈一下在中测的提交中我发现的自己的一些bug:

  • 首先是线程的启动问题,如果在一个线程开始运行的时候,它所需要访问的对象是还没有构造new出来的对象的话,则会出现错误,所以一定要先构造出共享的资源之后再构造线程。
  • 线程的中止问题。在前两次的作业中,我采用的方法是,当输入线程接收到ctrlD的终止信号时,将共享对象exitFlag设为true,而电梯线程的run方法的while循环的判断条件是,电梯里有乘客或者请求队列还有乘客或者exitFlag是false,满足条件之一则该线程就不会中止,在while后紧跟判断两个队列是否为空再运行,这样避免了收到ctrlD的终止信号电梯立刻停止,但是还有乘客没有运送到目的地的情况。在第三次作业中比较复杂的是有三个电梯线程,并且还多加入了一个调度器线程,以及还要考虑换乘的问题,所以终止的顺序就很关键。我的思路是,当三个电梯都处于waiting状态的时候,并且收到了ctrlD产生的终止信号,则调度器线程停止。如果三个电梯有没处于waiting状态的,则该电梯有可能会出现换乘的情况,也就是说要重新添加一个新的请求,然而若此时调度器线程没有判断三个电梯的waiting状态,则调度器线程已经停止,没法处理新加进来的换乘请求,所以会出现错误。所以判断电梯线程的状态是十分重要的一部,确定此时没有任何电梯在运行了之后,才可以向电梯发送终止信号,电梯线程终止。

  • 换乘捎带的问题。这个bug出现是由于我自身程序的算法的问题。举个例子,比如如果有人要从2楼到3楼,由于没有电梯可以一次性的处理这个请求,此时我的算法会让电梯A捎带这个人,然而2楼已经是A电梯可达楼层的边界,所以A电梯拉上这个人后,会在把这个人放到2楼,然而A电梯重新遍历,还是会拉上这个人,以此循环,OPEN-IN-CLOSE-OPEN-OUT-CLOSE,出现死循环。同理该bug也出现在3-4,4-3,3-2三种请求中,根本原因还是因为3楼是一个特殊的楼层,只有电梯C可以到达,并且电梯C的3楼是其边界楼层(电梯C的每一个可达楼层都可以看成是一个边界楼层)。于是我的处理方法是对这三种特殊情况进行特判,比如2-3楼,我就将其拆分成2-1,1-3两个请求,让不同的电梯来处理这个请求。

 

四、分析找别人程序bug的策略

  在这三次作业中,我没有在互测阶段找到别人的bug。在测试别人的程序的时候,我会先使用一些我自己在测试自己程序时候发现bug的测试样例,首先是最简单的样例(如上文所题的换乘问题),其次是自己造出40条请求,一次性的输入,判断其程序是否会出现线程不安全,或者换乘情况考虑不周的问题。最后我会分批次输入,隔几秒输入一次,或者是在所有线程处于waiting状态的时候再次输入,用来判断其是否正确处理了线程结束的问题。

  在判断其输出的正确性的时候,只用眼来判断是不太现实的,很费时间和脑力。在这里我借用了同学写的mini评测机,将标准输入和标准输出文件和用输入到mini评测机的程序(jar文件)中,然后可以判断每个乘客是否送达至目的地,电梯里是否还有人,以及其换乘的路线是否正确。

 

五、心得体会

  这是我第一次接触到多线程的概念,在编写代码的时候需要严谨的考虑线程安全与同步的问题。这也让我更好的理解了并行和并发的区别,对于我理解os操作系统课上所讲的进程的切换、同步原语、自旋锁等问题有了很大的帮助。

  在处理线程安全的问题上,由于synchonizedCollection方法的使用,让我的代码的安全性可以有了初步的保障,并且不会在很多处理list的操作的时候加锁,造成代码可读性变差的现象,同时方法的行数也会变多,很难保证一个方法的代码控制在60行以内。但这其实对于我理解同步、锁的概念并不是好事,因为走了这个捷径所以吃过的亏也少,自己亲自动手修改线程安全的bug的机会也变少了。但是上网查阅synchronizedCollection代码的原理之后,我也理解了其大概的操作是怎样的,有这个很好的方法加以利用会提升自己coding的生产力。

  在设计原则上,基本上是延续了生产者-消费者的模型。在第三次作业中,由于新加入了调度器线程,所以在测试的时候出现了很多前两次作业没有出现的bug,但询问了其他同学之后,发现调度器线程其实也不是必要的,可以不把它作为一个线程,只是用其来拆分需要换乘的请求即可,但这样的话会降低运送性能,可能不会找到最佳最快的调度方法。我虽然使用了调度器线程,但是也并没有利用其优势,反而不太像是一个线程,完全可以和输入线程进行合并,一有输入进来就对请求进行判断、拆分。

 

  总的来说,虽然在这三次作业中踩了很多坑,但是这更好的帮助我理解了线程的安全性,以及锻炼了我分析寻找并修改自己bug的能力。在找bug的时候由于多线程debug比较麻烦,并不直观,所以大多数情况需要自己用大脑来分析、思考,而不是通过debug窗口,对于我自身能力的提升也是很大的,在今后编写多线程的代码也会更加严谨、高效,避开这些曾经踩过的坑。

posted @ 2019-04-23 17:06  zja1999  阅读(176)  评论(1编辑  收藏  举报