OO_summary_2.Elevator_dispatch

OO_summary_2.Elevator_dispatch

Charles Lv7

OO_summary_2.Elevator_dispatch

前言

OO第二单元的主题是 “多线程”,主要是让我们初步学习多线程的编程思想,理解和解决线程交互和线程安全问题,进一步深化对 “面向对象” 的理解。

本单元的三次作业都是和 “电梯调度问题” 有关,需要我们根据不同要求对电梯调度系统进行模拟。这三次作业不仅要求我们关注 “如何正确的加锁来解决线程安全问题”,还要求我们考虑到调度策略的整体性能

由于以前没有接触过多线程编程,因此刚开始做作业时稍微有些吃力。但是我随着对 “多线程”“线程同步” 理解的加深,在做后面的作业时越来越得心应手。同时,我也学会在代码中使用一些常用的设计模式,例如 “单例模式”、“工厂模式”、“流水线模式” 等等,进一步降低代码的耦合度,让代码层次更加清晰。

知识点总结

以下部分是对线程知识点的简单总结

进程知识的通俗介绍

什么是进程呢?在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。

所以我们可以知道,进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。

操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。因为同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,可以选择多进程模式(每个进程只有一个线程),多线程模式(一个进程有多个线程)和多进程+多线程模式(复杂度最高)。

而Java语言内置了多线程支持。当Java程序启动的时候,实际上是启动了一个JVM进程,然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。所以我们实验中所写的代码实际采用了多线程模式。

同步块与锁

​ 多线程编程目的是为了加快程序运行速度,线程之间会共享资源,由此自然而然会产生类似于计组C流水线PU中读写不一致的问题,故必须要正确地加锁。

​ 通过阅读训练教程和相关资料,了解到了原子操作的概念以及各种加锁的方法。虽然有更高级的条件锁以及读写锁等等,但它们对于电梯作业性能的提升并无裨益而且更容易出错,故我最终采用了最简单的synchronized锁。

​ 接下来就是同步块的设计。大体上会有两个地方涉及同步块:

​ 其一是共享对象本身。因为每次引用都有可能修改内部变量,故除了构造器外内部每个方法都需要加锁,锁住该对象本身(JAVA的特性决定了对象基本都是以类似于C语言中“指针”的形式出现,故只需要传参并加锁就好)

​ 其二是线程内部对共享对象进行操作时。虽然共享对象内部方法已经加锁了,但其线程内部会对共享对象进行读写操作,其中写操作显然要加锁,而读操作若不加锁可能其他线程会向其中写入改变状态,故也要加锁。此处就涉及到了加锁粒度的问题,由于一个方法中不是所有语句都涉及读写操作,故我仅在涉及读写之处加锁。这样使得加锁更加灵活,同步块不会过于臃肿,在有较多线程之时也可以提升性能。

代码实现的一些注意点

在简单介绍了一下进程和线程的相关知识后,我来进一步介绍一下我们在编写电梯时的一些有关多线程的知识。

start()和run()的关系

基础款

要创建一个新线程非常容易,我们需要实例化一个Thread实例,然后调用它的start()方法,start()方法会在内部自动调用实例的run()方法,从而运行一个新的线程,就像下面几行那样。

1
2
3
4
5
6
7
8
9
10
11
12
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
}
}
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("Thread start");
}
}
拓展点

这些相信大家在看了OO课程和电梯的历练后都是会的,但其实这两个方法还有一些注意点。就比如我在写电梯时,由于老是遇到电梯wait后无法notify的情况,于是就直接在调度器加入新的乘客后进行了相应电梯线程的run方法,这样在逻辑上虽然非常合理,但在JVM中的行为却是大相径庭的。

我们要知道,run()其实也就是个重写的普通方法,并不属于某个线程,换言之,直接调用run()方法,相当于调用了一个普通的Java方法,当前线程并没有任何改变,也不会启动新线程。上述代码如果将t.start()改成t.run()的话,实际上是在main()方法内部又调用了run()方法,打印hello语句是在main线程中执行的,没有任何新线程被创建。这一点即使在线程已经start()后也是如此,而这显然不是我们刻意调用了run()方法的本意。

