OO第二单元博客
一、同步块的设置和锁的选择
虽然我们学了很多种锁,synchronized隐式锁,lock显式锁等等,但我在三次作业中都只用了隐式锁。锁与同步块中代码之间的关系,也就是锁共享对象当且仅当同步块中代码对需要保护的该共享对象进行读写。
在第五次作业中,我的作业中的共享对象有requestQueue和elevator本身,前者在input与schedule,后者在schedule与elevator本身间交互。所以我给requestQueue和elevator的大部分方法都上锁。最后也是因为这种锁的设计出了一些bug,详情见bug分析。bug修复时,我选择requestQueue都是方法锁,但没有给elevator的方法上锁,而是在需要锁elevator的代码块特别地上锁。但这种情况下特别容易忽视某些需要上锁的地方,我也确实因为这种情况下锁的不全面而出bug了,但借助第五次的bug修复能无限次提交程序,最后也基本上保证了这种上锁不出bug了。
第六次作业与第五次作业相比,我的程序的架构基本一样,唯一多的部分也就是elevator可以与requestQueue交互。上锁依旧采用第五次作业bug修复的方法,requestQueue上方法锁,elevator不上方法锁。
第七次作业与第六次作业相比,架构就是没区别。只不过此时我对于给方法上锁,以及给代码块上锁的理解加深,我为了代码的整洁统一,统一使用方法锁,最后在锁上也没有出现bug。
二、程序分析
调度器设计:
我三次作业的调度器设计主要参考的就是第三次实验的代码。调度的主要问题就是schedule从requestQueue中取出一个Request后,如果是PersonRequest,考虑分给哪个电梯,如果需要换乘,如何处理。其他两种Request很好处理,就不细分析了。
我的程序中的线程只有三种:
input获取输入线程:将request放入requestQueue,结束时为requestQueue置isEnd为true
schedule调度器线程:处理从requestQueue得到的request,如果PersonRequest则根据某种策略将其分配给合适的电梯;如果是MaintainRequest,处理对应电梯线程;如果是ElevatorRequest,添加新电梯线程。
elevator电梯线程:只处理各自的等待队列,捎带策略为ALS,根据主请求运动。
requestQueue是三者共同的共享对象:input与schedule之间,schedule与elevators之间的托盘,有isEnd,MaintainNumber,changeNumber(第七次)控制线程什么时候全部结束。
schedule只有从requestQueue中获取第一个Request,和给elevator分配PersonRequest的职责。
调度策略:
我的调度策略很简单,三次作业都一样。schedule中有elevator,elevator有接受一个PersonRequest返回elevator是否适合被分配该PersonRequest的方法,遍历所有电梯,如果都不适合分配,那么就找到所有电梯中等待队列人数最少的电梯分配该人。
该方法判断的标准也很简单,当前电梯对于起始楼层和目标楼层可达(第七次),电梯内人数少于最大载客量时,电梯处于等待状态或者电梯运动方向与该请求运动方向一致,且请求的初始楼层位于电梯移动方向上,则返回true,其他情况返回false。
其实这种策略是很偷懒的,没有很好地满足性能要求,没有考虑电梯当前楼层与起始楼层间的关系,不算一个好的调度方案,但也一定程度上也有很好的捎带性能,第七次强测在错了一个点的情况下也有92分。也很好写,哈哈哈哈。
架构设计图:
第五次:
第六次:
第七次:
协作图:
至于线程协作和主线程有什么关系?主线程只是用于实例化这些线程,schedule里面也有实例化新的电梯线程的方法。
稳定的内容:
观察我三次作业的类图,其实不难发现我三次作业的内容都很稳定,新的内容只用增添,很少在原来的基础上修改,最多在原有内容上修改或新增一些逻辑判断。变化较多的也就只有Schedule和Elevator。
易变的内容:
schedule中对于请求的处理,每次有新的请求都需要增加选择的内容。我想可以把请求的处理内置到各个请求类本身,为类的共同方法。即Request为一个接口,实现deal(),各种请求如PersonRequest,MaintainEleRequest,AddElevatorEleRequest,实现各自的deal(),为deal传入Elevators,作为必要的唯一的参数。这样schedule就可以稳定不变。
elevator运行中随着每次迭代都得多些情况,但我暂时没有什么想法可以综合它,使之稳定。
三、程序bug分析以及多线程程序debug方法
第五次作业中,我的程序出现了较为严重的bug,强测只得了14分,错误都是REAL_TIME_LIMIT_EXCEED。分析后,发现问题在于我的电梯线程在move、open、close的sleep时间内仍然占有电梯线程的锁,导致schedule无法在此期间内为电梯分配人员,导致几乎电梯每次只接送一个人。当人员较多时,就无法在限制时间内送完所有人,从而导致超时。bug修复我也是把sleep从Synchronized块中移出即修复了bug。
但其实这种写法我还有线程不安全的bug,在多次提交互测后才发现,问题出在读写冲突上,最后也是给读部分加上锁即解决。
第六次作业中,我的程序出现了小bug,强测错了一个点。仔细分析后发现问题在于有电梯maintain时,又恰好需要结束时由于线程随机性导致的不可复现bug。电梯正在returnRequest但还未返回到RequestQueue时,schedule判断RequestQueue为空,且isEnd为true后,电梯完成returnRequest,maintainnumber为0,schedule进入结束状态,但RequestQueue并不为空,不应该结束。最后的bug修复也只用将maintainnumber等于0的判断提到最前既可。
第七次作业中,出现了两种bug,虽然只错了一个点。第一个bug是schedule中由于当requestQueue满足一定条件时,schedule无法通过getFirstRequest()暂停,导致轮询,从而cpu超时。第二个bug是由于对于换乘人员的一些处理出现错误,导致某些情况下线程结束的条件无法满足。
四、心得体会
线程安全的角度:
线程安全问题在本单元作业中可以算是一个既简单又困难的很重要的问题。
说其简单,是因为只要理解了对多个线程之间的共享对象的读写,写写两者都是会产生冲突,然后在需要保护,可能产生冲突的地方上锁即可,理论上工作量极少。
说其困难,是因为就算只是一个地方少了锁,程序就可能出问题,而且还很难找到哪里少了锁,找缺锁的地方这一工作工作量极大。
说其重要,是因为线程安全是程序正确的基础。只要线程不安全,就会时不时地产生不可复现的bug。我认为设计多线程程序的第一步也是最重要的一步就是考虑线程安全问题,要考虑如何架构起我们的程序,能够保证线程安全。
层次化设计的角度:
在我看来,本单元的作业是能够比较好地体现层次化设计编程思想的。从input输入,Schedule分配处理,Elevator最终运行,requestQueue作为它们之间的桥梁。各个模块之间层次分明,便于迭代开发。
本单元作业较为成功的迭代开发,也让我从如何做到层次化设计,做到在迭代开发中保持高内聚、低耦合,中总结出两点。
第一点是对类的职责划分要清晰、明确。我们不要给一个类赋予过多的职能,也不要让类的职能产生重叠,也尽量减少类的职能之间的依赖,只传递必要的参数。
第二点是充分复用前次工作的成果。如第三次作业中,对于要换乘的乘客,我选择为乘客请求新增一个finalFloor属性,电梯运行时仍然如往常一样只需要乘客请求的fromFloor,和toFloor,这部分在迭代开发时就完全不需要修改,只在乘客请求从电梯队列离开时当finalFloor不等于toFloor则返回该乘客请求给requestQueue。