OO_第二单元总结

第二次总结博客(电梯单元)

16071070 陈泽寅 2019.4.20

一:多线程实验初感

这个单元是多线程设计的实践单元,主要让我们运用多线程的原理与思想去完成一个模拟电梯运行的策略。从最开始的单步电梯的傻瓜式调度,到第二次作业的单步电梯的捎带式策略,再到第三次作业的多部电梯捎带式运行策略。一次次的难度加强,也让我们发现了多线程的使用规则和方法,并且在一次次的bug中更加体会到了锁的机制,以及各种并发机智的使用规则。虽然还是有很多的问题,但是从这个单元确实学到了很多东西。

二:3次单元作业的设计策略

第五次编程作业(单线程傻瓜电梯)

这个单元编程作业,我设计了两个线程类来完成所需的请求。分别为:输入请求类也就是我们的生产者类,还有一个线程就是我们的电梯,也就是消费者类。两个线程共享一个请求队列。输入请求线程是非阻塞的,它负责向请求队列里写入请求,而电梯线程负责从请求队列里读出请求。并执行相应的请求。

相应的类图如下。

里面一共有三个类,其中Quene是我们的共享队列,是一个资源,供InputElevator线程去争夺。因此Quene里的方法都应该写成同步方法,这样才能避免线程不安全的情况发生。时序逻辑如下。

输入线程不断地向Quene里写入请求,Elevator不断地从Quene里取出请求并且执行。当Input读入null结束请求时,便向Elevator发出一个结束信号,此时Input线程结束。Elevator线程收到这个结束信号时,判断Quene是否为空,不为空则继续取出任务执行完。直到Quene也为空时则 Elevator线程也结束并退出。

  • 第五次作业代码分析报告:
sourceFile TotalLine
Dispatcher.java 3
Elevator.java 99
Input.java 53
Quene.java 44

度量分析:

method ev(G) iv(G) v(G)
Elevator.close() 1.0 2.0 2.0
Elevator.Elevator(Quene) 1.0 1.0 1.0
Elevator.go(int) 1.0 2.0 2.0
Elevator.in(int) 1.0 2.0 2.0
Elevator.open() 1.0 1.0 1.0
Elevator.out(int) 1.0 2.0 2.0
Elevator.run() 4.0 5.0 6.0
Input.getFlag() 1.0 1.0 1.0
Input.Input(Quene) 1.0 1.0 1.0
Input.main(String[]) 1.0 1.0 1.0
Input.run() 3.0 3.0 4.0
Quene.getList() 1.0 1.0 1.0
Quene.getNum() 1.0 1.0 1.0
Quene.requestGet() 1.0 2.0 2.0
Quene.write(PersonRequest) 1.0 1.0 1.0
Total 20.0 26.0 28.0
Average 1.33 1.73 1.87
  • 代码分析:

    我们可以看到这次代码中,ELevator.run的复杂度较高,这是因为在第一次多线程的过程中没能够把代码模块化,导致电梯在运行的过程中还承担了很多本不应该电梯来做的事情。其实在第一次作业中,由于没有涉及调度器,此时电梯还需要同时扮演调度起的角色,因此其复杂度会比较高。

  • 代码缺陷:

    第一次多线程实验中,还没有理解到线程wait()notify()的好处与作用,因此并没有使用等待唤醒机制,导致电梯进程一直在轮询判断Quene里到底有没有新来的请求,这也导致当请求队列位空,但是输入进程还没有结束的时候,电梯线程一直在做无谓的动作,占据了大量的CPU时间,这是一个很愚蠢的设计,解决方法就是引入了等待唤醒机制,具体实施在下一次实验中。

  • 关于Solid原则

    这次的作业没有很好的遵循SOLID原则,首先电梯线程参与了向Quene索要请求的事情,违反了SRP原则。而电梯应该只需管自己的运行,应该把请求的分配交给Dispatcher线程来管。同时也没有很好的践行ISP原则,没有实现接口。

第六次编程作业(多线程捎带电梯)

在第六次编程中虽然还是一部电梯,但是引入了捎带的策略。我吸取上一次编程中违反SOLID的教训,将Elevator线程与调度分开,重新设计了一个Dispatcher调度器线程,其作用是从Quene中获取请求,并且根据Elevator的运行状态将其分配给电梯即可。而这次的电梯有一个主请求,和一个捎带请求队列。电梯不管怎么获得请求,它只管按照自己的请求去执行上楼下楼,上客下客的操作。请求的分配只归Dispatcher来管。
电梯的数据结构如下。

