多线程电梯设计的总结与反思

Posted thunderzh-buaa

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程电梯设计的总结与反思相关的知识,希望对你有一定的参考价值。

 一、FAFS电梯设计

这是第一次使用java多线程,主要的问题主要集中在两个方面

1、共享资源的数据同步

2、整体架构

先考虑第一个问题:

数据同步的问题显然可以使用synchronized解决,也就是经典的生产者消费者模型。

但是由于初次接触,对锁机制理解不清,我还探索了一种不那么好的方法——volatile,具体使用方法见Whai is volatile

它显然能用在这个问题的解决,但是其实volatile带来的问题很多,比如性能损失,也不具很好的普适性。

第二个问题:

FAFS在调度策略的实现是非常简单的,因此更多思考留给了如何设计一个良好的架构使得其策略易调整,电梯易拓展等问题。

我在这次的设计中,更多得考虑到了策略的可延伸性。

我认为所有的调度无非就归结到两个方面,每个楼层有无请求,电梯up or down。

所有的请求一定是人在确定的一层上发出的,因此与其考虑维护所有请求队列,不如维护各个楼层请求。这给调度时候的扫描判断带来一定的便利。(后来我发现这样做有个问题:如果只维护楼层请求,只针对当前楼层调度,会使调度策略具有局限性,可能全局考虑才可以做出更好的优化)

3、两种实现方式的对比

1、volatile

技术图片

 

技术图片(2)Synchronized

 

相同点:

Ask类是自己封装的一个请求queue类,主要是解决java自带Queue类的功能不能完全满足我的设计需求的问题,这里设置的flag是为了exit而特意准备的。

Elevator是电梯类,设计缺陷在于未考虑继承,调度策略全盘体现在goTo方法中。应该考虑针对openDoor(),closeDoor(),printOut()等电梯基本操作作为一个父类,针对后续不同的电梯进行继承并补充。第二次作业也考虑到了这个问题。

InputHandler是做输入处理,是三次作业几乎一直沿用的一个类

不同点:

Elevator 和 inputHandler共享volatile修饰的askList,两个类都可以操作同步的askList。

Elevator看作producer,inputHandler看作consumer,利用一个Tray类做线程的数据同步。其中使用wait/notifyAll来解决consumer反复轮询造成的性能损失,是有利的。

 

二、AlsElevator设计

这大概是真正java多线程的入门,第一次作业一直在探索阶段,一直在查阅相关的资料,进行了学习传送门

技术图片

 技术图片

这一次算是正式引入了我自己楼层请求的想法和生产者消费者模型。

先来谈一谈楼层请求的思路:

Floor给每个楼层设置了askIn,askOut,askQueue队列,分别储存该层要求进入的请求,要求出去的请求,所有请求。

input后只需把相应请求put进对应楼层以及总请求队列

public void put(PersonRequest request) {
      ...
       askQueue.add(request); //总请求队列
       askIn[floorTrans(request.getFromFloor())].add(request); //from floor 的 in 队列
}

每次到达一个楼层,只需遍历该楼层的in、out队列即可(只谈Als,不考虑其他更好的策略)。并且in之后,需要把相应请求给到目的楼层的out队列,实现的大体框架如下

loop:
arrival();
checkInOut();
FloorAdd();
?
private boolean checkIn() {
  ...
   long lastDate = new Date().getTime();
       while (abs(new Date().getTime() - lastDate) < 400) {
           if(ifTake()) {    //捎带判断
              ...
               askQueue.remove();  //移除
               askOut.add();       //相应楼层增加out请求
              ...
          }
      }
}
?
private boolean checkOut() {
  ...
   // scan askOut
   printOut();
   askOut.remove();   //相应楼层移除out请求
  ...
}
?
private int checkInOut() {
   checkOut();
   checkIn();
}

如此之下,我们只需(1)设置好mainRequest (2)不断update loop的最极限楼层 (3)捎带条件判断

之后,一个完整的Als电梯就大功告成了。

个人认为这种楼层请求思路对Als的实现还是相当友好的(但可能仅限于Als)。

结构安排

Elevator类实现电梯各种最基本操作,开关门,到达...

AlsElevator类继承基本的电梯类并实现了Als调度算法

inputHandler处理输入请求。

