BUAA OO Unit2总结

本文介绍了使用Java实现的电梯系统模拟,包括多线程的生产者消费者模型,电梯的运行逻辑,请求处理,以及调度器的设计。在不同作业阶段,逐步增加了电梯的动态扩展和维护功能。文章强调了线程安全和调度策略的重要性,并分享了在实现过程中遇到的挑战和解决方案,如wait-notifyAll机制和线程交互问题。
摘要由CSDN通过智能技术生成

BUAA OO Unit2总结

写在前面

声名远扬的电梯月终于落下了帷幕。在本单元的作业中,我们主要学习了Java多线程的相关知识。在数次的迭代作业中,我近似完成了基于课程组给出的标准ALS策略电梯系统。

相较于第一单元多项表达式没有任何一次作业通过公测的窘迫,这一单元我有了很大的进步。其一是对于Java语言的基础语法有了相当大的了解,例如ArrayList<>MashMap<>等容器和工具的使用,另一方面是花费了更长的时间学习Java相关的教程和教学视频。

当然,在这个月的学习过程中还是有一些遗憾的,我也并没有十分完整地达成课程组的所有要求。但对于我自己来说,已经有一种劫后余生的侥幸了。

为了确保正确性,我忽视了电量性能的优化,专注于缩短电梯的总运行时间。

第五次作业

作业要求

本次作业需要模拟一个多线程实时电梯系统。

系统基于一个类似北京航空航天大学新主楼的大楼,电梯可以在楼座内 1−11 层之间运行。

系统从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息通过输出接口进行输出。

具体而言,本次作业电梯系统具有的功能为:上下行,开关门,以及模拟乘客的进出。

电梯系统可以采用任意的调度策略,即在任意时刻,系统选择上下行动,是否在某层开关门,都可自定义,只要保证在电梯系统时间不超过系统时间上限的前提下将所有的乘客送至目的地即可。

电梯每上下运行一层、开关门的时间为固定值,仅在开关门窗口时间内允许乘客进出

电梯系统默认初始在11层有六部电梯。

架构分析

类的解析

在第五次作业中,我的架构共有六个类,分别是MainInputElevatorRequestQueueFloorQueueSchedule

Main类:

import com.oocourse.elevator1.TimableOutput;

public class Main {
    public static void main(String[] args) {
        try {
            TimableOutput.initStartTimestamp();
            RequestQueue queue = new RequestQueue();
            Thread producer = new Thread(new Input(queue), "input");
            Thread consumer = new Thread(new Schedule(queue), "schedule");
            producer.start();
            consumer.start();
        } catch (Exception e) {
            System.exit(0);
        }
    }
}

作为电梯系统程序的入口,需要整体筹划多线程的运行逻辑。在这里我选择按照生产者——消费者模型进行操作,将数据的输入和输出分为两个线程producerconsumer进行,因为输入和输出之间并没有特定的相互依赖关系,因此需要使用不同的线程实现并行。同时,二者应当对某一个共享对象进行操作,因此在Main类中创建一个共享数据——请求队列(或称为等候队列)。

RequestQueue类:

根据Main类的架构需求,其成员应该是一个存储请求的队列,因此我使用ArrayList集合来按顺序存储队列。

    private final ArrayList<PersonRequest> queue; 
	public RequestQueue() {
        this.queue = new ArrayList<>();
    }

之后便可以思考,这个等候队列可能需要进行什么操作来实现对一个电梯系统的构建。显然,我们需要进行添加请求add、取出队头请求getFirst、判断是否为空isEmpty等操作,构建起对应的方法即可。值得注意的是,由于多线程程序运行的不确定性,我们需要维护共享对象的相关数据,即加锁。最简单的加锁方式是对方法或对象进行synchronized声明。queue显然是一个需要维护的对象,因此对于它的增删需要加锁。例如下面的方法:

    public synchronized void add(PersonRequest pr) {
        queue.add(pr);
    }

而如果仅仅是读取共享对象则不需要加锁,因为读取操作不会导致多线程运行时发生逻辑错误。例如下面的方法:

	public PersonRequest first() { 
        if (queue.size() > 0) {
            return queue.get(0);
        } else {
            return null;
        }
    }

