概述:
面向对象的第二单元是多线程电梯。第一次实现一部傻瓜电梯,每次只送一个人;第二次实现一部可稍带电梯;第三次实现三部可稍带电梯。
一、设计策略
1、第5、6次作业设计思路
第5、6次作业的架构相似,由一个电梯线程,一个读请求线程,一个调度器组成;电梯和读请求进程通过调度器交互。
读请求线程不断的从标准输入读入请求,将请求存放在调度器的队列中;
电梯线程从调度器中获取请求并执行。
调度器维护一个共享队列,用来存放请求。
读请求线程和电梯线程并发执行,二者对调度器中队列的操作保证互斥,同一时刻只有一个线程能够操作队列。
2、第7次作业设计思路
三部电梯调度中,三个电梯线程和主线程同步执行,四者对于主调度器的写操作是互斥的,主调度器对副调度器的写和相应电梯线程对副调度器的读操作是互斥的。
二、程序结构分析
1、通过Designjava分析三次作业的类和类中的方法
图 1 第5次作业类和类中方法度量结果
图 2 第6次作业类和类中方法度量结果
图 3 第7次作业类和类中方法度量结果
可以看到第5次作业RequestQueue类的LCOM值为正(其余都为0或负数),代表其内聚缺乏度高,该类中的函数逻辑相关性差,容易导致bug。
分析发现第5和第6次的作业FANIN和FANOUT都为0,耦合度较差,第7次作业的FANIN和FANOUT值比较合理,可能原因是第3次作业将功能进行分类,每个功能都封装在一个包里,而前两次作业直接将class放在default package中。
通过分析LOC方法的行数发现,除了几个逻辑复杂的方法之外,别的方法基本控制在30行以内,效果比较好。
2、三次作业的类图
图 4 第5次作业类图
图 5 第6次作业的类图
图 6 第7次作业类图
第5次和第6次作业的逻辑差不多,只不过第5次作业单独一个线程来读取请求,第6次作业是在Main中读取请求,两次作业都是读请求线程和电梯线程通过一个调度器进行交互。第7次作业两层调度器,每个电梯绑定一个调度器,三部电梯统一建模,三个副调度器统一建模,逻辑清晰,不容易出错,但是基本没有考虑优化。
3、第7次作业协作图
图 7 第7次作业协作图
4、从SOLID五原则角度分析第7次作业的问题
4.1 SRP(每个类或方法都只有一个明确的职责)
Elavator.java中的run函数代码长度将近60行,其中实现了太多的功能,容易出错,应该将其分解成几个方法,每个方法实现一项职责。
4.2 OCP(无需修改已有实现,而是通过扩展来增加新功能)
第7次作业的电梯和副调度器跟第6次作业的很像,应该通过扩展来完成,而不是像我一样重构。
4.3 LSP(任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误。)
ISP(当一个类实现一个接口类的时候,必须要实现接口类中的所有接口,否则就是抽象类,不能实例化出对象)
DIP(高级模块不应依赖于低级模块。两者都应该依赖于抽象)
本次作业没有涉及这三个点
三、分析自己程序的bug
第5次作业中由于共享变量endFlag没有加锁,导致程序出错。这给我的经验教训是,多线程中一定要找出所有的共享变量,考虑每个共享变量的安全问题,多次反复执行复现bug。写的代码一定要逻辑清晰,否则绕来绕去的很容易出错。
四、心得体会
通过第二单元的三次电梯作业,我了解了java的多线程实现方法和原理。总的来说三次作业的步骤大致如下:
1、首先要确定好哪些是线程,哪些不是线程,例如作业中的调度器是否将他设计为线程;
2、然后设计多线程的交互,通过什么方式,谁调用谁的方法;
3、要找到所有的共享变量,对存在安全隐患的共享变量加锁;
4、对于同一组测试数据要多次运行,确保无隐藏的bug。
多线程利用好了可以提高资源利用率,而利用不好会导致死锁、结果出错等各种各样的bug。多线程很难通过构造全的测试数据来debug,只能通过确保架构无误,逻辑无误来实现正确性,这就要求我们在设计阶段多花时间,写出一个逻辑清晰,高内聚低耦合的程序。