综述

主要任务就是写一个电梯模拟器,读入每一个人的请求然后让电梯把他们送到想去的地方。

从第一次到第三次作业,三次的主要任务都是相同的,但是每次都增加了很多的细节,每次的难度都逐步增长,电梯复杂度和瞎跑度都大大提高;同时我们也对于多线程的设计、写法、调试也逐渐有了心得和经验。

以综合性最强的第三次作业为例。它要求的电梯和一般的普通的常见的电梯,又有很多不同:首先这玩意是目标选层电梯,在进电梯之前就先选好楼层,然后坐等电梯送你去;然后这玩意开关门十分鬼畜,开门一瞬间(即使只有一条缝),我们的乘客都可以从这一条缝挤进电梯;再然后这个电梯好像是支持全自动的:假如A电梯能从1楼到3楼(但不到5楼),B电梯能从3楼到5楼(但不到1楼),这时候一个想从1楼道5楼的乘客,他只需要在1楼设置好楼层,然后进A电梯,随后我们的电梯系统能够自动完成换乘的事情,而根本不用乘客去考虑:我需要先到3楼然后摆摆腿出来换乘。这就好比你从沙河上了昌平线然后地铁上睡了一觉,原地醒来就到了知春路。

我们呢先不管这些逻辑上的不熟悉,来考虑一下电梯的设计。

需求

设计一个电梯系统,包含三个电梯,名为A、B、C,他们的可停靠楼层、最大载客量、上下层速度都不一样。可停靠楼层如下图所示:

要求对于所有不同时输入的请求,都有相应的调度,能够在最短时间内送人到达目的地。

设计

输入处理:利用官方给的阻塞式端口输入,然后判断这个请求能不能由一台电梯独立解决;不行则拆解成两个请求。比如从-3到3的请求,我就拆解成-3到1,再从1到3。然后将请求加入一个队列。

电梯调度:调度器为每个空闲的电梯分配一个合适的任务,让他去跑。那么什么叫“合适的任务”呢?就比如本题的C电梯,它的合适的请求就是出发楼层和到达楼层都在C电梯的可停靠楼层范围内的那些请求,比如1-3、5-15,但像2-4、6-14就不是了。当然对于一组有先后顺序的请求,比如一个原本为从-3到3的请求,被拆解成-3到1,再从1到3——在-3到3完成之前,1到3也不算一个“合适的”请求。

捎带和被捎带问题:设计一个楼层类Floor,用来记录在着一层楼等待的乘客。电梯每到一个可停靠楼层,我们就先把想要这一层楼离开电梯的兄弟给踢走,然后在着一层楼中先找到那些能捎带上的兄弟,把他们带上。如果更详细地再分析一下,这其实还包含着主请求的变更,比如你在1楼接了一个FROM-1-TO-3,然后在2楼顺了一个FROM-2-TO-5的兄弟;那么电梯到达了3楼之后,把第一个兄弟踢走之后需要把主请求修改为FROM-2-TO-5,好让电梯接着走。

输出:电梯每执行完一步操作就对应输出“arrived”,“open”,“close”等。

多线程的设计

设计五个线程:主线程(兼输入)、调度器线程、三个电梯线程。调度器与主线程之间通过线程安全的“队列”进行交互,调度器与电梯之间直接通过调用异步的方法进行通信。

历程

重构前:对于每个请求,我都随机派一个电梯去起始楼层 x 接人。而这分为两种情况:若该电梯能到达 x ,开门就完事了;要是到不了,再把这个“去楼层 x 接人”的请求放回队列。假如成功接到了人,电梯也还要动脑子:这个人想去3楼,我能不能到3楼呢?哎呀,好像不行。那,我又该在几楼把他放下来呢?我是不是该通知C电梯,让给他帮我一手,把这兄弟送到目的地。

重构后:拆解请求,让前文“想”的这个过程交给调度器,把任务分解、分配给傻瓜电梯,这样电梯就不用动脑子去想我该在哪里把这个人放下来,而是简单机械地执行调度器给的请求就好了(对电梯和程序员都友好)。

重构是件很纠结的事情:本来写了快一千行,但是好像又不满意,bug越写越多;重构吧,又怕遇到新的问题白白浪费时间。

结果

前两次作业没有问题,就是老老实实按照指导书算法写,正确性没有什么问题。