总而言之,必须调用Thread实例的start()方法才能启动新线程,如果我们查看Thread类的源代码,会看到start()方法内部调用了一个private native void start0()方法,native修饰符表示这个方法是由JVM虚拟机内部的C代码实现的,不是由Java代码实现的。

线程同步

线程安全的简单介绍

上面那点相信大家即使不是很清楚应该也影响不大,毕竟我当时好像也是被逼急了才给出调用run()方法这种下三滥的手段,但线程同步就是毫无疑问对于每个电梯选手必须了解的课题了。

简单介绍一下线程同步吧。在Java中,当多个线程同时运行时,线程的调度由操作系统决定,程序本身无法决定。因此,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。这个时候,有个单线程模型下不存在的问题就来了:如果多个线程同时读写共享变量,会出现数据不一致的问题。这时候就不得不提到这次作业的一个重要难题——合理的加锁和解锁。

Java程序使用synchronized关键字对一个对象进行加锁,synchronized保证了代码块在任意时刻最多只有一个线程能执行,从而解决了多线程同步访问共享变量的正确性问题。但是,它的缺点是带来了性能下降。因为synchronized代码块无法并发执行。此外,加锁和解锁需要消耗一定的时间,所以,synchronized会降低程序的执行效率。

在了解这些之后,线程锁的使用还是比较简单的,如果你使用的是非线程安全容器(如ArrayList)作为共享容器时,找出修改共享变量的线程代码块,选择一个共享实例作为锁,最后用synchronized套起来就可以了。

如何省略synchronized减少运行时间

好吧,相信这些大家都是知道的,下面我来讲一些可以不用synchronized的地方,毕竟就像前文所说的那样其实synchronized代码块会增加代码运行时间的。

首先,单条原子操作的语句不需要同步。原子操作是在操作系统的辅助下可以一步执行完的语句(引语),所以他不存在线程安全的风险,所以也就没有嵌套的synchronized的必要。如下面这个例子中的synchronized就可以省略。

1
2
3
4
5
public void set(int m) {
synchronized(lock) {
this.value = m;
}
}

除此之外,synchronized关键词虽然表面上叫做对于读写的安全加锁,但我们可以简单考虑一下,如果两个线程同时要获得一个共享数据的值,他们同时读取这个数据的值并不会有什么影响,换言之,我们真正需要加锁的线程是那些写操作的线程,只要保证共享数据完成被改写的过程中不会被其他线程读取,也就完成了线程安全的功能了。

synchronized在方法中的使用

我们知道Java程序依靠synchronized对线程进行同步,使用synchronized的时候,锁住的是哪个对象非常重要。让线程自己选择锁对象往往会使得代码逻辑混乱,也不利于封装,更好的方法是把synchronized逻辑封装起来。

换言之,我们可以自己定义一个线程安全的容器,在源头上解决线程同步问题,这样在其他类里使用这个容器的数据时也可以更加省心。(其实java中有许多帮你包装好的线程安全容器,如CopyOnWriteArrayList,但我觉得这样没有达到练习的目的,所以没有选择走这条捷径,但去看一看这些线程安全容器的内部代码书写还是非常涨知识的)。

而在容器内部,我们就不可避免的要将一些方法设为synchronized的,那我们要怎么理解synchronized的方法呢,我写一个简单的例子,相信大家就明白了。

1
2
3
public synchronized void add(int n) {
count += n;
}

这是一个被synchronized了的代码,将其用我们容器的synchronized来转化其实可以写成下面几行语句,他们在使用时是完全相同的。

1
2
3
4
5
public void add(int n) {
synchronized(this) {
count += n;
}
}

因此,用synchronized修饰的方法就是同步方法,它表示整个方法都必须用this实例加锁,这样理解也就易懂了许多。

我们再思考一下,如果对一个静态方法添加synchronized修饰符,它锁住的是哪个对象?

