1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > java面向对象电梯运行代码_面向对象编程总结——多线程电梯

java面向对象电梯运行代码_面向对象编程总结——多线程电梯

时间:2021-09-06 14:12:22

相关推荐

java面向对象电梯运行代码_面向对象编程总结——多线程电梯

最近的三周面向对象的作业是电梯问题。主要是对多线程编程的一种训练。主要还是对生产者——消费者模型的使用的理解。作业分成了三个阶段,第一个阶段是FIFO调度策略的电梯。第二个阶段是可捎带的电梯设计,第三个部分是多个停靠楼层不同运行速度不同的电梯的合作。

FIFO

因为第一次的作业没有性能上的要求只需要满足正确性,所以我设计了两个线程协同完成这个任务。是个标准的生产者消费者问题。输入线程向共用缓冲区中写入请求,电梯每次读出一个请求之后完成这个请求。期间不关注其他请求,电梯完成请求的动作是硬编码,是正确性的考虑。唯一的多线程问题是安全停止程序。我的策略是在读到请求结束的时候,向中间缓冲区写入end标志,电梯在buffer空的时候检查end标志来确定是否退出。

第一次的程序结构非常简单,GetInput和Buffer共享缓冲区,GetInput从输入中读取内容,写入buffer然后电梯从buffer中读取指令。是简单的单生产者-单消费者问题。程序结构简单,逻辑非常简化,不易出现问题,但是电梯执行的逻辑是硬编码,所以功能十分有限,这导致之后的作业需要重写电梯类。

程序复杂度分析

程序整体的复杂程度非常低,整体代码量也比较少:

代码的数量统计:

进程之间的协作逻辑

第一次作业的时序非常简单,输入线程得到输入,将输入写入缓冲区,电梯从缓冲区读指令,如果有指令则读取并且完成,在完成过程中不关心缓冲区中是否有其他的指令,没有指令就挂起电梯线程,在输入线程输入一个新的指令的时候被唤醒。

线程协作图

程序整体复杂度很低,因为功能非常单一。

SOLID原则分析

单一责任原则:基本满足,对电梯的每个行为都用了一个方法封装

开放封闭原则:因为是原始代码不存在拓展问题,省略

里氏替换原则:功能相对单一,不存在集成和接口问题

接口分离原则:做的不够好,对于buffer的读和写是同一个方法,并没有划分成两个接口函数

依赖倒置原则:程序结构简单,没有太多的复杂设计,原则无法体现

可捎带单电梯

因为要求可捎带,所以我采用了一种新的调度策略。电梯每到达一个新的楼层就进行一次判断,通过当前的电梯和缓冲队列中的状态来确定电梯的下一步运行方向

主要判断逻辑为:

1.判断该层是否有人要进、出,处理完成进出(只有和电梯当前运行方向想同的需求会上电梯,这和现实中的电梯乘客的运行逻辑是相同的)

2.判断电梯中是否有乘客,

2.1.如果有乘客就调用nextFloor(修改楼层值,休眠),进行下一次循环

2.2如果没有乘客就判断下一次的运行方向

2.2.1如果同层有同方向的请求则进行下一次循环(再进行一次判断是因为有可能在1步骤之后插入了输入的进程将新的请求恰好放在了这一层)

2.2.2如果同方向上有请求就调用nextFloor,进行下一次循环

2.2.3如果同层有不同方向的请求就修改运行方向

2.2.4如果不同方向上有请求则修改运行方向,调用nextFloor

2.2.5如果没有请求,电梯线程挂起等待输入唤醒

程序类图

同样是一个生产者消费者的问题。但是这个架构有几个地方有点问题,最大的问题就是buffer的功能函数的设计不够完善。buffer采用了一个二维的Vector,在写完才发现完全没有必要采用这样的设计。导致在很多功能函数的实现中带来了很大的问题。

同时程序出现了一个问题就是在[0.0]时刻上输入的请求不会被第一时间被电梯得到,主要原因是在2.2.1的判断上出现了问题导致性能出现了很严重的影响。

程序复杂度分析

程序复杂度高的主要原因是Elevator对于功能的划分不够彻底,Elevator.run()里面分担了很多应该有子函数完成的内容。导致run过于臃肿,内部需要处理的分支情况比较多。Buffer.nearestRequest(int,boolean)是向电梯给出电梯中的请求分布情况,返回flag来帮助电梯线程来确定下一个运行的方向。这里逻辑比较复杂是因为buffer采用了一个二维Vector来保存请求,这给这个函数的实现导致了很多问题。可以对这个函数的功能做进一步的拆分。

代码数量分析:

电梯的整体协作逻辑和第一次作业基本相同,只是电梯在运行过程中会对缓冲区的状态进行检查,但是线程之间的协作关系没有本质上的改变,具体如下:

线程协作图

SOLID原则分析

单一责任原则:没有满足,在elevator类中的私有方法的相互依赖很严重,主要是在最开始设计电梯的运行逻辑的时候做的不好

开放封闭原则:第一次作业的代码的Elevator,Buffer进行了重构,没有很好的做到开放封闭原则的要求

里氏替换原则:功能相对单一,不存在集成和接口问题

接口分离原则:对第一次的buffer 进行了优化,更加符合接口分离原则

依赖倒置原则:程序设计的时候是通过自顶向下的方法设计的对外部接口的输入输出结果在设计之前就进行了确定,符合依赖倒置原则。

多电梯协同问题

现在有几个电梯同时完成运送的工作电梯的运行图如下,每个电梯停靠的楼层不同,电梯的运行速度不相同。