Input类:

该类是一个在Main类中需要的输入线程,而属性自然就是刚刚构建好的等候队列RequestQueue

	private final RequestQueue queue;
    public Input(RequestQueue queue) {
        this.queue = queue;
    }

作为一个生产者,需要解决的问题是对于输入何时停止的判定。在第五次作业中,一旦输入的请求为null(键盘输入Ctrl+D),则表明输入已经完成,可以将输入线程终止。

	if (request == null) {
		queue.setStop(true);
		queue.notifyAll();
		break;
	} else {
		queue.add(request);
		queue.notifyAll();
	}

利用上述的几个类就实现了对于请求的读取和记录,接下来要完成对请求的处理和实现。

Elevator类:

用来表示每个电梯。因为电梯的运行是独立的过程,因此我们需要将它开辟为一个独立的线程。六部电梯则需要六个独立线程进行操作。

在第五次作业中,电梯的多数属性都不可自定义,而是固定不变的,直接在构造方法中定义即可,参数传递也并不复杂。

    public Elevator(int id) {
        this.inEle = new FloorQueue();
        this.outEle = new FloorQueue();
        this.queue = new RequestQueue();
        this.id = id;
        this.mainReq = null;
        this.curNum = 0;
        this.curFloor = 1;
        this.direction = 0;
    }

可以看到真正需要自定义的只有电梯的ID。这里的inEle表示电梯内部正在进行的请求,outEle表示外部请求,queue用来存储分配给该电梯还未处理的总的等候队列,mainReq表示基准策略中提到的主请求。FloorQueue作为类将在下面介绍。

电梯需要完成的有开门,进人,关门,移动,出人这五个工作。而这五个工作显然需要在电梯进程中完成并且打印相关的信息。因此我们可以先梳理出一个大致的运行流程:选择主请求、(开门)、(出人)、(进人)、(关门)、移动,总体上是一个ALS可捎带的运行策略。这里需要注意的是,按照作业要求,电梯应该先出人再进人。

a电梯任何时候,内部的人数都必须小于等于轿厢容量限制

b、也就是说,即便在 OPEN、CLOSE 中间,也不允许出现超过容量限制的中间情况。

	public void run() {
        while (true) {
            synchronized (queue) {
                if (queue.isEmpty() && queue.getStop() && inEle.isEmpty()) {
                    break;
                }
            }
            selectMainReq();
            int flag = 0;
            if (inEle.containsKey(curFloor)) {
                open();
                flag = 1;
                scanInEle();
                if (outEle.containsKey(curFloor)) {
                    flag = scanOutEle(flag);
                }
            }
            if (outEle.containsKey(curFloor)) {
                flag = scanOutEle(flag);
            }
            if (flag == 1) {
                if (outEle.containsKey(curFloor)) {
                    scanOutEle(flag);
                }
                close();
            }
            if (direction != 0) {
                int re = move();
                if (re == 1) {
                    arrive();
                }
            }
        }
    }

剩下的就是写出相关的方法来实现具体的需求,这里不再过多赘述。

FloorQueue类:

用来按照楼层存储需要处理的请求,采用HashMap的结构处理,主要用于电梯运行过程中抵达楼层后判断是否进出乘客。

    private HashMap<Integer, ArrayList<PersonRequest>> pq;
	public FloorQueue() {
        this.pq = new HashMap<>();
    }

Schedule类:

调度器,从等候队列中取出请求并分配给某个电梯。在我的架构中,电梯并不直接与等候队列交互,因此需要利用调度器间接地将请求传入电梯中。而根据标程,只需要按顺序给电梯分配请求即可。下面是一段丑陋的代码,当然后两次作业中用ArrayList优化了一下

    public void selectEle(int flag) {
        if (flag == 1) {
            curEle = ele1;
        } else if (flag == 2) {
            curEle = ele2;
        } else if (flag == 3) {
            curEle = ele3;
        } else if (flag == 4) {
            curEle = ele4;
        } else if (flag == 5) {
            curEle = ele5;
        } else if (flag == 6) {
            curEle = ele6;
        }
    }

