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

一、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、内存等情况。

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

七、心得体会

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

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

原文地址:https://www.cnblogs.com/thunderZH-buaa/p/10755458.html

时间: 2024-10-07 23:50:49

多线程电梯设计的总结与反思的相关文章

Elevate_601电梯设计软件

Elevate_601电梯设计软件Lift.Designer.v5.2.Premium.Suite 电梯系统设计 2020 Kitchen design 6.1(占美国97%以上市场的橱卫设计软件)Segmented.Project.Planner.v2.01.0126 车床木器部件加工辅助软件Sheet.Layout.v8.02.WinAll.Cracked 为木匠设计的软件 CABINET.VISION.SOLID.V4.0 厨房设计CABINET_VISION_SOLID_v4.0_TRA

Windows多线程多任务设计初步(转)

Windows多线程多任务设计初步 [前言:]当前流行的Windows操作系统,它能同时运行几个程序(独立运行的程序又称之为进程),对于同一个程序,它又可以分成若干个独立的执行流,我们称之为线程,线程提供了多任务处理的能力.用进程和线程的观点来研究软件是当今普遍采用的方法,进程和线程的概念的出现,对提高软件的并行性有着重要的意义.现在的应用软件无一不是多线程多任务处理,单线城的软件是不可想象的.因此掌握多线程多任务设计方法对每个程序员都是必需要掌握的.本文针对多线程技术在应用中经常遇到的问题,如

python多线程爬虫设计及实现示例

爬虫的基本步骤分为:获取,解析,存储.假设这里获取和存储为io密集型(访问网络和数据存储),解析为cpu密集型.那么在设计多线程爬虫时主要有两种方案:第一种方案是一个线程完成三个步骤,然后运行多个线程:第二种方案是每个步骤运行一个多线程,比如N个线程进行获取,1个线程进行解析(多个线程之间切换会降低效率),N个线程进行存储. 下面我们尝试抓取http://www.chembridge.com/ 库存药品信息. 首先确定url为http://www.chembridge.com/search/se

OO_2019_第二单元总结——多线程电梯

传说中的多线程(魔鬼)电梯完成啦! 一.程序设计分析与基于度量的程序结构分析 三次电梯都统一地采用了生产者-消费者模型,每次在前一次的基础上进行添加,没有大规模的重构,可以说设计含有一定的可拓展性. 第一次电梯(单部多线程傻瓜调度(FAFS)电梯) 这是第一次接触多线程,十分迷惑.起初实在不知道什么是多线程,甚至写了单线程版本.后来通过查资料和多次尝试,根据生产者-消费者模型探索出了主线程(RequestInput)负责管理输入请求并存入Queue,另一个线程(ElevatorRun)模拟电梯的

网络与多线程的设计例子

上一篇的<网络与多线程设计模式>讲的是一些设备上的纯理论的东西,本篇将介绍一个本人写的使用EPoll+TcpServer+多线程的开源库,已在https://bitbucket.org/johnson_he/epolltcpserver中供开源下载. 该库使用了Qt的工程方式进行封装(实在不想写configure),里面的log也使用了log4Qt的方式进行,如果不想使用Qtcreator的话,可以自行引用tcpserver文件夹里面的所有文件即可. 使用Qtcreator的好处是,里面的de

java多线程3.设计线程安全类

实例封闭:将数据封装在对象中,将数据的访问限制在对象的方法上,确保线程在访问数据时总能持有正确的锁 java平台的类库中有很多线程封闭的示例,其中一些类的唯一用途就是将非线程安全的类转为线程安全的类.一些基本的容器类并非线程安全,如ArrayList和HashMap,但类库提供了包装器工厂方法,如Collections.synchronizedList,使这些非线程安全的类可以在多线程环境中安全地使用. 这些工厂方法通过"装饰器"模式将容器类封装在一个同步的封装器对象中,包装器将接口中

多线程场景设计利器:分离方法的调用和执行——命令模式总结

前言 个人感觉,该模式主要还是在多线程程序的设计中比较常用,尤其是一些异步任务执行的过程.但是本文还是打算先在单线程程序里总结它的用法,至于多线程环境中命令模式的用法,还是想在多线程的设计模式里重点总结. 实现思路 其实思路很简单,就是把方法的请求调用和具体执行过程分开,让客户端不知道该请求是如何.何时执行的.那么如何分开呢? 其实没什么复杂的,就是使用 OO 思想,把对方法的请求封装为对象即可,然后在设计一个请求的接受者对象,当然还要有一个请求的发送者对象,请求本身也是一个对象.最后,请求要如

大作业:电梯设计的概要设计文档

一.系统硬件接口定义 1.电梯内外操作按钮与显示屏幕 电梯内部的操作界面包括楼层按钮.对应的指示灯.开关门按钮(紧急呼叫按钮)以及显示当前楼层数的屏幕.按钮有向指示灯和CPU传输的接口,屏幕有接受CPU传输的接口. 每个楼层的操作界面包括上行与下行按钮.对应指示灯以及显示两台电梯楼层数和运行状态的屏幕.每层楼的行程开关通过CPU将电梯运行信息传输给屏幕,按钮有向指示灯和CPU传输的接口. 2.电梯的动作 电梯动作包括电梯上下行与电梯门的开闭.电梯上下行由大电机带动,电梯门的开闭由独立小电机控制.

电梯设计大作业——概要设计文档

一.     系统硬件接口定义 电梯操作和显示面板 电梯内有显示面板,一台电梯一个,除了七个楼层的按钮,对应的指示灯,以及将传往控制芯片的接口以外,开门关门的按钮也要有指示灯和向CPU指令传输的接口.显示电梯当前所在楼层的屏幕也要留下接口. 电梯外是每个楼层都要有的上下楼按钮指示灯与控制接口,还有两个显示屏显示两台电梯所在位置和另外两个显示电梯下一步行进方向的屏幕也分别要留接口.每一楼都要留下行程开关. 2.电梯动力部分 电梯门自身打开关闭需要行程开关,一个小电机提供动力,电梯上下运动需要触顶触