面向对象设计与构造第二单元总结

面向对象设计与构造第二单元总结

概述

第二单元我们进入了多线程程序设计的学习。第一单元的训练让我们初步形成了面向对象思维,而第二单元则正式开始向实际工程靠拢。多线程,是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。所以在多线程程序设计中,性能永远放在第一位。

Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

第五次次作业

在第五次作业中,我们要实现的是单部多线程傻瓜电梯(FAFS)的模拟过程,本次作业本质上还是帮助我们熟悉多线程的操作,使用的算法是非常简单的傻瓜调度。

设计策略

本次作业模拟的是单部多线程电梯,所以我在本次程序设计中只使用了两个线程,一个作为输入线程,一个则是电梯线程,输入线程每接受到一个请求便加入到电梯的请求队列中,再由电梯调度完成模拟过程。

输入线程

在本单元作业中,输入线程都是由main方法来承载,

	public static void main(String[] args) throws Exception {
        ElevatorInput elevatorInput = new ElevatorInput(System.in);
        Thread elevator = new Elevator();
        elevator.setName("elevator1");
        elevator.start();

        while (true) {
            PersonRequest request = elevatorInput.nextPersonRequest();
            if (request == null) {
                elevatorInput.close();
                ((Elevator) elevator).setHasInput(false);
                return;
            }
            Person person = new Person(request);
            ((Elevator) elevator).addPersonRequestList(person);
        }
    }

而加入请求的过程大致参考了输入接口中的DEMO设计,每加入一个新的请求就将其加入到电梯请求队列中。在后面的作业中,我在每个电梯中都设置了一个电梯的请求队列。

电梯线程

本次作业由于FAFS调度策略简单,所以直接在电梯中设置了调度器。

电梯中只包含三个属性,请求队列,电梯目前楼层,和输入线程是否结束,通过一个个从请求队列头中取出请求并处理来实现调度。

	private List<Person> personList = new ArrayList<>(); //请求队列
    private int elevatorNowFloor;						//电梯楼层
    private boolean hasInput;							//输入线程是否结束标志

代码度量分析

类图

可以看出本次作业的架构设计较为简单,每个类的实现都不复杂,同时,输入线程只负责往电梯队列添加元素,电梯线程只负责取出元素并将其送往楼层,在第二次作业中只需重写调度方法即可,拥有较好的鲁棒性。

复杂度分析

类复杂度

方法复杂度

这里再对以上几个复杂度说明:

对于类,有OCavgWMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。

ev(G)基本复杂度是用来衡量程序非结构化程度的,非结构成分降低了程序的质量,增加了代码的维护难度,使程序难于理解。因此,基本复杂度高意味着非结构化程度高,难以模块化和维护。实际上,消除了一个错误有时会引起其他的错误。

Iv(G)模块设计复杂度是用来衡量模块判定结构,即模块和其他模块的调用关系。软件模块设计复杂度高意味模块耦合度高,这将导致模块难于隔离、维护和复用。模块设计复杂度是从模块流程图中移去那些不包含调用子模块的判定和循环结构后得出的圈复杂度,因此模块设计复杂度不能大于圈复杂度,通常是远小于圈复杂度。

v(G)是用来衡量一个模块判定结构的复杂程度,数量上表现为独立路径的条数,即合理的预防错误所需测试的最少路径条数,圈复杂度大说明程序代码可能质量低且难于测试和维护,经验表明,程序的可能错误和高的圈复杂度有着很大关系。

可以看到,本次作业的复杂度都被限制在一个合理范围内,只有电梯内的调度方法work,由于需要与电梯类内部请求队列交互所以圈复杂度较高。

UML线程合作分析

S.O.L.I.D代码分析

  • Single Responsibility Principle:在电梯类内部实现调度,在类/方法单一性上做的不够好。
  • Open Close Principle:类内部方法功能明确,无需修改,在后面两次作业中证明满足本原则。
  • Liscov Substitution Principle:无子类无继承,满足本原则。
  • Interface Segregation Principle:无接口,满足本原则。
  • Dependency Inversion Principle:未使用接口,没有以抽象为基础来进行架构的搭建,在本原则上需要改进。

BUG分析

本次作业在强测和互测中均未出现问题,但是采用的暴力轮询方法在第二次作业中证明是不可取的。

在多线程程序中,调试经常都是一个非常让人头疼的问题,由于线程并发,我们以往的断点将完全不起作用,甚至会影响已有线程的运行,起到反作用。同时由于线程安全问题(本次作业并不明显),很多运行结果都是不可复现的,这也很大程度的提高了debug的难度。在第二次作业的BUG分析中我将进行更深入的说明。

第六次作业