private volatile int floor = 1; //初始化在1层
private volatile PersonRequest mainRequset = null;//主请求
private volatile Vector<Person
            subRequest = new Vector<Person();
private volatile int empty = 1;//主请求是否为空的标志
private volatile int inside = 0;
private volatile int status = 1;//0 means down,1 means up

其中每个电梯都有一个mainRequest,和一个Vector类型的SubRequest,并且每个电梯会返回给调度器他的当前楼层,他的运行方向等信息,来让Dispatcher完成新的调度。

类图

可以看到最大的变化就是增加了一个Dispatcher调度器类,这个类连接了Elevator线程和Input线程。它从Input中获取请求,然后分配给Elevator

时序图

  • 1: Input线程和Dispatcher之间的时序关系

从上图可知,Input每次来消息,写入到Quene中去,然后会向Dispatcher发起一个notify信号唤醒正在等待的调度器,告诉它有新的任务来了,他可以恢复调度。然后DispatcherQuene中去取心得请求,把它放到自己的队列中,并完成与Elevator的交互。之后如果没有其他新的请求,则它wait()。等待心得唤醒信号。

  • 2:Dispatcher线程和Elevator线程的时序关系图。

Elevator线程当自己主请求为空的时候,就进行wait(),等待调度器线程给其分派请求。Dispcther给其分派请求时,notify()这个电梯,其执行自己的主请求和副请求,一旦主请求结束,并且没副请求时,电梯notify()调度器,让它给自己分配新的请求。

  • 3:结束判断标志

1:Input结束标志为读到null

2:Dispatcher结束标志为Input结束并且Quene以及自己的请求队列全部分配完成为空时。

3:Elevator结束标志为Dispacther结束并且自己当前的主请求以及副请求全部执行完毕。

代码分析报告

代码规模

Source File Total Lines Source CodeLines
Clock.java 3 3
Dispatcher.java 137 111
Elevator.java 269 233
Input.java 87 69
Person.java 30 20
Quene.java 51 33
Total 577 469

代码复杂度分析

method ev(G) iv(G) v(G)
Dispatcher.check() 1.0 6.0 6.0
Dispatcher.Dispatcher(Elevator,Quene) 1.0 1.0 1.0
Dispatcher.getEndFlag() 1.0 1.0 1.0
Dispatcher.isSubrequest(PersonRequest) 4.0 6.0 7.0
Dispatcher.run() 3.0 14.0 14.0
Elevator.arrive() 1.0 1.0 1.0
Elevator.checkcurrentFloor() 1.0 3.0 4.0
Elevator.close() 1.0 1.0 1.0
Elevator.getCurrentFloor() 1.0 1.0 1.0
Elevator.getMainRequset() 1.0 1.0 1.0
Elevator.getStatus() 1.0 1.0 1.0
Elevator.getSubRequest() 1.0 1.0 1.0
Elevator.in(int) 1.0 1.0 1.0
Elevator.loopOut() 1.0 6.0 6.0
Elevator.method1() 1.0 9.0 9.0
Elevator.open() 1.0 2.0 2.0
Elevator.out(int) 1.0 1.0 1.0
Elevator.run() 4.0 10.0 12.0
Elevator.setMainRequset(PersonRequest) 1.0 1.0 1.0
Elevator.up(int,int) 1.0 17.0 23.0
Input.getFlag() 1.0 1.0 1.0
Input.Input(Quene) 1.0 1.0 1.0
Input.main(String[]) 1.0 1.0 1.0
Input.run() 3.0 7.0 8.0
Person.getInside() 1.0 1.0 1.0
Person.getRequest() 1.0 1.0 1.0
Person.Person(PersonRequest) 1.0 1.0 1.0
Person.setInside(int) 1.0 1.0 1.0
Person.setRequest(PersonRequest) 1.0 1.0 1.0
Quene.addNum() 1.0 1.0 1.0
Quene.getList() 1.0 1.0 1.0
Quene.getNum() 1.0 1.0 1.0
Quene.remove(int) 1.0 1.0 1.0
Quene.setList(Vector<PersonRequest) 1.0 1.0 1.0
Quene.setNum(int) 1.0 1.0 1.0
Quene.subNum() 1.0 1.0 1.0
Quene.write(PersonRequest) 1.0 2.0 2.0
Total 47.0 108.0 119.0
Average 1.27 2.92 3.22