Tray作为托盘,协调AlsElevator和inputHandler,进行put,get操作。(生产者消费者模型

Floor包含所有楼层的in、out请求,放置在托盘中,供输入线程和电梯线程实时操作。

复杂度分析

技术图片

技术图片

复杂度问题主要在于AlsElevator电梯,查看方法复杂度发现:

技术图片

技术图片

问题在于

getIn方法:

openDoor()分散在方法中,导致反复判断,应该额外操作

反复遍历tray中Floor存的队列

电梯能去的极限楼层的判断没有独立出来。

goTo方法:

楼层的循环采用for并且反复修改fromFloor 和 toFloor

ifTake方法:

过多过复杂的布尔表达式

解决方案:

取消for循环控制,直接针对各个楼层判断电梯up or down,放置于run,降低判断语句使用频率。

open和in 、out分离,checkIn checkOut只针对是否出入,开关门新设方法控制。

每层设置一个方法判断电梯极限楼层。

布尔表达式化简。

修改调度方案,无需过于依赖mainRequest,让调度接近scan算法,不仅能有效减少mainRequest带来的比较多的判断,简化布尔表达式,还能提升性能。

 

三、SS电梯设计

真*重构huozangchang的实例

这次使用了Worker Thread设计模式

 

这次的设计并不好,所以主要做一个总结和反思

技术图片

技术图片

经过思考,本次作业我没有继续沿用之前的楼层请求思路(其实是可以的),由于存在拆请求调度等问题,这个思路操作起来相对困难,因此我使用了一种相对比较好实现的思路。

实现思路

(1)把请求分成两类,可乘坐电梯直达的和需要一次换乘的。设置request类,用于装入请求,并在装入时判断该请求是否可直达,如果可直达,必须乘坐直达电梯,如果不可直达,在请求被电梯读取时,选取该电梯能到的最近的换乘点。所有的请求的读取由多线程电梯自己竞争。

(2)设置调度器分派请求,其中有两个队列inputQueue和requestQueue,其中inputQueeu用于与inputHandle交互,requestQueue与电梯交互,由于电梯运行过程会操作并锁住请求队列,为了防止此时读入被拒绝,因此再设置一个队列。只需在调度器中更新requestQueue即可。

(3)电梯类只需实现Als电梯的基本功能,再重写floorAdd()保证不该停的楼层不停,以及重写checkOut()针对换乘请求放置新的请求给requestQueue即可。实现较为简单。

线程安全

线程安全的保证可能是本次设计最大的难点。

线程不安全主要出现在对inputQueue和requestQueue两个队列的操作处。

Scheduler中:

(1)put()方法:将请求放置于inputQueue,锁住inputQueue,保证数据同步。

(2)upgrading()方法:锁住inputQueue和requestQueue,requestQueue新增请求,inputQueue删除请求。

(3)get()方法:用于三部电梯得到请求,锁住requestQueue,需要删除requestQueue中的请求。

MoreElevator中:

(1)由于Als捎带规则,每次checkIn()如果有人进入需要访问requestQueue并删除请求,锁住。

(2)由于换乘规则,每次checkOut()如果某请求到达换乘点需要新增requestQueue中的请求,并删除原请求,需要锁住requestQueue。

至此,所有线程不安全的问题已经通过synchronized解决了。

复杂度分析

技术图片

技术图片

 技术图片

技术图片

从上述数据可以发现,

MoreElevator和Request两个类具有比较高的复杂度。查看相应的方法可以看出

问题在于:

MoreElevator中:

getIn(),ifTake(),goTo()存在几乎与第二次作业相同的问题(因为实现几乎是沿用的)

Request中:

adjustDirection()用于判断该电梯中的最近换乘点,由于对A,B,C三种电梯集中判断,过多的if语句和过复杂的布尔表达式使这个方法复杂度爆掉。

getStop()用于服务adjustDirection(),寻找该请求所有换乘站。也对A,B,C三种电梯集中判断,过于复杂。

解决方法:

换乘点的判断和最近换乘点的寻找在装入相应的电梯时再判断,避免冗余。

 

四、bug分析

三次作业只有第三次作业在公测中被发现bug,互测中第三次作业被hack20次(太致命了)

这些bug都有一个相同的报错RUNTIME ERROR

最后我发现所有测评的cpu时间过长,自己反复分析无果之后,求助插件,并探索了一套方法。

希望和大家分享、交流,给广大cpu时间问题的朋友们一个帮助。

 

CPU time分析与解决

以第三次作业为例

本次出现了较多的cpu时间超时的问题,大量的问题也让我自己摸索了一条定位,分析和解决cpu time问题的方法。我主要使用VisualVM插件进行分析。

1、运行VisualVM,使用抽样器进行CPU抽样
2、在不输入任何请求的情况下观察线程cpu时间,发现main线程在无请求过程中占用大量的cpu

技术图片

技术图片

因此决定解决main的cpu时间问题。

切换到查看方法的cpu,发现upgrading方法占用大量cpu

技术图片

技术图片

于是发现upgrading方法在inputQueue为空时持续工作,存在暴力轮询,考虑使用wait/notify进行修改

public void upgrading() {
   synchronized (inputQueue) {
       while(inputQueue.size() == 0) {
           inputQueue.wait();
      }
        synchronized (requestQueue) {
            ...
      }
       inputQueue.notifyAll();
      ...
  }

再做一次抽样,发现

技术图片

技术图片

main线程cpu时间显著下降。upgrading方法几乎无cpu时间。

因此,无输入等待过程cpu时间问题基本解决。

3、输入足够多的请求,保证足够长的观察时间

技术图片

技术图片

运行过程电梯线程是占用cpu时间的主力,并且,相比无输入过程总cpu时间显著上升,速度较快,也就是说,一旦输入请求过多,电梯线程对cpu时间的占用必然导致最后超时,因此定位到了电梯线程的问题。

接下来,查看各方法:

技术图片

技术图片

检查自己的设计发现get方法已经是由wait/notify,goTo,floorAdd是普通的循环逻辑不存在暴力轮询。

接下来,重点就是getIn,checkIn了,观察数遍后发现,笔者上一次作业遗留下来一个大坑:

private int checkIn (...) {
      ...
       long lastDate = new Date().getTime();
       while (abs(new Date().getTime() - lastDate) < 400) {
           if ((temp = getIn(hasOpen, from, maxFloor)) != 0) {
              ....
          }
      ...

暴力轮询显而易见。

因此,做了修改

private int checkIn (...) {
      ...
       long lastDate = new Date().getTime();
       while (abs(new Date().getTime() - lastDate) < 400) {
           if ((temp = getIn(hasOpen, from, maxFloor)) != 0) {
              ....
          }
           sleep(long(399.9))     //防止暴力轮询,先睡一会再起来检查吧
      ...

重新CPU抽样

技术图片

技术图片

总CPU时间增加缓慢,电梯线程cpu时间下降,OK了!!!!

到这里,笔者的CPU time基本控制在一个慢增长,并且稳定在10s以内,问题已经解决。

不得不说,VisualVM用于CPU时间分析是大有益处的,可以帮你精准定位到相应的线程,方法,只需自己少量分析观察,即可解决。

通过这次深刻的被hack经历和公测惨状,让我意识到了一定不要暴力轮询一定不要暴力轮询一定不要暴力轮询。多线程设计一定要善用wait/notify,一定要在线程安全的基础上,关注线程竞争和性能分析,多静下心来,认真分析cpu时间,分析运行状况。

 

五、发现别人bug策略

 

主要采用了python写的对拍器和Python写的随机样例生成以及python书写的简单输出结果检查进行测试。

 

只在第三次作业发现别人的bug,主要集中在:

 

1、程序退出错误,提前退出问题,需要大量对拍

 

2、程序退出错误,程序无法退出

 

3、输出逻辑问题,某些楼层未关门

 

4、换乘问题,未成功换乘就停止了

 

5、cpu_time_error,部分程序存在较为严重的暴力轮询问题。

 

1,2,3,4主要通过对拍器发现。

 

5 主要通过VisualVM运行对应的程序发现。

 

本次测试与之前最大的差异在于需要定点投放,我自己写的python程序实现不了,后来通过讨论区大佬的方法实现了定点投放。

 

同时本次增加了cpu_time问题的测试,我借助了手动运行VisualVM的方法。

 

六、总结

线程安全的分析主要关注所有对共享资源的访问上,在操作共享资源的方法,代码块,要考虑是否加锁的问题。一定要对所有共享资源的访问做覆盖性的分析。

多线程设计要关注暴力轮询的问题,多用wait/notify,尽量减小CPU时间。

擅用插件分析cpu、内存等情况。

多线程电梯设计进行策略机制分离,保证线程之间的低耦合。

七、心得体会

整体架构,分块充分测试,精心调试,多考虑后续可增加可复用。

多打代码,多线程多用插件,多学习经典模型。

 

 

以上是关于多线程电梯设计的总结与反思的主要内容,如果未能解决你的问题,请参考以下文章

第5-7次OO作业总结分析

OO面向对象多线程编程作业总结

OO博客作业2:第5-7周作业总结

面向对象第二单元训练总结

第五到七次作业总结

OO作业总结第二弹