对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。上述synchronized static方法实际上相当于:

1
2
3
4
5
6
7
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {
...
}
}
}

wait()和notifyAll()

在debug过程中,我经历了很多不同的TLE的情况,所有都是拜这两个函数所赐,一会是notifyAll()多了导致轮询最终CPU_TIME_TLE,一会又是notifyAll()少了导致某些线程不能顺利结束造成REAL_TIME_TLE,感觉自己就不停在两个极端反复横跳,究其原因,一方面是我没有使用线程安全容器而是可以使用synchronized导致编码难度偏高,另一方面也确实对多线程不够了解,所以在此简单总结一下并给出一些细节点。(当然帆姐那篇已经讲的很好了,在此引流一下——学习《图解JAVA多线程设计模式》分享

基础知识

在Java程序中,synchronized解决了多线程竞争的问题,但是synchronized并没有解决多线程协调的问题。我们在使用while()循环时,如果没有wait(),while()循环永远不会退出。因为线程在执行while()循环时,已经在函数入口获取了this锁,其他线程根本无法调用该函数,因为该函数执行条件也是获取this锁。

因此,多线程协调运行的原则就是:当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。

使用notifyAll()将唤醒所有当前正在this锁等待的线程,而notify()只会唤醒其中一个)。通常来说,notifyAll()更安全。有些时候,如果我们的代码逻辑考虑不周,用notify()会导致只唤醒了一个线程,而其他线程可能永远等待下去醒不过来了。

一个细节点

首先重要的事情说三遍,我们一定要在在while()循环中调用wait(),而不是if语句,这个是我真的使我de了很久非常痛苦的bug。

为什么一定要用while呢,因为线程被唤醒时,需要再次获取this锁,而在每部电梯都wait()一次之后,锁又自动回到了第一部电梯的手上,此时已经没有wait()了,也就跳出了if语句,最终导致如果长时间没有乘客加入,就会造成非常严重的轮询。所以,要始终在while循环中wait(),并且每次被唤醒后拿到this锁就必须再次判断。

所以,正确编写多线程代码是非常困难的,需要仔细考虑的条件非常多,任何一个地方考虑不周,都会导致多线程运行时不正常。

作业分析

作业总体概况

这一单元作业三次的结构结合度非常高,三次下来完成了一个简单的电梯调度问题,总体下来还是非常有难度的。,但同样也是收获满满。

第一单元要求我们实现了对于一栋楼的6部电梯接送乘客的调度要求,主要在于让我们初步熟悉多线程的相关操作。第二单元是在此基础上添加了维护电梯和动态增加电梯的操作,算是比较简单的一轮迭代。这次作业的主要难度在于第三次作业,这次作业要求我们实现控制每层楼的接送人以及纯接人的电梯数量,并限制了电梯可停靠的楼层数,要求实现电梯多次换乘的操作。

在三次作业迭代下我的uml类图如下:

hw7

时序图如下:Dispatcher_run

行数分析

文件 行数
Building.java 17
Dispatcher.java 66
Elevator.java 485
ElevatorQueue.java 59
Floor.java 35
Graph.java 92
Main.java 39
Person.java 109
RequestQueue.java 44
Schedule.java 68
Strategy.java 243

和大多数架构相同,由于电梯的本身运行逻辑比较复杂,所以也就导致虽然我为电梯类单独写了调度器和策略类,但还是会出现电梯类的复杂冗长的问题,这应该是因为我写的调度策略非常类似ALS,这本身就比look算法要更加难写,而其他部分代码的行数均处于正常水平。

复杂度分析