Elevator.up()的复杂度比较高,原因在于电梯的运行时需要判断每一层是否有要进出的客人,并且需要更新其楼层运行状态,因此复杂度较高。

SOLID分析

这次作业基本符合SOLID要求,满足SRP要求,每个线程只做自己份内的事情,Input只负责输入,Elevator只负责根据请求来上下运行,而Dispatcher负责从队列里取请求分配给电梯。每个线程之间的耦合性很小。

1:单一性原则:添加了Dispacther类,使电梯线程不再负责请求的调度。

2:开放封闭原则:这点没有很好地实现,电梯在运行的地方很多都是硬编码,不能很好的实现扩展。

3:里氏替换原则:因没有使用Extends因此不存在此问题

问题分析

1:存在的问题主要是线程与线程之间的通信太过频繁,线程与线程之间的关联还不够分离。比如Input需要和Dispatcher交互,而Dispatcher需要和Elevator进行交互,频繁的交互使程序逻辑显得比较混乱。

2:优势,所有的地方取消了轮询查询,而是全部采取了wait()notify()机制,减少CPU不必要的执行时间,很好地迎合了多线程机制的规则。

第七次作业(多线程多电梯调度实验)

这次是第三次多线程实验,相比较前两次实验这次实验使用了三部电梯,并且每部电梯有自己不同的停靠楼层,不同的搭乘上限,不同的运行速度。这就不能采取之前的硬编码模式,需要对电梯的类进行改造。

电梯类:

    private volatile int floor = 1; //初始化在1层
    private volatile Vector<Person taskList = new Vector<Person();
    private Dispatcher dispatcher;
    private String name;
    private long uptime;
    private int currentPersonNum = 0;
    private int space;
    private int[] stayFloor;
    private volatile int freeFlag = 1;

每个电梯有自己的当前楼层,名字,自己的请求列表,自己的姓名,运行速度,电梯内空间与当前人数等私有属性。还有一个重要的私有属性,就是它的停靠楼层stayFloor,这是个一位数组,其在每个电梯被创建的时候初始化。