需要注意的是,在调度器线程中,如果等候队列为空,不应该直接停止这个线程,而是等待可能还会到来的新的请求,需要进行特殊的判断和处理。

	if (queue.isEmpty() && queue.getStop()) {
		break;
	}
	while (queue.isEmpty()) {
		try {
			queue.wait();
			if (queue.getStop()) {
				break;
			}
		} catch (Exception e) {
			e.printStackTrace();
        }
	}

至此,我们完成了一个基于ALS运行策略的电梯系统。

代码行数

在这里插入图片描述
总体而言,代码量集中在Elevator类中。为了使逻辑更加清晰,我将很多的电梯运行方法都写在了Elevator类中,导致该类略显冗杂。

复杂度

可以预见的,调度器和电梯的复杂度都是比较高的。

互测与强测

本次作业互测中我未能找到别人的bug,组内只有一位同学被成功hack1次。

本次作业强测得分95.0668,可见课程组给出的基准策略效率相当不错。

BUG修复

本次作业没有被查出的bug。

第六次作业

作业要求

本次作业需要模拟一个多线程实时电梯系统,并实现对电梯系统的动态扩展和日常维护。

系统基于一个类似北京航空航天大学新主楼的大楼,电梯可以在楼座内 1−11 层之间运行。

系统从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息通过输出接口进行输出。

具体而言,本次作业电梯系统具有的功能为:上下行,开关门,模拟乘客的进出,以及模拟电梯系统扩建和日常维护时乘客的调度

电梯系统可以采用任意的调度策略,即在任意时刻,系统选择上下行动,是否在某层开关门,都可自定义,只要保证在电梯系统时间不超过系统时间上限的前提下将所有的乘客送至目的地即可。

电梯每上下运行一层、开关门的时间为固定值,仅在开关门窗口时间内允许乘客进出

电梯系统默认初始在 11层有六部电梯。

架构分析

类的解析

第六次作业中,没有新增类,在原有的代码基础上进行新增方法即可。

addEle方法:

addEle方法定义在Schedule类中,按照输入接口接收到的请求进行新增电梯。毫无疑问,新增的每个电梯都是一个线程,因此也需要利用Thread类开辟线程。

    public void addEle(int id, int startFloor, int maxNum, double speed) {
        Elevator newEle = new Elevator(id, maxNum, startFloor, speed, queue);
        elevators.add(newEle);
        Thread newThread = new Thread(newEle, "Ele-" + id);
        newThread.start();
    }

新增电梯指令比较简单,也没有什么嵌套方法。

maintainOp方法:

maintainOp方法定义在Elevator类中。作为一个处理维护指令的方法,由调度器分配维护指令后,在电梯内部自行解决,而不阻塞其他线程的运行(按理来说应该是这样)

    public void maintainOp() {
        while (!queue.isEmpty()) { //等候队列不为空的时候,全部加入总队列中
            totalQueue.add(queue.getFirst());
        }
        if (!inEle.isEmpty()) { //电梯内不为空,将电梯内成员送出电梯并将请求加入总队列中
            open();
            for (int i = minFloor; i <= maxFloor; i++) {
                if (inEle.containsKey(i)) {
                    for (int j = 0; j < inEle.get(i).size(); j++) {
                        TimableOutput.println("OUT-"
                            + inEle.get(i).get(j).getPersonId()
                            + "-" + curFloor + "-" + id);
                        if (inEle.get(i).get(j).getToFloor() != curFloor) {
                            PersonRequest tmp = new PersonRequest(curFloor,
                                    inEle.get(i).get(j).getToFloor(),
                                    inEle.get(i).get(j).getPersonId());
                            totalQueue.add(tmp);
                        }
                    }
                    inEle.remove(i);
                }
            }
            sleep(400);
            close();
        }
        TimableOutput.println("MAINTAIN_ABLE-" + id);
    }

维护的逻辑并不复杂,需要注意的就是将电梯内的人全部放在最近的一层,并且把还未来得及处理的请求电梯内请求重写后的新请求加入总的等候队列中。更何况课程组非常仁慈地给了两次arrive的缓冲空间,可以说线程的运行逻辑已经简化到极致了。

