面向对象程序设计第二单元总结
第一次
摘要:本次作业的基本目标是模拟单部多线程电梯的运行。
调度方式
-
采取生产者消费者模式:
生产者:输入线程(InputThread)
消费者:电梯线程(Elevator)
托盘:调度器(Dispatcher)
-
双线程模式:
线程1:输入线程(InputThread)
线程2:电梯线程(Elevator)
调度算法
-
Night模式:
先用2s的时间上到4楼,然后找到最高有人的楼层,到达该楼层,下到1楼,在此期间,只要有人需要上电梯则携带,直到电梯满。
只可能有两种想法:从低到高运输,或从高到低运输。这两种方法在人数为6的整数倍时没有区别。
假设有7个人,分别在2-8楼,要回到一楼。
第一种方法:电梯的运转模式是:1-7-1-8-1。
第二种方法:电梯的运转模式是:1-8-1-2-1。
假设两种方法开关门时间完全相同,则时间差为\(5\times2\times0.4s = 4s\)。
如果人数更多,则时间差更多。因此第二种方法完胜第一种方法。
-
Morning模式:
电梯在一楼等待,直到电梯乘满或输入结束,开始运行电梯,直到电梯为空。
若输入已经结束且电梯为空,则结束线程。否则,回到一楼,由近即远携带乘客。
Morning模式和Night模式的一个区别就是:当送完最后一拨人之后,不需要再回到1楼。
假设7个人分别到2-8楼。
第一种方式,先送低的,再送高的。1-7-1-8。共19层
第二种方式,先送高的,再送低的。1-8-1-2。共15层
看起来似乎第二种方式更好。
但再假设有12个人,6个人要到5楼,6个人要到20楼。
第一种方式:1-5-1-20。共27层
第二种方式:1-20-1-5。共42层。
-
Random模式:
判断电梯有无人,如果没有人,则将请求队列中fromFloor离电梯所在楼层最近的人的fromFloor设为target。如果有人,则将电梯中toFloor与当前电梯位置最近的人的toFloor设为target。
电梯无人情况:找一个离电梯当前位置的人的位置作为目标位置。
电梯有人情况:送最近的一个人到达目的地,在此过程中,如果遇到有人要上电梯且运行方向和当前电梯运行方向一致时,将其捎带。
流程图
代码结构
第一次作业要求实现单部多线程可捎带电梯,代码结构比较简单,UML类图如下:
第一次作业的要求比较简单,只有一部电梯,因此在本次作业中只有5个类,其中的Person类主要是为了后续的扩展而设计的,因此主要有4个类。其中Main为主类,InputThread为输入线程类,Elevator为电梯线程类,Dispatcher为调度器类,负责从输入线程中获取指令,并且将指令发放给电梯。
同步块的设置和锁的选择
Dispatcher的requests需求队列作为输入线程和电梯线程的共享变量,需要保证线程安全。具体设置同步块和锁的方式如下:
Dispatcher:
public void add(Person person) {
synchronized (requests) {
this.requests.add(person);
requests.notifyAll();
}
}
}
//在random模式下判断是否需要开门
public boolean open(int floor, boolean up) {
synchronized (requests) {
for (Person person : requests) {
if (person.getPersonRequest().getFromFloor() == floor && person.getUp() == up) {
return true;
}
}
requests.notifyAll();
}
return false;
}
//openNight:在night模式下判断是否需要开门
public boolean openNight(int floor) {
synchronized (requests) {
for (Person person : requests) {
if (person.getPersonRequest().getFromFloor() == floor) {
return true;
}
}
requests.notifyAll();
}
return false;
}
Elevator
synchronized (dispatcher.getRequests()) {
if (this.dispatcher.getRequests().isEmpty() && !this.dispatcher.isEndOfInput()) {
try {
this.dispatcher.getRequests().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//让人们进入电梯的方法
public synchronized void getIn() {
if (num == capacity) {
return;
}
synchronized (dispatcher.getRequests()) {
Iterator<Person> iterator = dispatcher.getRequests().iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
PersonRequest personRequest = person.getPersonRequest();
if (personRequest.getFromFloor() == floor) {
TimableOutput.println("IN-" + personRequest.getPersonId() + "-" + floor);
this.persons.add(person);
num++;
iterator.remove();
if (num == capacity) {
return;
}
}
}
}
}
代码复杂度
- 类复杂度
- 方法复杂度
从复杂度分析中可以看到,Elevator类中的runMorning,runNight和runRandom方法的复杂度较高,主要原因是这三个方法作为run方法的附属方法,与它们相关联的方法和变量较多。
Bug分析
- 自己
本次作业在强测、互测中没有出现Bug。
-
他人
本次作业在互测中没有查出其他同学的Bug。
第二次
摘要:本次作业要求模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。
调度方式
-
继续采取生产者消费者模式,但是本次作业开始为多消费者模式:
生产者:输入线程(InputThread)
消费者:电梯线程(Elevator)
托盘:控制器(Controller)
-
多线程模式:
输入线程(InputThread)
每个电梯为一个线程
调度算法
-
三种到达模式的单个电梯的算法与第一次作业相同。
-
不同电梯共享一个需求队列,由Controller将需求下发给每一个电梯的调度器,每个电梯的调度器分别从需求队列里选择自己的需求。
流程图
代码结构
第二次作业要求实现三部多线程可捎带电梯,UML类图如下:
第二次作业的要求比第一次稍微复杂一些,但仍然比较简单,只有三部电梯,且电梯的型号完全相同,因此相比于上一次作业,多了一个Controller类作为总控制器,还多了一个Output类保证输出线程安全。每个电梯拥有自己的Dispatcher调度器。其中Main为主类,InputThread为输入线程类,Elevator为电梯线程类,Dispatcher为调度器类,负责从输入线程中获取指令,并且将指令发放给电梯。此时Person类多出了一个私有变量:taken。当taken为true时,说明这个人已经在电梯上了,就不可以再上其他电梯了。注意此时需要保护Person类的线程安全,因为Person是所有电梯的共享对象。
同步块的设置和锁的选择
与第五次作业相同,虽然有多部电梯,但是每个电梯的调度器和输入线程之间的共享对象为电梯自己的调度器的需求队列,因此和上一次作业几乎相同,需要保证电梯调度器的需求队列的线程安全。具体的锁如下:
Dispatcher:
//在random模式下判断是否需要开门
public boolean openRandomNotEmpty(int floor, boolean up) {
boolean need = false;
//System.out.println("dispatcher openRandom");
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (person.getPersonRequest().getFromFloor() == floor
&& person.isUp() == up && !person.isTaken()) {
//System.out.println("random need in");
need = true;
}
if (person.isTaken()) {
iterator.remove();
}
person.notifyAll();
}
}
}
return need;
}
public boolean openRandomEmpty(int floor) {
//System.out.println("dispatcher openRandom");
boolean need = false;
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
//System.out.println(person.getPersonRequest().
//getPersonId() + " " + person.isTaken());
if (person.getPersonRequest().getFromFloor() == floor
&& !person.isTaken()) {
//System.out.println("random need in");
need = true;
}
if (person.isTaken()) {
iterator.remove();
}
person.notifyAll();
}
}
requests.notifyAll();
}
return need;
}
public boolean openMorning() {
boolean need = false;
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (!person.isTaken()) {
need = true;
} else {
iterator.remove();
}
person.notifyAll();
}
}
requests.notifyAll();
}
return need;
}
//openNight:在night模式下判断是否需要开门
public boolean openNight(int floor) {
boolean need = false;
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (person.getPersonRequest().getFromFloor() == floor && !person.isTaken()) {
need = true;
}
if (person.isTaken()) {
iterator.remove();
}
person.notifyAll();
}
}
requests.notifyAll();
}
return need;
}
为了避免轮询,采用wait-notifyAll模式进行编程,具体代码如下:
if (this.persons.isEmpty() && !dispatcher.isEndOfInput()) {
synchronized (dispatcher.getRequests()) {
try {
if (dispatcher.getRequests().isEmpty()) {
dispatcher.getRequests().wait();
}
} catch (Exception e) {
;
}
for (Person person : dispatcher.getRequests()) {
synchronized (person) {
int b = Math.abs(person.getPersonRequest().getFromFloor() - this.floor);
if (b < a) {
a = b;
target = person.getPersonRequest().getFromFloor();
}
person.notifyAll();
}
}
}
代码复杂度
- 类复杂度
- 方法复杂度
从复杂度分析中可以看到,依然是Elevator类中的runMorning,runNight和runRandom方法的复杂度较高,原因同上一次作业相同。
Bug分析
- 自己
本次作业在强测、互测中没有出现Bug。
-
他人
在强测中测出两位同学的Bug,一位同学是由于线程不安全导致,另一位同学是由于线程过早结束,有一部分人还没有到达toFloor线程就全部结束了导致。(本次作业深切体会到了多线程的随机性,同一组数据有的时候就hack中,有时候就hack不中,还有的数据在本地测试对方输出是有问题的,交到测评机上怎么都不中)。
第三次
摘要:本次作业要求模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,以及最重要的——可停靠楼层的不同。
调度方式
-
继续采取生产者消费者模式,但是本次作业开始为多消费者模式:
生产者:输入线程(InputThread)
消费者:电梯线程(Elevator)
托盘:控制器(Controller)
-
多线程模式:
输入线程(InputThread)
每个电梯为一个线程
调度算法
-
三种到达模式的单个电梯的算法与第一次作业相同。
-
同一型号的电梯共享一个需求队列,由Controller将需求下发给对应型号电梯的调度器(具体分配方式取决于Person的CurrentFromFloor和CurrentToFloor),每个电梯的调度器分别从需求队列里选择自己的需求。
-
换乘模式:
根据多次实验,最后采取使用Person类直接计算出中间电梯层的方式,有两种情况:一是不需要换乘的乘客,即currentTofloor与tofloor相同的乘客,另一种是需要换乘的乘客,即currentTofloor与toFloor不相等的乘客。当到达currentTofloor时,需要进行判读那并重新赋值,核心代码如下:
if(currentTofloor == toFloor) { taken = false; finished = true; } else { currentFromFloor = currentTofloor; currentTofloor = toFloor; taken = false; }
流程图
代码结构
第三次作业要求实现多部不同型号多线程可捎带电梯,UML类图如下:
第三次作业的要求相比第二次作业,增加为出现不同型号的电梯,它们的移动速度、可到达楼层、容量是不同的,因此存在换乘的情况。在本次作业中有7个类,其中Main为主类,InputThread为输入线程类,Elevator为电梯线程类,Dispatcher为调度器类,负责从输入线程中获取指令,并且将指令发放给电梯,Person类多了一些私有变量,用来计算换乘楼层,Output类为输出线程安全类,总体上与第二次的框架相同。
同步块的设置和锁的选择
第七次作业同第六次作业相比,不仅需要保证Dispatcher的requests线程安全,Person也成了几个线程的共享对象,Person的一些变量也会被不同的电梯线程或调度器改变,因此需要保证Person的线程安全。具体代码如下。
public void getOut() {
if (this.persons.isEmpty()) {
//TimableOutput.println("this.persons is empty");
return;
} else {
Iterator<Person> iterator = persons.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
if (person.getCurrentToFloor() == floor) {
output.print("OUT-" + person.getId() + "-" + floor + "-" + id);
if (person.getCurrentToFloor() == person.getToFloor()) {
person.setTaken(false);
person.setFinished(true);
iterator.remove();
} else {
person.setCurrentFromFloor(person.getCurrentToFloor());
person.setCurrentToFloor(person.getToFloor());
person.setFinished(false);
person.setTaken(false);
controller.add(person, arrivePattern);
iterator.remove();
}
num--;
}
}
}
}
public synchronized void getIn() {
if (num == capacity) {
return;
}
synchronized (dispatcher.getRequests()) {
Iterator<Person> iterator = dispatcher.getRequests().iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (person.getCurrentFromFloor() == floor && !person.isTaken()
&& !person.isFinished()) {
output.print("IN-" + person.getId() + "-" + floor + "-" + id);
person.setTaken(true);
this.persons.add(person);
num++;
iterator.remove();
if (num == capacity) {
dispatcher.getRequests().notifyAll();
return;
}
} else if (person.isTaken() || person.isFinished()) {
iterator.remove();
}
person.notifyAll();
}
}
dispatcher.getRequests().notifyAll();
}
}
代码复杂度
- 类复杂度
- 方法复杂度
从复杂度分析中可以看到,因为本次作业放弃了runMorning模式的调度,将其与runRandom模式下的调度合并,因此只有runRandom和runNight的复杂度较高,原因同前两次作业。
Bug分析
- 自己
本次作业在互测中被查出一个Bug,是由于Morning模式下CTLE导致的。查看代码后,发现是由于错误的进行了notifyAll语句导致的CTLE。
-
他人
在强测中测出一位同学的Bug,是由于线程不安全导致的,这位同学可能会让同一个人上好几个电梯,或者生出更多电梯,但是很难hack中,在一晚上的努力下终于中了一刀(sad)。
程序可扩展性
首先,本程序能够通过改变Elevator类的内容以及实例的数量扩展到不同数量、不同类型的电梯。第二,在面对不同模式和模式转变的时候,只需要输入类给控制器类传递信息就可以改变调度模式。在程序编写过程中,考虑到了高内聚低耦合的设计策略,每一个类各司其职,因此当程序的需求发生变化时,不会产生“牵一发而动全身”的情况,具有良好的可扩展性。
Hack策略
-
分析自己发现别人程序bug所采用的策略
本次发现别人程序的bug采取了阅读代码+测评机的模式,首先使用测评机进行评测,当出现问题(如CTLE,输出错误)等情况,再去阅读对方的代码,找到问题所在,以便构造针对性的数据进行hack。 -
列出自己所采取的测试策略及有效性
因为测评机的测试点很多,所以能够有效发现对方程序的漏洞,但是在上传到课程平台时经常会出现两种情况,一是hack不中,二是hack中了,但是中到了另一位同学身上。这是由于多线程的输出不稳定导致的。
-
分析自己采用了什么策略来发现线程安全相关的问题
阅读代码,找到共享变量,再检查在使用共享变量的时候是否采取了synchronized块进行加锁保护。
-
分析本单元的测试策略与第一单元测试策略的差异之处
第一单元为单线程,只需要检查输出是否正确,采取python的库就可以进行正确性判断,而第二次作业为多线程作业,只有一次输出是正确的时候是无法确定他的程序没有问题的,因此需要对每一个测试点进行重复测试的方式去验证这个测试点对方是否能够通过。
心得体会
难点__线程安全
个人认为本单元作业最难处理的地方有如下三点:
- 保证线程安全,合理使用synchronized块。
- 避免轮询,使用wait和notifyAll。notifyAll如果在不恰切的地方使用也会导致CTLE(第三次作业有感)。
- 如何在正确的时刻结束线程,三次作业结束线程的判断均不一样,如果提前结束线程或由于死锁无法结束线程是很可惜且严重的失误。
- 第一次作业:在输入结束且电梯送完需求队列里所有人时结束线程。
- 第二次作业:在输入结束且电梯送完自己电梯中所有人时结束自己的线程。
- 第三次作业:在输入结束且所有人的状态都是finished的时候结束线程。从后续来看,第三次作业结束线程的判断标志可以沿用到第一次作业和第二次作业。
层次化设计
本人的代码中除了对Thread类的继承,没有其他继承关系,因此代码的结构比较简单,三次作业均选取了生产者-消费者模式,从双线程到多线程的转变。对于生产者-消费者模式的应用,三次作业经历了从单消费者到多消费者的过渡,让我更加深刻的了解了多线程的一种常用模型:生产者-消费者模型,有助于后续对于多线程项目的开发。
互测
homework7的互测是我第一次被hack,究其原因是前期的测试做的不够充分,并且由于homework7与homework6相比代码的更改量比较小,因此没有想到自己可能会出现bug,因此,在后续的作业中一定要在前期进行充分的测试,尽量避免这种事情再次发生(虽然发生了也没辙)。