int[] a = {-3, -2, -1, 1, 15, 16, 17, 18, 19, 20};
int[] b = {-2, -1, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
int[] c = {1, 3, 5, 7, 9, 11, 13, 15};
Elevator elevatorA = new Elevator("A", 400, 6, a);
Elevator elevatorB = new Elevator("B", 500, 8, b);
Elevator elevatorC = new Elevator("C", 600, 7, c);

这就初始化好了每一个电梯的信息。

调度原则:

因为这次作业涉及换乘,因此调度策略发生改变,调度任务依旧由Dispatcher线程来执行。具体策略为每来一个请求,调度器就根据现有电梯的停靠楼层判断其是否需要换乘,如果需要换乘,则根据实际情况将这个请求拆分成两个请求。例如FR0M-2-TO-3,则需要先从2-1,再从1-3,拆分成两个请求之后,我们封装一个类Person类。

    private PersonRequest request1;
    private PersonRequest request2;
    private volatile int change = 0; //是否需要换乘
    private volatile int inside = 0;
    private volatile int first = 0;
    private volatile int second = 0;
    private volatile int finishFirst = 0; //是否已经完成第一次请求,等待执行换乘请求。

每个Person对象记录者自己有哪两个请求,并且第一个请求是否已经执行完,调度器根据这个对象的信息,将其分派给相应的电梯,电梯执行完第一个请求,则将其还给调度器,让调度器继续将其分派给下一个电梯完成换乘任务。

    public Person analyse(PersonRequest request) {
        Person person;
        int from = request.getFromFloor();
        int to = request.getToFloor();
        int id = request.getPersonId();
        int[] listFrom = getList(from); //获得停靠from楼层的电梯
        int[] listTo = getList(to); // 获得停靠to楼层的电梯
        int coincide = isCoincide(listFrom, listTo);
        /**
         * public Person(PersonRequest request1, PersonRequest request2,
         * int change, int first, int second)
         */
        if (coincide != -1) { // no need change
            person = new Person(request, null, 0, coincide, 0);
        } else { //需要换乘的情况
            PersonRequest[] newRequest =
                    newTwoRequest(listFrom, listTo, request);
            person = new Person(newRequest[0], newRequest[1],
                    1, listFrom[0], listTo[0]);
        }
        return person;
    }

这个函数就是完成请求拆分的方法,先获得一个请求的from和to楼层,然后看哪些电梯能够到达这些楼层,如果有重合的,则不需要换乘,否则找最快的两部电梯,到这两部电梯重合处进行换乘。生成两个新的请求。

电梯运行规则:

不再设定捎带请求,电梯根据自己的请求队列以及运行方向来进行运行状态的调整。如果当前运行状态向上,就会把请求队列里同方向的请求全部一次性处理,直到没有同方向的或者到达顶楼。如果一个请求执行完毕,并且其是个换乘请求则将其还给Dispatcher,如果其不需换乘,则直接将其从列表里移除。

类图:

时序图

时序图基本框架与第六次作业差不多

  • 1:DispactherInput的时序逻辑。

不同的地方在于Dispatcher线程的结束条件为Input线程结束,并且自己的请求队列为空,并且所有的电梯的请求为空。这是因为有时候电梯运行完一个换乘请求还会把剩下的请求还给调度器,如果提前结束,换乘请求的后半段就没办法完成了。

  • 2:DispatcherElevator的时序逻辑交互

此时的Dispatcher不能提前结束,须得等到自己的请求队列为空,并且三部电梯请求队列也为空并且Input线程向其发送End标识时,才能结束。

代码规模

Source File Total Lines Source CodeLines
Clock.java 3 3
Dispatcher.java 291 246
Elevator.java 320 276
Input.java 91 70
Person.java 61 45
Quene.java 22 16
Total: 788 656

代码复杂度

method ev(G) iv(G) v(G)
Dispatcher.analyse(PersonRequest) 1.0 2.0 2.0
Dispatcher.check() 1.0 6.0 6.0
Dispatcher.Dispatcher(Elevator[],Quene) 1.0 1.0 1.0
Dispatcher.down(int,int,PersonRequest) 6.0 10.0 12.0
Dispatcher.getEndFlag() 1.0 1.0 1.0
Dispatcher.getList(int) 4.0 3.0 5.0
Dispatcher.getQuene() 1.0 1.0 1.0
Dispatcher.isCoincide(int[],int[]) 4.0 1.0 4.0
Dispatcher.newTwoRequest(int[],int[],PersonRequest) 7.0 11.0 13.0
Dispatcher.run() 3.0 23.0 23.0
Elevator.arrive() 1.0 1.0 1.0
Elevator.close() 1.0 1.0 1.0
Elevator.compareFloor(int) 2.0 1.0 2.0
Elevator.Elevator(String,int,int,int[]) 1.0 1.0 1.0
Elevator.findMaxTask(int) 1.0 6.0 6.0
Elevator.getCurrentFloor() 1.0 1.0 1.0
Elevator.getFreeFlag() 1.0 1.0 1.0
Elevator.getIndex(int) 3.0 1.0 3.0
Elevator.getStayFloor() 1.0 1.0 1.0
Elevator.getTaskList() 1.0 1.0 1.0
Elevator.getUptime() 1.0 1.0 1.0
Elevator.in(int) 1.0 1.0 1.0
Elevator.method1() 2.0 6.0 9.0
Elevator.open() 1.0 2.0 2.0
Elevator.out(int) 1.0 1.0 1.0
Elevator.outThisfloor() 3.0 23.0 24.0
Elevator.run() 5.0 8.0 9.0
Elevator.setDispatcher(Dispatcher) 1.0 1.0 1.0
Elevator.setFreeFlag(int) 1.0 1.0 1.0
Elevator.up(int,int) 2.0 6.0 10.0
Input.getFlag() 1.0 1.0 1.0
Input.Input(Quene) 1.0 1.0 1.0
Input.main(String[]) 1.0 1.0 1.0
Input.run() 3.0 5.0 6.0
Person.getChange() 1.0 1.0 1.0
Person.getFinishFirst() 1.0 1.0 1.0
Person.getFirst() 1.0 1.0 1.0
Person.getInside() 1.0 1.0 1.0
Person.getRequest1() 1.0 1.0 1.0
Person.getRequest2() 1.0 1.0 1.0
Person.getSecond() 1.0 1.0 1.0
Person.Person(PersonRequest,PersonRequest,int,int,int) 1.0 1.0 1.0
Person.setFinishFirst(int) 1.0 1.0 1.0
Person.setInside(int) 1.0 1.0 1.0
Quene.getList() 1.0 1.0 1.0
Quene.setList(Vector<PersonRequest) 1.0 1.0 1.0
Quene.write(PersonRequest) 1.0 2.0 2.0
Total 79.0 146.0 168.0
Average 1.68 3.11 3.57

SOLID分析

1:单一性原则:每个类的职责明确,Input只负责输入,Elevator只负责根据请求来上下运行,而Dispatcher负责从队列里取请求分配给电梯。每个线程之间的耦合性很小。

2:开放封闭原则:实现软编码,可以添加新的电梯进入到这个系统,并且可以在原基础上更改Dispatcher以修改调度策略。

3:里氏替换原则:因没有使用Extends因此不存在此问题

存在问题

  • 调度策略比较简单:优点是调度策略简单明了,但是由于调度的过程是静态的,即来了请求就将其拆分,而没有考虑实际的电梯位置以及电梯里的空间等因素,却少动态的考虑,导致一些请求的调度不理想,或者一些电梯空闲等待等不良情况的发生。

三:作业存在的BUG

第五次作业:

  • 存在问题

结束进程的判断失误。当Input进程都到null请求时,发出Input结束信号,Elevator读到这个请求就结束了自己的线程。但其实队列中还存在请求没有执行完。

  • 解决方案:

在电梯收到Input线程结束的请求时,再判断一下请求队列是否还存在未完成的请求,如果没有,则电梯线程也结束,否则继续执行直到请求队列为空。

while (true) {
            if (Input.getFlag() == 1 && Quene.getNum() == 0) {
                break;
            }

第六次作业:

  • 存在问题

仍然是Dispatcher线程提前终止的问题。当Input请求发出终止信号时,Dispatcher去判断Quene是否为空,如果为空,则Dispatcher也终止。但实际上调度器自己的队列里还有请求没有分配给电梯执行。

  • 解决方案

在受到Input的结束请求时,Dispatcher增加一条对自己请求队列是否为空的判断,当input请求结束,Quene请求队列为空,自己的请求队列为空时,向电梯线程发送Dispatcher结束的信号。

while (true) {
            if (quene.size() == 0 && Input.getFlag() == 1
                    && requsetQuene.getList().size() == 0) {
                endFlag = 1;
                break;
            }

第七次作业

  • 存在问题

电梯运行状态的问题,电梯每次都会根据第一条请求确定运行方向,但如果当第一条请求为顶层,并且电梯已经满,则电梯在20层时又会进行一次判断,但它还是会向上,因此这是就会出现一个死循环。

  • 解决方案

当电梯运行到顶层或最低层的时候进行判断,改变其运行方向。

if(this.floor==stayFloor[stayFloor.length-1]){
            k = -1;
        }
        if(this.floor == stayFloor[0]){
            k = 1;
        }

这里的判断,当到了当前电梯的最底层时,方向向上,最高层时运行方向向下,就不会发生死循环的情况了。

四、验证方法:

为了模仿测评机的按时输入,我利用python以及linux的shell写了测试脚本,提取每条指令的时间,然后按时将指令放到管道中去,然后重定向到标准输入,模拟了在确定时间输入的情况。

解析时间:

import time
f = open(r"/Users/chenzeyin/Desktop/data",'r')
temp=f.readline()
sleeptime = []
req = []
lastTime = 0
while temp:
    temp = temp.strip("\n")
    gg = temp.split("]")
    gg[0]=gg[0][1:]
    sleeptime.append(float(gg[0])-lastTime)
    lastTime = float(gg[0])
    req.append(gg[1])
    temp = f.readline()    
sleeptime =[2.3,1.3]
req=["1-FROM-2-TO-5","2-FROM-3-TO-4"]
for i in range(len(req)):
    #print(sleeptime[i])
    time.sleep(sleeptime[i])
    print(req[i])
f.close()

测试

import os
import time
path = "/Users/chenzeyin/Desktop/czy/git/Elevator3/out/production/Elevator3"
def ownTest():
    os.chdir(path)
    os.system(r"python -u /Users/chenzeyin/Downloads/timeInput.py|java -Djava.ext.dirs=/Users/chenzeyin/Downloads Input/Users/chenzeyin/Desktop/out")
ownTest()

五、学习心得与总结

  • 1、多线程是一个非常困难但是有意思的章节。我们在学习多线程的时候要抓住一个大的方向,共享对象资源,当多个线程去抢夺一个资源时需要对那个对象的资源加锁,要搞懂synchronized的用法,并且不要盲目使用该方法,因为这会增加CPU不必要的开销。

  • 2、同时,尽量使用线程安全的容器,如vector,这样会减少线程冲突的可能性。

  • 3、合理使用wait(),notify()机制,减少CPU没必要的浪费时间。

posted @ 2019-04-20 16:41  愤怒的大猪蹄  阅读(164)  评论(0编辑  收藏  举报