第三次作业嘛。重构一下,就出事了:因为最大楼层不知道为什么写成了19(显然20才对啊,真是蠢),然后导致第20层楼捎带的时候会爆炸,表现在“这个人莫名其妙地在19楼进了电梯”,所以就爆炸了。出现这个问题,确实应该怪自己,怪自己畏惧近千行代码没有敢去一行行看,去找逻辑错误;怪自己没有想到用随机生成数据去做覆盖性测试,不然这样的问题肯定能发现。

性能分析

类图:

方法复杂度:

(仅显示复杂度较高的方法)

明显可以看出disassemble方法(也就是那个拆解一个请求为两个所用的方法),由于包含了大量的if-else语句导致设计复杂度很高,但是本质复杂度很低。

另外两个复杂度大头相信大家也差不多:线程的run()方法,好像没什么方法来做有效的优化。

illegalFromFloor()方法中,采用了很多类似于

if(A == true) return true;

这样丑陋的代码,导致本质复杂度很高。

lift()方法由于包含了很多与捎带有关的和与电梯状态的修改,但是写法又是基于简单的for-if,所以本质复杂度不高但是设计复杂度比较高,所以这也是容易产生bug的地方,需要程序员仔细检查代码逻辑。

类复杂度:

改进和SOLID原则分析

  • 有些方法(参考性能分析-方法复杂度)的本质复杂度较高,体现的是程序员对语言的的不够熟练。
  • 有些特殊情况不能正常退出。
  • 电梯和调度器有些功能的分离还不是很彻底,因为有一些方法到底是属于电梯好还是属于调度器好,我还没有定论。有少许违背单一责任原则(SRP)原则。
  • 对于扩展不是很方便,虽然利用了工厂来建立新电梯,使得第四台电梯的加入十分方便,但是假如有其他“反人类”的需求比如乘客1不喜欢A电梯非要坐B电梯,那么对于既有的代码不能够保持原封不动。有违单一责任原则(OCP)原则。
  • 没有类的父子关系。不违背里氏替换原则(LSP)原则。
  • 高层模块“调度器”对于不同的底层模块“电梯”都能够同样地执行。符合依赖倒置原则(DIP)原则。
  • 仅使用了Runnable一个接口。不违背分离原则(ISP)原则。

心得体会

通过这三次作业,逐渐地了解了多线程设计的思想。从第一次作业的毫无头绪、仿照课件,到查阅锁的使用,再到研究设计模式和SOLID原则,自己的代shi码shan也逐渐地OO了起来。对我来说,几大难题分别是:多线程的同步控制和电梯逻辑的控制。对于多线程的控制是通过了大量的本地测试和根据例子进行仿照,后来才开始系统的了解锁的使用,确立哪些东西需要加锁哪些不需要,哪些需要保护哪些不需要;线程之间可以有哪几种通讯的方式。另外一个难点是多线程环境下对于每个线程的逻辑debug。我写的电梯逻辑一开始是充满了许多bug的,后来是通过”走一步print一步“的思想来肉眼跟踪找到bug的。业界有人认为:多线程是程序员在工作中遇到的最大的魔鬼。在这三次作业中,我也感受到了,因为多线程的bug隐藏的一般很深,有些bug产生的条件也比较苛刻,甚于充满偶然性。