为了实现上面的功能,我采用了如下的程序结构:

画成时序图如下(主要描述了一条指令分成两个电梯执行,同时在电梯运行过程中遇到null输入的过程)

线程的协作设计

输入线程从标准输入中得到指令请求缓存在buffer中,controller从buffer得到指令信息,将指令分成有不同的电梯完成。电梯完成之后返回指令结束的信息到controller,controller再对这个指令的后续接着进行处理。因为总共有5个线程停止的时候会存在很多同步性的问题,最后为了避免这个同步性的问题所以我才用了一种整体的分析方法来结束整个程序。即在读到输入结束之前记录总共来了多少条指令,存在counter中,在每条指令完成的时候将counter减一,当counter减为零的时候说明没有其他的指令正在运行说明可以正常的退出整个程序。

类图

程序主要采用了两个阶段的生产者--消费者模式,第一个在输入线程--控制线程之间,通过InputBuffer相连,第二个是在控制线程--电梯之间,每个电梯采用独立的buffer 来保存指令,指令的分配由控制线程来确定,每次电梯完成一条指令就将完成的指令返回控制线程。

程序的复杂度分析

Elevator类需要处理的情况相对比较多,导致程序的逻辑比较复杂,从而具有比较高的设计复杂度。有个地方设计的不好就是run方法中承担了很多运行的功能,应该将功能从run中拆出来。因为程序中肯定有一个部分处理电梯的运行逻辑和拆分逻辑,这部分的代码很难再进行分解否则可能导致方法之间的耦合程度比较高。有个地方写的有点不太好就是内部的Request类应该去继承PersonRequest类,这样可以简化很多地方的逻辑,但是却会增加Request类本身的编写难度,这里应该再优化一些。

代码数量和分支数量

SOLID原则分析

单一责任原则:相比第二次作业优化了Elevator的内部结构,使得方法之间的耦合降低了

开放封闭原则:再一次进行了重构,没有达到开放封闭原则的要求

里氏替换原则:基本实现里氏替换原则,ElevatorBuffer 是继承自 InputBuffer,只是对InputElevator 的方法进行了拓展,所以ElevatorBuffer可以替代InputBuffer。

接口分离原则:细化了buffer的功能,接口更加细化

依赖倒置原则:程序设计的时候是通过自顶向下的方法设计的对外部接口的输入输出结果在设计之前就进行了确定,符合依赖倒置原则。

三次作业的bug分析

因为大三高工没有互测的阶段,而且在公测中没有发现问题,但是在自己做本地测试的时候发现线程之间的协作的时候出现了几个问题。最主要的就是先wait后notify的无限等待,还有线程安全退出。我的这个程序结构如果只通过电梯的状态来判断是否可以结束需要处理的公共数据很多会遇到很多的同步性问题,所以采用了指令计数的方式来安全退出。

这个单元就我的代码结构而言只要能完成比较简单的指令组合就基本可以确定主逻辑是没有问题,唯一可能由问题的地方就是多线程的协作问题 分析bug的主要策略就是在特殊节点位置来输入请求来检查多线程协作逻辑的正确性1.通过0时刻的特殊指令输入来检查换乘逻辑会不会造成死锁,即有两个乘客同时需要在同层换乘的情况2.通过不同时间输入终止指令来确定线程是否可以安全退出。

三次作业的设计思想总结

这次电梯的作业的多线程设计主要运用的模型就是生产者--消费者模型,主要的想法就是采用中间的缓冲区来作为两个线程的通信渠道,这样的方式可以通过对中间缓冲区的同步保护来确保进程之间通信的稳定性。第三次作业则采用了课上介绍的master--worker模型,我认为这种模型本质上是生产者消费者模型的一种拓展,只是通信的内容更加复杂。

心得体会:

从线程安全的角度来分析:

这是我第一次真正意义上接触多线程编程,虽然曾在很多门课上了解了多线程编程的各种模型和死锁的原理,但是在这次动手实践的过程中还是遇到了一些阻力。以下是我认为在多线程编程的时候需要注意的问题。

关于多线程编程的无限等待问题:

1.先notify后wait的问题:如果有一个buffer,线程A先对buffer做读,发现buffer是空的,然后线程发生切换,线程B向buffer写入了东西(此时notify),然后线程切换到了A,A执行了wait,这样会存在当B不在写入的时候A无限等待下去,同时buffer中有一个命令没有完成处理。这时候必须使得check-wait的过程是一个原子过程,才能保证wait之后一定可以被唤醒。

2.注意notify,wait的写法采用Object.notify()的写法可以更好的更灵活的控制线程同步,相比对函数加锁的方式要好很多

死锁问题:

最主要的死锁问题是当两个不同的进程在申请锁的时候顺序不同导致的死锁,但是死锁的出现可能性没有无限等待出现的次数多。主要的处理方式就是对申请锁做一个排序,使所有线程申请锁的顺序都是相同的。但是在设计的时候应该尽量避免这种二重锁的存在,程序比较复杂的时候将很难去分析锁的方式。

从设计思想的角度:

1.应该注意自顶向下的设计思想,将功能分类,规定类的功能函数的接口再去思考函数的具体写法。

2.在多线程编程的时候尽量不要使线程之间的交互过于复杂,应该将线程的公共区域部分单独形成对象,统一管理多线程问题,在线程主题如果能做到线程透明是最好的。

3.对象的加锁范围要尽可能小,减少使用二重的锁,可能会导致死锁问题。

4.对于系统状态的判断(比如程序何时结束)依赖的线程间公共信息越少越好。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。