代码行数

在这里插入图片描述

相比于第五次作业,主要增加了Elevator类进程的维护请求和Schedule类增加电梯的请求,其他类并没有发生改变。

复杂度

在这里插入图片描述

这次作业在第五次作业的基础上迭代,相似度很高,总体的复杂度没有很大的变化,依旧集中于Schedule类和Elevator类。

互测与强测

本次作业我未能进入互测,主要原因就在于未能成功解决线程安全的问题。在编写出能够实现课程组要求的代码后,程序总会出现轮询的情况。为了能够按时通过公测,我不得已采用了非常投机的sleep方法,每接收到一个MAINTAIN指令后,调度器线程就休息一段时间,即既不分配任何请求,也不判断线程终止条件。这样的做法确实一定程度上达到了和线程交互类似的效果,但其局限性和劣势也相当明显。首先,这种调度方法会导致整个电梯系统的效率大大降低,当维护请求的密度相当大的时候,系统的运行效率惨不忍睹;其次,如果为了维持维护逻辑的正确性而延长sleep()时长,会导致电梯运行多于两层而无法及时停止并维护的问题。

本次作业强测得分23.4644,主要原因是sleep()的时长需要与测评样例的时长限制的不均衡。如果sleep()时间过短,会因为尾部某些需要重新分配的请求还没来得及加入等候队列时进程便停止了导致的WA;而如果sleep()时间过长,那么大概率是会出现RTLE的。

BUG修复

回顾代码后,我意识到自己犯了一个很低级的错误:在Schedule线程和Elevator线程交互时,会出现二者均处于wait()状态而无人唤醒的情况。对于这个问题,如果在调度器线程中加入对电梯的notifyAll()方法,便可以很轻松地解决这个问题了。

好亏啊就差一点点就可以了

第七次作业

作业要求

本次作业需要模拟一个多线程实时电梯系统,该电梯系统支持动态扩展和日常维护,同时需要支持更加高级的调度功能。

系统基于一个类似北京航空航天大学新主楼的大楼,电梯可以在楼座内 1−111−11 层之间运行。

系统从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息通过输出接口进行输出。

具体而言,本次作业电梯系统具有的功能为:上下行,开关门,模拟乘客的进出,以及模拟电梯系统扩建和日常维护时乘客的调度

电梯系统可以采用任意的调度策略,即在任意时刻,系统选择上下行动,是否在某层开关门,都可自定义,只要保证在电梯系统时间不超过系统时间上限的前提下将所有的乘客送至目的地即可。

电梯每上下运行一层、开关门的时间为固定值,仅在开关门窗口时间内允许乘客进出

电梯系统默认初始在 11层有六部电梯,这些电梯均可达所有楼层。

架构分析

本次作业中增加了可达性的概念,因此对于特定情况下的乘客请求,必须要进行电梯的换乘才能完成。此外,还对楼层停靠电梯数进行了限制。这两个新需求意味着Schedule调度类需要进行一定规模上的重构。