method CogC ev(G) iv(G) v(G)
Building.Building() 1.0 1.0 2.0 2.0
Building.getBuilding() 0.0 1.0 1.0 1.0
Dispatcher.Dispatcher(RequestQueue, ElevatorQueue, Building) 0.0 1.0 1.0 1.0
Dispatcher.run() 14.0 3.0 7.0 8.0
Elevator.checkRoad() 12.0 1.0 8.0 8.0
Elevator.close(boolean) 3.0 1.0 2.0 3.0
Elevator.echo() 1.0 1.0 2.0 2.0
Elevator.Elevator(int, RequestQueue, ElevatorQueue, int, Building) 3.0 1.0 3.0 3.0
Elevator.Elevator(int, RequestQueue, int, int, double, ElevatorQueue, int, …) 3.0 1.0 3.0 3.0
Elevator.elevatorClear(int, boolean) 11.0 1.0 6.0 6.0
Elevator.elevatorCommonOpen(boolean) 23.0 1.0 8.0 8.0
Elevator.elevatorErrorOpen() 18.0 1.0 7.0 7.0
Elevator.elevatorMove() 3.0 1.0 1.0 3.0
Elevator.elevatorOpen() 5.0 2.0 3.0 4.0
Elevator.elevatorOpenAccept() 5.0 2.0 3.0 4.0
Elevator.elevatorReverse() 0.0 1.0 1.0 1.0
Elevator.elevatorStop() 1.0 1.0 2.0 2.0
Elevator.elevatorWait() 1.0 1.0 1.0 2.0
Elevator.getBuffers() 0.0 1.0 1.0 1.0
Elevator.getId() 0.0 1.0 1.0 1.0
Elevator.getPos() 0.0 1.0 1.0 1.0
Elevator.getRequests() 0.0 1.0 1.0 1.0
Elevator.getRes() 0.0 1.0 1.0 1.0
Elevator.isEmpty() 4.0 3.0 3.0 4.0
Elevator.isRequestEmpty() 4.0 3.0 3.0 4.0
Elevator.notEnd() 0.0 1.0 1.0 1.0
Elevator.open() 1.0 1.0 1.0 2.0
Elevator.run() 14.0 4.0 10.0 10.0
Elevator.setEnd(boolean) 0.0 1.0 1.0 1.0
Elevator.setRes(int) 0.0 1.0 1.0 1.0
Elevator.stop() 6.0 1.0 4.0 4.0
Elevator.synchronizedBuffers(Vector, RequestQueue) 14.0 3.0 9.0 9.0
Elevator.synchronizedList(Vector, Vector) 10.0 3.0 10.0 10.0
Elevator.toString() 0.0 1.0 1.0 1.0
ElevatorQueue.add(Elevator) 0.0 1.0 1.0 1.0
ElevatorQueue.ElevatorQueue(RequestQueue) 0.0 1.0 1.0 1.0
ElevatorQueue.getRequests() 0.0 1.0 1.0 1.0
ElevatorQueue.hasEmpty() 12.0 5.0 7.0 8.0
ElevatorQueue.isEmpty() 4.0 3.0 3.0 4.0
ElevatorQueue.remove(int) 0.0 1.0 1.0 1.0
ElevatorQueue.toString() 1.0 1.0 2.0 2.0
Floor.addLayers() 1.0 2.0 1.0 2.0
Floor.addOpen() 2.0 2.0 1.0 3.0
Floor.reduceLayers() 0.0 1.0 1.0 1.0
Floor.reduceOpen() 0.0 1.0 1.0 1.0
Graph.add(Elevator) 3.0 1.0 3.0 3.0
Graph.dfs(int, int, Vector) 15.0 7.0 7.0 8.0
Graph.getGraph() 0.0 1.0 1.0 1.0
Graph.hasRoad(Vector, Vector) 7.0 4.0 6.0 7.0
Graph.printVector(Vector, int) 1.0 1.0 2.0 2.0
Graph.remove(Elevator) 3.0 1.0 3.0 3.0
Graph.searchNext(int, int, int) 1.0 1.0 1.0 2.0
Graph.setGraph(ConcurrentHashMap>) 0.0 1.0 1.0 1.0
Main.main(String[]) 2.0 1.0 3.0 3.0
Person.getFromFloor() 0.0 1.0 1.0 1.0
Person.getPersonId() 0.0 1.0 1.0 1.0
Person.getTempFromFloor() 0.0 1.0 1.0 1.0
Person.getTempToFloor() 0.0 1.0 1.0 1.0
Person.getToFloor() 0.0 1.0 1.0 1.0
Person.in (Elevator) 5.0 3.0 4.0 5.0
Person.init(int) 0.0 1.0 1.0 1.0
Person.isOut() 0.0 1.0 1.0 1.0
Person.next() 1.0 1.0 2.0 2.0
Person.notEnd() 0.0 1.0 1.0 1.0
Person.out() 4.0 2.0 3.0 4.0
Person.Person(int, int, int) 0.0 1.0 1.0 1.0
Person.Person(PersonRequest) 0.0 1.0 1.0 1.0
Person.setChangePos(Vector) 0.0 1.0 1.0 1.0
RequestQueue.addRequest(Person) 0.0 1.0 1.0 1.0
RequestQueue.checkSame(Person, Elevator) 4.0 3.0 5.0 5.0
RequestQueue.getRequests() 0.0 1.0 1.0 1.0
RequestQueue.isEmpty() 0.0 1.0 1.0 1.0
RequestQueue.isEnd() 0.0 1.0 1.0 1.0
RequestQueue.RequestQueue() 0.0 1.0 1.0 1.0
RequestQueue.setEnd(boolean) 0.0 1.0 1.0 1.0
Schedule.run() 38.0 8.0 20.0 21.0
Schedule.Schedule(RequestQueue, ElevatorQueue) 0.0 1.0 1.0 1.0
Strategy.checkDir(ConcurrentHashMap>, RequestQueue, boolean, int, Vector) 2.0 1.0 2.0 2.0
Strategy.checkEmpty(ConcurrentHashMap>) 4.0 3.0 3.0 4.0
Strategy.checkReverse(ConcurrentHashMap>, RequestQueue, boolean, int, Vector) 20.0 9.0 13.0 14.0
Strategy.checkSignDir(ConcurrentHashMap>, RequestQueue, int, Vector) 22.0 10.0 14.0 15.0
Strategy.checkUnSignDir(ConcurrentHashMap>, RequestQueue, int, Vector) 22.0 10.0 14.0 15.0
Strategy.isOpen(ConcurrentHashMap>, int) 1.0 1.0 2.0 2.0
Strategy.isOpenAccept(ConcurrentHashMap>, Vector, int, int, boolean, Vector) 18.0 9.0 18.0 20.0
Strategy.lookStrategy(int, int, boolean, ConcurrentHashMap>, RequestQueue, Vector, boolean, …) 16.0 8.0 11.0 13.0
Total 366.0 172.0 286.0 315.0

