这个单元中,我们主要通过电梯调度相关的程序学习了多线程以及一些常见的设计模式。
本次作业,需要完成的任务为单部多线程可捎带电梯的模拟。 ——第一次作业指导书
第一次作业的难度并不大,主要是要初步掌握 Java 多线程程序的编写方法,尤其是保证多线程协作时的线程安全。为了达到这个目标,我们需要对相关方法或者成员变量进行加锁,我在这次作业中选择的是 synchronized 将 PassengerQueue 类中的 addPassenger 和 subPassenger 方法进行加锁。从而不管是输入线程向缓冲队列添加乘客请求还是电梯线程从缓冲队列获取请求,都不会产生数据冲突,尽可能保证线程安全。
我在这次作业中的设计模式,主要是参考生产者/消费者模式。采用了输入线程和电梯线程双线程的方法,输入线程主动添加乘客请求,电梯线程当电梯内为空即没有主请求的时候主动向缓冲队列获取请求。而缓冲队列相对而言只是承担了维护缓冲队列,也就是正在电梯外等待的乘客请求的容器。
在这次作业的过程中,相较之下,主要的困难在于初步掌握 Java 多线程的编写方法以及调试方法。
本次作业,需要完成的任务为多部多线程可捎带调度电梯的模拟。 ——第二次作业指导书
第二次作业相较于第一次作业增加了在程序开始时输入电梯数量的要求,同时对电梯中乘客数量也做了限制。随着电梯数量的增加,多部电梯协同调度算法也开始重要起来。
我在第二次作业的主要架构上,跟第一次作业没有大的变动,主要是在生产者/消费者模型的基础上,参考 Worker Thread 模式在细节上进行了一些修改。在第一次作业中,我的电梯进程是由 MainClass 直接产生并启动的,在这次作业中,由于电梯初始数量不确定,将这些任务都交给 MainClass 代码就不够有层次化了,也不符合主类尽可能简洁的要求。所以,我把这一个任务交给了 Channel,在 Channel 构建方法中根据电梯线程数量进行初始化,建立了 elevatorPool 对电梯进程进行统一管理,然后新建一个独立的方法来启动电梯。这样做,在一定程度上降低了类之间耦合性。
本次作业,需要完成的任务为多部多线程可捎带调度电梯的模拟。 ——第三次作业指导书
虽然在指导书的概述中与第二次作业没有任何差别,但是实际上相对于第二次作业还是有很大的不同的。主要是在前两次作业的基础上,增加了电梯停靠楼层以及运行过程中添加电梯的要求。后一个条件其实并没有带来很大的变化,主要是电梯停靠楼层的加入进而产生了一些换乘的需求。这些换乘需要进行一些宏观调度,不能单纯的将一个乘客请求调度至某个电梯,因为其有可能还要再次搭乘电梯。
第三次作业我用了两种架构来完成。
第一种架构是在我前两次作业的基础上迭代而来。主要是将 PersonRequest 包装成了 LiftRequest 类,在这个类中增加了对于是否需要换乘的判断,以及换乘的楼层、电梯等相关信息。将原来缓冲队列中的 PersonRequest 元素全部换成了 LiftRequest 元素,有助于换乘时进行判断。除此之外,大体上还是继承了之前的思路。
第二种架构是因为感觉原来的架构中的调度器担负的任务过多,不太符合面向对象的相关需求,对其进行拆分。
我建立了两层的 Scheduler 类,分为宏观调度和微观调度,分别对应 MasterScheduler 和 SlaveScheduler,均继承自 AbstractScheduler 接口。前者从 InputHandler 中获取乘客请求和电梯请求,然后根据不同的电梯分别加入不同种类电梯的专属调度器。后者在相同种类的电梯进程间进行调度。
简单来讲,主调度器判断需要搭乘的电梯类型,从调度器选择在该电梯类型中选择具体的电梯。
不同于前两次作业,除了输入进程结束以及缓冲队列为空,调度器的结束还要加入换乘乘客全部乘上第二部电梯的要求。考虑以下情况:
最后一名旅客需要换乘,在搭乘第一部电梯时,MasterScheduler 中的缓冲队列为空同时输入进程也已经关闭,但是之后还有这名乘客还需要再次加入主调度器判断需要搭乘的下一部电梯类型。
所以,为了保证需要换乘的乘客请求都能圆满地到达目的地。我在主调度器中保存了需要换乘的乘客名单,将进程结束的判断条件在输入进程结束、缓冲队列对空的基础上加入了需要换成的乘客为空的条件。以此来避免电梯进程提前结束的情况。
在第二种架构中,我还使用了 CopyOnWriteArrayList 来代替 ArrayList 以提高程序的线程安全性。
由于前两次作业的调度并不是很复杂,所以我主要分为三个部分:输入线程读取请求、调度器保存缓冲队列并响应电梯进程的获取乘客的请求、电梯进程运送乘客当电梯为空时从调度器获取乘客请求。
在第三次作业中由于调度器的任务越来越繁重,我在按照原来的架构实现相关需求之后,对调度器进行了拆分,分为了主调度器和从调度器。前者主要负责从输入进程读取乘客请求并判断乘客需要的电梯类型并传送至专属的从调度器;后者则是在电梯类型的基础上选择具体的电梯来运送乘客。这样做,较好地满足了 SRP 的原则。
这次的作业中我的电梯进程除了在后两次作业中根据电梯类型添加了运行速度、载客量等变量之外,主体的函数并没有产生较大的变化,较为符合 OCP 原则。我的调度器则是进行了多次重构,在第三次作业中由于任务过于繁重进行了拆分。由于在第一次作业中对之后可能涉及的要求考虑不是很全面,导致了一定的重构,这点还是需要进一步改善。
第三次作业的第二种架构中,我的主调度器和从调度器均继承自 AbstractScheduler 抽象类,也都实现了该类中的抽象方法,可以做到子类对父类的替换。
本次作业中未涉及接口。
依赖倒置原则要求程序尽量依赖于抽象接口,不依赖于具体实现。简单来说,就是要程序尽可能对抽象进行编程。在这一点上,我觉得我还有一定差距。由于这次程序并没有太多的继承层次,所以我的输入进程以及电梯都没有继承自抽象接口,如果后续不同种类的电梯内部的实现差异更大的话,可以新建一个电梯的抽象类。
以下是第一次作业的 UML 图。
以下是第一次作业的代码数量。
以下是第一次作业的复杂度分析。(由于插件使用异常,等可以正常使用后再补充)
以下是第二次作业的 UML 图。
以下是第二次作业的代码数量。
以下是第三次作业第一种架构的 UML 图和代码数量。
以下是第三次作业第一种架构的代码数量。
以下是第三次作业第二种架构的 UML 图和代码数量。
以下是第三次作业第二种架构代码数量。
第一次作业时出现了一个严重的错误,就是我对 ALS 调度算法的理解出现了一定的偏差,同时由于中测中并没有对运行时间进行一定的要求,导致我没能发现我的理解错误,进而导致我的程序在强测中全部出现了超时的情况,最终没能进入互测。
在第二次作业中,我的程序主要出现了部分时候第一个乘客请求会被忽略的问题。在 debug 的时候发现,这是有同学在评论区已经分享过的错误,主要是由两个 Scanner 造成的。这也提醒了我平时要多关注评论区的相关分享。
在第三次作业中,我的主要问题是部分时候在程序运行结束后有电梯进程没有结束的问题。
本单元的测试我还是采用手动结合自动化测试的方法。因为本单元的作业不像上一个单元有很多的特殊情况,而更加注重程序的性能。而在正确性方面,只要能解决好线程安全的问题,经过一定的正确性测试,就不会有太大的问题。
在本单元作业中,我会先测试上一次作业的强测数据以及自己手动的测试数据进行一定的正确性测试,然后结合结束输入的时间来进行一定的线程安全的测试,主要是看输入结束后程序能否在正确地运送完所有乘客后关闭电梯进程。本单元的测试相对来说主要是线程安全的问题,我认为只要程序的线程调度较为科学,应该可以避免大部分的线程安全问题。
在这一单元的学习中,我接触学习了很多关于多线程的设计模式。
这两个是我们在理论课和实验课上主要学习的两种常见的设计模式。
在课外,我还阅读了《图解 Java 多线程设计模式》一书,并通过网络上的相关资料,了解了更多的多线程设计模式。
比如在生成输入进程的时候,我尝试使用了单例模式的双检锁校验,能在安全的前提下保持较高的性能。
public static InputHandler getInputHandler(Scheduler scheduler) {
if (inputThread == null) {
synchronized (InputHandler.class) {
if (inputThread == null) {
inputThread = new InputHandler(scheduler);
}
}
}
return inputThread;
}
这个单元的学习,是我第一次接触多线程编程,在调试多线程时难以复现的 bug 以及莫名其妙的死锁都令人印象深刻。我希望我能在课下学习更多的多线程编程的设计模式,在以后的学习生活中,更加科学准确且高性能地应用多线程的相关知识。
ew InputHandler(scheduler);
}
}
}
return inputThread;
}
这个单元的学习,是我第一次接触多线程编程,在调试多线程时难以复现的 bug 以及莫名其妙的死锁都令人印象深刻。我希望我能在课下学习更多的多线程编程的设计模式,在以后的学习生活中,更加科学准确且高性能地应用多线程的相关知识。