课程组给出了可达的表示方法: a c c e s s & ( 1 < < ( f l o o r − 1 ) ) { ≠ 0 可达 = 0 不可达 access\&(1<<(floor-1))\begin{cases}\ne0\quad可达\\=0\quad不可达 \end{cases} access&(1<<(floor1)){=0可达=0不可达

那么可以利用一个ArrayList存储电梯的全部可达楼层。accessFloor作为电梯的一个属性,在Schedule中初始化或添加电梯时进行设置。

	Private ArrayList<Integer> accessFloor;
	for(i = 1; i <= 11; i++) {
 		if (access & (1 << (i - 1)) == 1) {
  	    	accessFloor.add(i);
  		}
	}

关于服务中只接人

这部分内容也不难实现。新建一个Service类,其中创建两个HashMap<Integer, ArrayList<Integer>>的哈希表,用来表达某层楼的服务中只接人状态中的电梯号,计数时使用Service.get(floor).size()方法即可。

	private HashMap<Integer, ArrayList<Integer>> service;
	private HashMap<Integer, ArrayList<Integer>> receive;
	
	public Service() {
		service = new HashMap<>();
        receive = new HashMap<>();
        for(int i = minFloor; i <= maxFloor; i++) {
            service.put(i, new ArrayList<Integer>);
            receive.put(i, new ArrayList<Integer>);
        }
	}

难点实在是体现在换乘上。鄙人不才,第一眼看到要求后,我想到了用DFS来递归寻找路径,无奈数据结构的知识掌握的不是很好(忘完了),于是在换乘调度上使用了十分愚蠢的能送多远送多远,即保证每次运送乘客都是使其朝目标楼层更近的方向移动。因为我这种实现的代码没有什么寄术含量,所以我只简单介绍一下思路。

/*很原始的换成策略
get FromFloor and ToFloor of request
visit accessFloor of current Elevator
if not contains FromFloor,change next Elevator until contains FromFloor
if contains FromFloor, but not ToFloor,detect the nearest floor it accessable and send
if contains both, send directly
*/

但这样有一个十分严重的问题:在运送过程中可能会出现没有电梯能把乘客从当前所在楼层运到目标楼层的情况。

例如:现有4部电梯,1号可达楼层为1,2,3;2号为3,5,10;3号为6,9,10,4号为7,8,11

现在对于请求1-FROM-1-TO-11,这样的换乘方式会做出如下处理:

step1: OUT-1-3-1 	//由1号电梯送至3楼
step2: OUT-1-10-2	//由2号电梯送至10楼
step3: ......		//乘客被永久地放在了10楼

这是一个十分尴尬的情况,自己为数不多能写的策略竟然有这样的漏洞😅。研究了很久之后发现自己还是搞不清楚换乘策略到底应该怎么写,在蓝桥杯和冯如杯两座大山的压迫之下,我屈服了,并且贯彻了自暴自弃的做法:忽略一切的ADD请求和id大于7的MAINTAIN指令,确保电梯系统中的所有电梯都是全达电梯(写到这自己都没绷住),然后就没有然后了。

铁打的CTLE

UML图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gxazgAb2-1681346853358)(C:\Users\张炎坤\AppData\Roaming\Typora\typora-user-images\image-20230405164350577.png)]

代码行数

在这里插入图片描述

复杂度

在这里插入图片描述

代码行数、类复杂度和方法复杂度承袭前两次作业的情况,没有很大的变化。

互测与强测

不出意外,没有进入互测,强测也不尽人意,这是换乘策略带来的问题。

稳定类与易变类

在电梯系统中,显然有些类是表示属性的,在三次作业中基本不变;而有些类需要负责调度或运行,因此i需要经常改变其代码逻辑。

稳定类:

FloorQueue表示按楼层存储需要处理的请求,从始至终不变

RequestQueue表示总的等候队列,从始至终不变

Main表示程序的主入口和对线程的调度,从始至终不变

Input表示接收从官方接口输入的请求,从始至终不变

易变类:

Elevator表示电梯的属性,在不同的要求下需要变化运行方法和新增其他成员方法

Schedule表示调度器策略,需要不断变化以适应新增需求

心得体会

这三次作业我都使用了wait-notifyAll解决问题。这样的设计策略使得CPU运行时间比较少,但必须要控制好各个锁之间的控制关系,否则一不小心可能就会出现死锁和轮询。

经过第二单元的学习和三次作业的训练,我对于多线程的运行和线程安全交互有了比较深的认识。最大的收获在于使用Runnable创建线程类和使用sychronized方法和RetrantLock方法对数据加锁处理。而在三次作业中均利用ALS捎带策略和平均的分配策略是一种求稳的方式,但我仍旧有一些没有付诸实际的更好的优化方法,以后可以应用试验一下。通过这个单元的训练,我对多线程编程有了一定的了解,掌握了线程间通信、同步、互斥的方法,保证线程的安全性。更重要的一方面是,设计架构的能力有了不小的提升,明白如何才能设计出高内聚、低耦合的程序,为以后的学习打下了良好的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值