从上表不难看出,我的架构复杂度主要来自两个因素,一方面来自于图遍历算法dfs本身O(n!)的复杂度加成,另一方面来自于各种run()进程及其包含的类共同加成的结果。这里我们需要重点关注Strategy这个类,这个类是我用于调度出电梯行为的类,由于我在我的架构中设计了缓冲区,带来的后遗症也就是判断电梯行为时的逻辑非常复杂,也就间接导致了我的Strategy类难以处理,复杂度较高。

bug分享及debug思路

这三次作业互测都没有出bug,但第二次强测我被狠狠橄榄了,主要出了一个由于线程不同步的问题,导致我维护电梯的id会多次出现,最终只改了4行就改掉了,但还是避免不了被橄榄的事实。

由于多线程的bug非常不好复现,所以我选择使用了朴实无华的println方法来debug,虽然方法简单,但很有效,甚至帮我解决了查轮询很麻烦的问题(真的奇效orz)。

研讨课收获

在这几次研讨课上我主要更加深入地了解了solid算法,更让我有了更加深刻的需要在自己编码时时刻注重于代码结构,必要时强迫自己使用合适的工程模式。

架构设计体验

第五次作业简单架构介绍

基本情况

首先设计了一个Dispatcher作为输入线程不断从ElevatorInput读取请求,然后将请求装入RequestQueue中,此时RequestQueue就是我们要保护线程安全的公共资源,与此同时,我为每个电梯Elevator编写了Schedule调度器类和Strategy策略类,从而降低耦合,让每个线程可以做好自己主要负责的部分