设计策略

第六次作业相比于第五次作业唯一的变化在于电梯调度的策略,指导书中推荐使用的策略是ALS,(但很显然ALS其实效率低下。。。而我为了稳妥起见还是用了ALS,所以性能分直接爆炸,所以后面我们不能盲目相信指导书更不要怕麻烦自己钻研思路

所以在本次作业中我只在第一次作业的基础上重写了work调度方法并增加了捎带队列。

private List<Person> personList = new ArrayList<>();
private int elevatorNowFloor;
private boolean hasInput;

private boolean hasMainPerson;
private Person mainPerson;
private List<Person> followPersonList = new ArrayList<>();

具体实现的策略与指导书流程完全一致。

其次,本次作业使用暴力轮询将不可取,在这里就可以使用wait() + notifyAll()或者while+sleep()节省CPU时间,这里使用的是后者

while ((hasInput) ||
    (personList.size() != 0 || followPersonList.size() != 0 ||
        hasMainPerson == true)) {
    try {
        Thread.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    work();
}

代码度量分析

类图

除了重写的方法和为了过checkstyle而新增的方法外,其余和第一次作业完全一致。

复杂度分析

类复杂度

方法复杂度

由于过checkstyle要将一个方法拆分,在本次作业中重写新增的几个方法相互依赖,耦合度比较高,其实在本次作业中就可以和大家做的一样单独设置一个调度器,这样就可以降低复杂度。其他方法的复杂度都控制在一个合理的范围内。

UML线程合作分析

架构和第一次作业一致。

S.O.L.I.D代码分析

  • Single Responsibility Principle:在电梯类内部实现调度,在类/方法单一性上做的不够好。
  • Open Close Principle:类内部方法功能明确,无需修改,第三次作业中仍然不需要直接修改原有代码。
  • Liscov Substitution Principle:无子类无继承,满足本原则。
  • Interface Segregation Principle:无接口,满足本原则。
  • Dependency Inversion Principle:未使用接口,没有以抽象为基础来进行架构的搭建,在本原则上需要改进。

BUG分析

本次作业在强测互测中均未出现BUG,但是在实际调试的过程中却真正开始体会到了线程安全调试的困难。会出现电梯提前下班、乘客被带到异次元空间、乘客多重影分身、乘客被困电梯事故等等BUG。

而我们手动在IDEA控制台输入完全起不到作用,我们应该完全模拟评测机的输入过程,加上大量测试才能找到自己的错误。

生成数据是非常简单的,只需要按照
$$
[time]ID-FROM-X-TO-Y
$$
生成随机数拼接即可。

重点是如何模拟评测机输入,这里我使用了Python来处理输入同时模拟cmd控制台。

import os
import time
import subprocess

f =  open("in.txt","r")
list1 = f.readlines()
timelist = []
instrlist = []

for i in list1:
    a=0
    b=0
    for j in range(len(i)):
        if i[j] == '[':
            a = j
        elif i[j] == ']':
            b = j

    c = i[a+1:b]
    timelist.append(c)
    d = i[b+1:len(i)]
    instrlist.append(d)
#########形成标准输入列表,接下来模拟控制台输入###############
with subprocess.Popen('java -jar HomeWork6.jar', stdin=subprocess.PIPE, universal_newlines=True) as p:

    for i in range(len(list1)) :
        if i == 0:
            time.sleep(float(timelist[i]))
        else:
            time.sleep(float(timelist[i])-float(timelist[i-1]))
        print(instrlist[i], file=p.stdin, flush=True)

核心代码就短短三十行,首先将输入的请求处理成时间以及标准输入命令。使用Python的subprocess来进行模拟cmd控制台输入,毕竟写bat体验太差。然后使用time库的sleep方法模拟间隔时间定时投放,最后使用subprocess管道输出,这样就模拟了真正输入的全过程。并且可以看到自己运行的结果。

检查数据正确与否的操作在研讨课上已经有同学分享,这里不予赘述。

第七次作业

设计策略

输入线程

在第七次作业中我没有对电梯线程做任何修改,做出的重大改变就是新增了一个一级总调度器,在总调度器设置一个总请求队列,同时创建运行三个电梯线程,而输入线程就不是往电梯请求队列里面添加东西了,而是往总调度器中添加请求。

总调度器

总调度器的的设计初衷在于确保线程安全,很多同学在本次作业中采用的是“电梯自动抢人”的做法,但是这样就会出现很多风险,如果不使用锁或者锁使用不当的话,乘客多重分身、乘客上不去电梯等问题又会出现。这里加一个总调度器的目的就是从总队列中往电梯分配人,通过不断与电梯交互数据来得出当前要将总队列首位乘客分配到哪个电梯中,这样做首先可以保证线程安全,其次不需要对上一次作业中的电梯做任何修改,即电梯只负责送人,降低了出错概率。

private List<Person> wholePersonList = new ArrayList<>();
//全部加入的人列表
private List<Person> changeElevatorList = new ArrayList<>();
//中途要换电梯的人的列表
private boolean hasInput;

private final int[] floorA = {-3, -2, -1, 1, 15, 16, 17, 18, 19, 20};
private final int[] floorB = {-2, -1, 1, 2, 4, 5, 6, 7, 8,
                              9, 10, 11, 12, 13, 14, 15};
private final int[] floorC = {1, 3, 5, 7, 9, 11, 13, 15};
private final int[] floorab = {-2, -1, 1, 15};
private final int[] floorac = {1, 15};
private final int[] floorbc = {1, 5, 7, 9, 11, 13, 15};

同时在本次作业中,判断楼层也交给调度器来判断,前面提到电梯只负责送人,所以对于特定楼层的判断也交给调度器完成。

乘客类

本次作业对乘客请求类做了扩充,在原有的PersonRequest上新增了换乘信息。

private PersonRequest personRequest;     //对电梯不可见
private int elevatorFromFloor;           //对电梯可见
private int elevatorToFloor;			 //对电梯可见
private boolean inElevator;
private char nextElevator;

电梯只可见本乘客在本趟电梯中的进入楼层以及目的楼层,需要换乘的话将由调度器加入换乘队列,并计算下一趟电梯。

代码度量分析

类图

电梯中除了为了过checkstyle而新增的方法外,其余和第二次作业基本一致,最重要的是增加了电梯人数上限的判断。

复杂度分析

类复杂度

由于电梯类和总调度类集成了大部分的功能,这两个类就显得比较臃肿,这个问题在以后需要改正。

方法复杂度

可以看到方法复杂度普遍不高,主要集中在调度线程的RUN函数以及调度函数中,由于调度算法较为集中且函数需要较多的调用所以耦合度高。

UML线程合作分析

S.O.L.I.D代码分析

  • Single Responsibility Principle:在电梯类内部实现调度,在总调度器中实现了较多算法,在类/方法单一性上做的不够好。
  • Open Close Principle:电梯直接复用第二次作业,方法职责明确,符合本原则。但调度器设计比较差,需要改进。
  • Liscov Substitution Principle:无子类无继承,满足本原则。
  • Interface Segregation Principle:无接口,满足本原则。
  • Dependency Inversion Principle:未使用接口,没有以抽象为基础来进行架构的搭建,在本原则上需要改进。

BUG分析

本次作业在强测和互测中均未出现BUG,主要还是自己采用了较为稳妥的思路,但是这也直接导致了性能分不高。

在互测中采用的策略仍然是生成数据对拍,但是在实际操作中发现:一股脑输入几十条极限数据,90%都能成功Hack到别人,其中重点查看的,其一是性能,有些同学的电梯调度着就成了傻瓜电梯,其二是线程操作与线程安全,这种错误通过阅读代码其实是比较难看出来的,所以在这里自动评测仍然是主要办法,由因溯果的难度比正向查找还是要大很多的。

最后,虽然自己在本次作业中没有受害,但还是希望以后大家手下留情。。。对这个房间的战况感到无比恐惧( ఠൠఠ )ノ

总结

本单元多线程作业首先是强化了面向对象的思维,对象的设计和功能的分配是做好线程安全的前提,只有明确的设计好了每个线程类该做什么,才能在几个线程并发运行时处理好线程协同,同时学习了几个经典的多线程协同模型,这些模型在以后的设计中仍然需要使用到。

在线程设计方面,我的设计思路其实是偏向于妥协的,我的设计中确保了不出现竞争,而是通过外力进行分配,同时三个电梯线程完全相互独立运行,协同合作关系仅仅在调度器中实现,通过与电梯实时交互人数、楼层数据来调配。

而在实际工程当中多线程一定会有相互竞争抢占资源的行为,而我则在三个电梯线程之外设置了一个总调度器,名字说是总调度器,实际上更像是一个管理者,站在一个上帝视角上给每个线程分发资源,这样确实避免了很多麻烦,但是现在想想这是一种取巧和妥协,为了确保正确性而做出的投机行为,后面有时间一定要回来最大限度的优化性能。

同时这三次作业对调试的技巧也在不断上升,在自我学习的过程中我也初步掌握了Python和控制台交互、定时投放数据的技巧,这也算是课程任务之外的收获。

posted @ 2019-04-24 16:39  NinjainPyjamas  阅读(252)  评论(0)    收藏  举报