OO随笔之纠结的第二单元——多线程电梯的更多相关文章

  1. OO第二单元多线程电梯总结

    OO第二单元多线程电梯总结 第一次作业 设计思路 Input为输入线程,负责不断读取请求并将读到的请求放入调度器中. Dispatcher为调度器,是Input线程和Elevator线程的共享对象,采 ...

  2. BUAAOO第二单元多线程电梯作业总结

    第二单元多线程作业需要保证线程安全

  3. OO第二单元多线程电梯总结分析

    一.概述 这一部分的作业考察的关注点与上一次的作业有所不同,上一次的考察重点主要集中在输入输出的判定以及多态的考察上面,而这一次是让我们进行多线程程序的调度与开发.这次开发过程中最大的感受就是自己之前 ...

  4. OO第二单元——多线程(电梯)

    OO第二单元--多线程(电梯) 综述 第二单元的三次联系作业都写电梯,要求逐步提高,对于多线程的掌握也进一步加深.本次作业全部都给出了输入输出文件,也就避免了正则表达式判断输入输出是否合法的问题. 第 ...

  5. 「BUAA OO Unit 2 HW8」第二单元总结

    「BUAA OO Unit 2 HW8」第二单元总结 目录 「BUAA OO Unit 2 HW8」第二单元总结 Part 0 前言 Part 1 第五次作业 1.1 作业要求 1.2 架构设计 1. ...

  6. oo第二单元——多线程魔鬼电梯

    在初步认识了面向对象思想后,立刻进入了多线程的学习,本单元的难点主要是锁的理解,需要保证线程安全的同时防止死锁的发生,也要尽可能缩小锁的范围,提高性能.这一单元以电梯为载体,让我们从生活出发,从电梯运 ...

  7. OO第二单元总结——电梯调度问题

    一.设计策略. 在三次作业中,多线程程序的实现分以下几个步骤: 1. 主线程Main类的创建多个线程. 2. 共享对象的synchronized锁保证线程之间的互斥访问. 3. 采用notifyAll ...

  8. OO第二单元总结——电梯

    在电梯系列的作业中,笔者的整体架构几乎没有发生改变.现介绍如下,对于一个电梯系统,主要的工作步骤就是获取乘客请求.分派请求.执行请求.针对这样的工作模式,笔者设计了Elevator.Uselist两个 ...

  9. OO随笔之魔鬼的第一单元——多项式求导

    OO是个借助Java交我们面向对象的课,可是萌新们总是喜欢带着面向过程的脑子去写求导,然后就是各种一面(main)到底.各种方法杂糅,然后就是被hack的很惨. 第一次作业:萌新入门面向对象 题目分析 ...

随机推荐

  1. python-3-3 字典

    一 元组(tuple) 1.元组也是一个list,他和list的区别是 元组里面的数据无法修改 元祖用()小括号表示,如果元祖里面只有一个元素的话,必须在这个元素的后面添加一个逗号,不然就不是元祖了 ...

  2. C语言中复杂声明的解读和简化

    code[class*="language-"], pre[class*="language-"] { color: rgba(51, 51, 51, 1); ...

  3. PAT (Advanced Level) Practice 1046 Shortest Distance (20 分) 凌宸1642

    PAT (Advanced Level) Practice 1046 Shortest Distance (20 分) 凌宸1642 题目描述: The task is really simple: ...

  4. Apache Hudi:CDC的黄金搭档

    1. 介绍 Apache Hudi是一个开源的数据湖框架,旨在简化增量数据处理和数据管道开发.借助Hudi可以在Amazon S3.Aliyun OSS数据湖中进行记录级别管理插入/更新/删除.AWS ...

  5. nsqlookupd:高性能消息中间件 NSQ 解析

    摘要:本篇将会结合源码介绍 nsqlookupd 的实现细节. 本篇将会结合源码介绍 nsqlookupd 的实现细节.nsqlookupd 主要流程与nsqd 执行逻辑相似,区别在于具体运行的任务不 ...

  6. 给我一个shell我能干翻你内网

    0x00 前言 在去年小菜鸡学了点内网知识就闲着没事跑点jboss的站看看,在经历过很多次内网横向失败之后终于算是人生圆满了一把,阿三的站一般进去之后很难横向,不知道是不是我太菜的原因,反正阿三的站能 ...

  7. NPM 与 NPX 区别

    NPM 和 NPX 区别 NPM Node Package Manager npm 是 Node.js 的软件包管理器,其目标是自动化的依赖性和软件包管理 NPX npx 是执行 Node 软件包的工 ...

  8. Java(265-278)【Map】

    1.Map集合概述 是一个接口 键是唯一的 java.util.Map<k,v>集合 Map集合的特点:      1.Map集合是一个双列集合,一个元素包含两个值(一个key,一个val ...

  9. Spring Boot 2.x 快速集成Kafka

    1 Kafka Kafka是一个开源分布式的流处理平台,一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据.Kafka由Scala和Java编写,2012年成为Apache ...

  10. matlab逻辑类型

    matlab逻辑类型 matlab逻辑表达式输出:1为真,0为假. matlab关系操作符: 关系操作符 说明 < 小于 <= 小于等于 > 大于 >= 大于等于 == 等于 ...