调度策略

调度方面,我使用的是ALS策略。如果将ALS和LOOK策略进行了横向对比,认为ALS策略的最大优势在于省电,且在小数据集的表现较优,而LOOK算法可能导致多部电梯同时响应,但在大数据方面表现良好。

细节剖析

这次作业按照生产者,消费者进程理解的话,Input是生产者,Elevator是消费者,而消费的盘子即公共资源是Person,所以我还额外编写了Person类继承了课程组提供的PersonRequest类,并在里面编写了in和out方法,这样封装后就更加接近生产者消费者模式。

第六次作业迭代思路

新增电梯

把电梯类中的相关可变参数改为成员变量,并增加一个新增电梯的构造方法。Dispatcher每读到一个新增请求就多开一个电梯线程。

同时设计了ElevatorQueue来控制此时正在运行的电梯,这个设计同样适用于维护电梯。

维护电梯

在电梯收到维护指令后,首先将电梯的end成员变量设置为true,这样就会调用Strategy类的STOP方法,在stop中分为电梯为空和不为空两种情况,为空则省略了维护电梯开关的过程,不为空则将当时所有电梯内人排出,并在判断此时的楼层pos和tofloor的关系,如果不同则重新加入电梯队列。

小tips:

电梯为停止状态需要从第一次架构产生修改。即需要保证所有电梯和等待队列均为空才可以结束电梯线程,剩下情况需要等待。

这样还会出现所有电梯都已经停止但最后指令是maintain的情况,所以我们需要额外设置保证输入结束之后才结束线程。

第七次作业迭代思路

换乘

由于题目中最后很有可能会关闭所有的全能电梯,所以实现换乘是绝对必要的。

换乘图的设计

我们可以设计一个图结构来进行换乘操作的实现。

对于图本身而言,由于对于我们的电梯调度本质上只需要一个图,我选用了ConcurrentHashMap<Integer, Vector>来存储每个楼层当时可达电梯的集合。所以我们可以设置为一个静态类,然后通过调用图的静态方法即可实现全局性的调控。

在图的算法方面,可以选用最短路径算法和dfs来实现,但这次作业而言,使用最短路径会导致大量请求堆积于全能电梯,所以我更倾向于选用dfs求出可达解即可。(这里在使用dfs时需要用book数组进行到过的楼层标记,否则可能会出现圈的情况)

最后我们可以把这个要走的路线按照一个数组存起来,叫做path。并将其作为每个请求person的一个属性。

换乘时的设计

首先在前两次作业没有换成的需求,所以我们也就只有tofloor和fromfloor两个变量,但在这次迭代中,我们需要将这两个变量改为对外露出路径中某一步的起点和终点。并将当前终点和目的地相同时作为结束条件即可。

由于调度过程中会出现maintain指令,所以可能出现在调度一半后需要换路径的情况,这里其实我们只需要考虑person在等待队列还未进电梯时电梯被maintain的情况,因为如果在电梯内被maintain,在放下乘客后自然是需要重现规划路径的。当放入乘客,由于我写了Strategy类,所以我会在每次电梯决定是否开门前如果发现某个乘客的路径上有被维护的电梯(即不可达)就选择重新规划路线。

电梯门数量控制

对于电梯开门数量来说我们可以设置一个新的公共变量来作为锁进行楼层开门数量控制。我选择的是设置了楼层floor类作为公共变量,并对floor写了addOpen(),addLayers(),reduceOpen(),reduceLayers(),之后用类似信号量机制当成一把lock锁使用即可。(当然也可以直接使用semaphore,但我是周五前写完的所以没有用)

每层电梯门数量控制

每层电梯门的数量用addlayers()和reducelayers()直接控制即可。

只进电梯数量控制

如何判断是否为只进电梯呢?最好的办法是看电梯open进出后前一个电梯内id集合的状态是否为后一个id集合状态的子集,之后可以通过这个因素来判断是否为只接人电梯。

1
2
3
4
5
6
7
8
9
10
synchronized (floor) {
while (!floor.addLayers()) {
try {
floor.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
open();
}

迭代细节

如何在迭代中保持高内聚低耦合?

首先我们要从solid原则的单一功能原则出发。在我的架构中,增加和删除维护电梯的操作全部交给了ElevatorQueue来维护,而电梯在知道自己被end前一直保持着正常的调度不受影响。Strategy负责给出电梯下一步行为的指导,Schedule专门负责调度等待队列,Graph专门用于调度路线,这样专职专用可以很好地降低耦合度。(高内聚——功能内聚,低耦合——控制耦合

第二点是不要重复造轮子,对于一些很常用或者使用类型比较相似的方法,我比较喜欢写一个静态类存储一些静态方法(如Graph和Strategy类)专门供全局调用和一些小的private的方法。这样就起到了类似函数的作用,便于修改和维护。(高内聚——功能内聚

第三点是需要减少非必要的公共数据的传递,例如我在第一次架构设计时在Elevator中传入了ElevatorQueue这个参数,这个就是一个十分不合理的公共数据分享,甚至可能出现线程安全问题,这种设计就需要优化掉,经可能避免非必要的公共数据传递。(低耦合——数据耦合

设计模式

  • 生产者消费者模式:输入类作为生产者,任务表作为托盘,电梯作为消费者

  • 工厂模式:自定义电梯的实现

  • 状态模式:电梯可以有好几种状态,上行下行,开门关门,满员,故障

  • 策略模式:主要考虑到,电梯策略的选择,与分派器策略的选择

  • 单例模式:Graph的实现

心得体会

线程安全

​ 线程安全是多线程相较于单线程而言难度较大的地方。其存在三个问题:读写、死锁以及轮询。

​ 对于读写,其有些类似于上学期计组课设流水线CPU设计。通过本单元的学习,我掌握了基本的确保线程安全的方式,学会了正确地加锁以及如何尽可能灵活地加锁(而不是一味地使用笨重的synchronized对整个方法加锁)。

​ 对于死锁,应该采用正确的wait–notifyAll方式编程,并回避循环引用。

​ 对于轮询,我设置了合理的等待条件,使得电梯以及调度器线程可以在合适的时候进入休眠,避免占用过多的CPU资源。

总结

第二单元作为OO的重中之重,还是非常让人舒爽的,多线程bug难以复现的问题真的让人非常折磨,这三周在非常忙碌的学习生活中真的让我从来没有这么充实过,减少了不少摸鱼的时间~。

OO学习到的经验而言,首先我们一定多多关注讨论区。当某个地方陷入死局时,阅读一下同学的思路和分享也许会让你豁然开朗,更有机会可以白嫖各种大佬的评测机等“神器”。第二是要关注代码复杂度,尽量不要写太长的方法。即使不得不写,也要多加小心,因为很有可能有bug藏匿其中。如果迫不得已要写,尽量强迫自己不要写大量相似的代码,提高代码的复用性。第三是优化时要慎重,正确性一定要放在第一位,如果没有正确性,性能分一点没有,还会有很多补bug的痛苦经历,得不偿失的。第四是注意测试的全面性。 在测试的时候不能只进行随机测试,还要针对我们做的优化多测几组特殊数据。最后,我们一定要注意代码可扩展性。写代码时不仅仅要着眼于本次作业的要求,还需要思考下一次作业中可能会有哪些新的要求,为迭代开发做好准备。

最后,在本单元作业中,老师、助教和同学们都给予了我相当大的帮助,数千行代码的书写也进一步提升了我的代码能力,并将我从面向过程的舒适区中拽了出来,希望后面的OO课程我可以取得更好的成绩,互测成绩越来越好。

  • Title: OO_summary_2.Elevator_dispatch
  • Author: Charles
  • Created at : 2023-04-09 21:21:22
  • Updated at : 2023-06-17 14:13:59
  • Link: https://charles2530.github.io/2023/04/09/oo-summary-2-elevator-dispatch/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments