OO:第二单元总结博客
(1)同步块的设置和锁的选择
- 三次作业的同步块的设置和锁的选择变化不大,以下仅以第三次作业的部分进行说明。
- 共享对象仅有waitQueue与serviceQueue两个队列,分别为输入的队列与电梯中的待处理请求队列。waitQueue对象由InputThread、Elevator与Dispatcher共享;serviceQueue对象由Elevator与Dispatcher共享
- 同步块与锁的选择上遵循线程安全的原则,凡需要对共享对象进行写入修改的地方均需加锁,例如对共享对象waitQueue进行读取与删除、对serviceQueue对象的取出等操作。锁与同步块中处理语句之间的关系为顺序与包含关系,需要先加锁才能够执行同步块中的处理语句,以保证执行语句过程中共享对象的状态不会发生改变。
- waitQueue的三处共享锁如下:
- InputThread类中:

-
- Elevator类中:

-
- Dispatcher类中:

- serviceQueue的两处共享锁如下:
- Elevator类中:

- Dispatcher类中:

(2)调度器设计
第一次作业
- 第一次作业仅调度一部电梯,因此调度器设计非常简单,仅需从waitQueue中取出指令并放入serviceQueue中。此外需添加判断线程的结束及等待逻辑条件即可。
- 调度器与程序中的线程进行交互过程中,一方面调度器本身就是一个进程,具有run函数,与其他线程间依赖线程的wait与notify函数进行协作;另一方面通过对共享对象的操作实现不同进程间的数据与信息的传递,例如存储了待处理请求队列的serviceQueue,调度器将请求存入这一对象,电梯则从中读取请求。调度器的具体run函数部分如下:
public void run() { ArrayList<PersonRequest> temp = new ArrayList<>(); while (true) { synchronized (waitQueue) { if (waitQueue.isEnd() && waitQueue.noWaiting()) { //todo System.out.println("Dispatch over"); synchronized (serviceQuene) { serviceQuene.notifyAll(); } return; } if (waitQueue.noWaiting()) { try { waitQueue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { temp.addAll(waitQueue.getRequests()); for (int i = 0; i < temp.size(); i++) { PersonRequest request = temp.get(i); synchronized (serviceQuene) { serviceQuene.changeUpdateState(true); serviceQuene.addRequest(request); serviceQuene.notifyAll(); } //todo System.out.println("Wait->Service:" + request.getPersonId() + // "-FROM-" +request.getFromFloor() + "-TO-" + request.getToFloor()); temp.remove(request); i--; } waitQueue.clearQueue(); } } } }
第二次作业
- 第二次作业加入了多部电梯,因此调度器的设计上需要略微调整,增加具体分配到哪部电梯的处理队列的函数allocRequest,其余部分沿用第一次作业的设计,整体上变化不大。
- 此外,为了针对电梯的添加导致的后加入电梯队列中无待处理请求,例如连续50条乘客请求,最后两条才是增加电梯请求的极端情况,还加入了realloc重分配函数,将不同电梯的请求进行重新平均分配,以达到较好的性能。调度器的具体run函数部分如下:
public void run() { ArrayList<PersonRequest> temp = new ArrayList<>(); while (true) { synchronized (waitQueue) { if (waitQueue.isEnd() && waitQueue.noWaiting()) { //todo System.out.println("Dispatch over"); for (ServiceQuene serviceQuene : serviceQuenes) { synchronized (serviceQuene) { serviceQuene.notifyAll(); } } return; } if (waitQueue.noWaiting()) { try { waitQueue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { temp.addAll(waitQueue.getRequests()); waitQueue.clearQueue(); } } if (realloc) { synchronized (serviceQuenes) { reallocRequest(); realloc = false; } } for (int i = 0; i < temp.size(); i++) { PersonRequest request = temp.get(i); synchronized (serviceQuenes) { ServiceQuene serviceQuene = allocRequest(); synchronized (serviceQuene) { serviceQuene.addRequest(request); serviceQuene.notifyAll(); } } //todo System.out.println("Wait->Service:" + request.getPersonId() + // "-FROM-" +request.getFromFloor() + "-TO-" + request.getToFloor()); temp.remove(request); i--; } } }
第三次作业
- 第三次作业的电梯增加了种类的区分,而且有运行楼层的限制,但本人未采用换乘策略,因此请求分配部分未作修改。
- 但在分配请求部分做出了许多调整,定量地采用权重的计算思路进行请求分配,该部分代码如下:
private ServiceQuene allocRequest(PersonRequest request) { ArrayList<String> arriveTypes = checkRequestType(request); //int[] multFactor = {8, 9, 12}; double[] multWeight = {9, 8, 6}; //公因数72,保持8:9:12配比 double[] notFullWeight = {1, 0.2, 0.1}; //人未满时优先分配权重 double[] plainWeight = {18, 9, 0}; //常数偏移权重 double[] distanceWeight = {0.6, 0.4, 0.2}; //距离偏移权重 ServiceQuene minQuene = null; double min = 0; double multweight; double nfweight; double bweight; double dweight; double randweight; int sizeFac; int floorFac; for (int i = 0; i < elevNum; i++) { ServiceQuene thisSQ = serviceQuenes.get(i); if (i == 0) { multweight = multWeight[0]; nfweight = 1; bweight = plainWeight[0]; dweight = distanceWeight[0]; randweight = (Math.random() + 4.5) / 10; sizeFac = serviceQuenes.get(0).getRequests().size(); floorFac = Math.abs(request.getFromFloor() - BASIC_FLOOR) + Math.abs(request.getFromFloor() - request.getToFloor()); min = (sizeFac * multweight * nfweight + floorFac * dweight + bweight) * randweight; minQuene = serviceQuenes.get(0); continue; } if (arriveTypes.contains(thisSQ.getType())) { int tp = thisSQ.getTypeOfInt(); multweight = multWeight[tp]; nfweight = (thisSQ.isFullCarry()) ? 1 : notFullWeight[tp]; bweight = plainWeight[tp]; dweight = distanceWeight[tp]; randweight = (Math.random() + 4.5) / 10; sizeFac = thisSQ.getRequests().size(); floorFac = Math.abs(request.getFromFloor() - BASIC_FLOOR) + Math.abs(request.getFromFloor() - request.getToFloor()); double thisweight = (sizeFac * multweight * nfweight + floorFac * dweight + bweight) * randweight; //todo System.out.println("m:" + multweight + "; // nf:" + nfweight + "; b:" + bweight); //todo System.out.println("Weight of Elev" // + thisSQ.getId() + ":" + thisWeight); if (thisweight < min) { min = thisweight; minQuene = thisSQ; } } } //todo System.out.println(request.toString() + // "-> Alloc to Elev id:" + minQuene.getId() + "; type:" + minQuene.getType()); return minQuene; }
(3)总结分析三次作业中架构设计
第一次作业
- 三次作业的架构基本相同,均为如下几个类,但为了实现新增功能,类中的属性、方法等略有区别。从三次作业的类图可以看出,三次架构变化不大,没有像第一单元一样进行大幅的修改,简单的增补修订即可实现新增需求。
-
第一次主要的工作量在编写电梯运行策略、规划线程、学习jar包导入与引用等,很多线程相关的知识都不太清楚,还需要了解、借鉴,参考了第三次实验的架构才勉强写出来。
- 第一次作业是可以不用设计两个等待队列waitQueue与serviceQueue的,但为了达到良好的可扩展性,方便后续的新增需求,还是采用了相对麻烦一些的方法,这也使得后续的扩展容易许多,在功能和性能设计上都还算良好
- UML类图:

- UML协作图(sequence diagram):(由于三次作业的UML协作图完全相同,下面两部分就不重复放图了)

第二次作业
- 第二次作业主要的区别是功能上要求多个电梯,以及新增电梯的请求,主要的修改部分在Dispatcher类中,需要将serviceQueue变成一个ArrayList类型,存储多个serviceQueue类型的请求;此外还需实现新增线程及控制其运行。
- 第二次增加了多部电梯,相应的增加对线程进行控制的部分,沿用了第一次的设计,工作量不大。
- 性能设计上主要体现在请求分配与电梯运行部分,能够合理地分配请求并使用较高效率的电梯运行策略能够明显地提升性能,同时应尽可能地充分利用每台电梯。策略部分在本次设计上在Strategy中进行,请求分配则归属Dispatcher类管理。

第三次作业
- 第三次作业的功能更加复杂,电梯具有ABC三个种类,每种电梯有着不同特征,最主要的区别是停靠楼层的限制,因此在请求分配上应作出一定修改。一种思路是将请求拆分,在不同电梯间换乘到达目的地;另一种思路是只使用满足到达楼层的电梯运送该请求,这次采用的是后者的方案。
-
第三次其实希望实现换乘,不过我个人分析换乘的模式仅在部分极端状态下(如只能A类电梯大量请求)能够效率较优,最后也没有添加这一部分,但理论上要想达到最优解必须加一个阈值实现换乘。不过认真优化了一下请求分配的策略,加了诸多因子、权重、偏移量等,甚至设想了类似两层神经网络的双矩阵相乘计算方法,但手动构造参数的情况下提交上去与平均分配区别不大,并未实现较大的性能提升,不过我觉得在赋予优良参数的情况下性能能够非常优越。
- 性能部分仍为电梯运行与请求分配两部分,我认为本次作业影响更大的是请求分配部分,如果能比较合理地为每个电梯分配合适的请求,能够充分利用电梯资源,显著提升性能,降低运行时间,因此在这一部分我作了一定的优化。
- 从架构上来说,其实目前的设计实现换乘仍然有些繁琐,而且需要对请求的部分属性、调度器的结构与策略做出大幅修改,类的设计上并不具有良好的扩展性

(4)分析自己程序的bug
- 第一次作业,电梯调度采用了LOOK策略,自行测试时出现过诸多bug:如电梯在邻近楼层间跃迁不开门、电梯超载等等问题。但这些bug主要是电梯运行策略上的逻辑谬误所致,问题所在的位置主要为Strategy中的updateStrategy方法,用于实时更新管理电梯是否开关门、向哪个方向运行等策略的一个函数,并不是与线程安全有关的问题,如线程上的死锁或无限等待。
- 第二次作业,增加到了多部电梯,并没有出现bug。这也与我仔细检查了所有的synchronize的顺序,尽量规避死锁的加锁顺序有关,为此还牺牲了一定性能,在增加电梯时并未即时重分配所有请求。
- 第三次作业,强测出现了一个bug,有乘客的请求并未得到响应,同时也有一定概率出现一个人重复进了两部电梯这样的问题。经过检查,发现主要是在新增电梯时对请求的重新分配出现了问题。
(5)分析自己发现别人程序bug所采用的策略
- 三次互测都没能找出他人的bug,互测屋中大多人提交测试数据的积极性也不高,整个互测结束甚至出现只有一人提交过一个数据或者无人提交的情况,也许与互测屋的零和博弈本质促成的道德觉醒有关。不过我认为在前两次作业时测出一些bug是有一定益处的,能够更好地完善代码,利于稳定通过后续的强测,最后一次作业时的hack就是纯粹的功利角度了。
- 其实本次与线程安全相关的bug比较难测试与复现,尤其是死锁等问题并不一定每次测试一定都会出现,一方面即使本地能够测出程序问题,在提交到评测机后不一定能够复现出同样的问题;另一方面输出的正确性不太好判定,需要考虑诸多因素,比较复杂,因此也没有实现本地测试。
- 具体到策略上,无非是有时间性能限制时构造一些边界数据。如第二次作业时,先添加大量数据,最后两条指令才添加电梯,用以过滤对后加电梯未支持运行的程序;第三次作业时,构造大量如1-16层的请求,可能未实现换乘的情况会超时,等等。
- 与第一单元不同的是,本次测试的输入数据需要构造时间戳,用以指定数据输入的时间,而且提交到评测机后不一定能够复现相同的错误。另一处很大的不同是第一单元的测试策略以构造边界情况为主,短小精悍的数据即可测出bug,本次的策略的长度与性能是一体的,仅有较长较多的请求数据才有可能出现问题,例如超时或调度上出现问题。
(6)心得体会
第一次作业
- 第一次作业为实现单部电梯、三种到达模式的电梯调度与操控。虽说主要目的是熟悉线程相关操作,难度不高,但是我刚拿到题目的状态是完全不会线程、不知道该怎么写和写什么,尽管研读了半天ppt上的关于生产者消费者模式的示例、synconize的锁的用法以及wait获sleep等线程相关操作的用法,但还是不知道具体怎么写。
- 另一个感到比较疑惑的地方是为什么要用多线程实现电梯?引入线程不仅凭空使代码更加复杂,而且需要考虑各种线程安全问题。我当时思考后的理解是请求输入与处理的实时性只有线程能够实现,也就是说输入并非一次性地进入,只需进行一定运算即可,另外也是为后续需要多个电梯同时运行作铺垫。
- 在设计架构上,助教在讨论区推荐了状态模式与策略模式。但经过相关资料的查询,也尝试着写了几个状态类、策略类等,最后还是放弃了,觉得实在是有些复杂且无谓了,而且一些关键实现处仍然不是很清楚。不过在分享课上,有同学介绍了自己用对应模式写出的架构,其中,状态模式指的应为电梯的不同状态,即开关门、运行、寻找楼层等等,采用这种模式的话电梯运行逻辑上应该会清晰一些
- 在关键的难点上,我认为有如下几处:
- 输入输出的请求读取与格式问题并不需要自己处理,课程组提供了完备的可供导入的jar包。需要依照教程在项目中导入这些jar包,之后import包中相应的类,之后使用已经写好的输入输出函数即可,并不需要自己实现相应内容,自己只需聚焦于核心的电梯逻辑即可。jar包中包括了PersonRequest类型的乘客请求,可以get到乘客id、fromFloor、toFloor等等信息,输出时也只需将System.out.println()替换成输出包中相应函数即可,这时输出中会自动包含了符合格式的时间信息,非常简单;此外初始化时间戳等操作也有相应的已经写好的函数,可以说非常体系化了。我在最开始时还误以为所有输入数据的读取、处理,时间戳的附加等等都需要在自己的程序中完成,感到此次作业的难度与工作量极其之大,后来才注意到不起眼的角落提到了有第一次作业的官方包可以调取使用。
- 线程部分有许多特殊的操作,包括wait、notifyall、sleep等关键操作,启动、运行与结束线程的方法如何操作,以及synchronized锁的含义与使用等等问题,这一部分需要仔细研读才能都勉强搞懂。这里贴一部分Elevator类、Dispatcher类与InputThread类的run部分以供参考:
- Elevator类
public void run() { while (true) { synchronized (serviceQuene) { synchronized (waitQuene) { if (innerPerson.isEmpty() && serviceQuene.isEmpty() && waitQuene.noWaiting() && waitQuene.isEnd()) { //System.out.println("Elevator 1 " + " over"); return; } } if (serviceQuene.isEmpty() && strategy.isFree()) { try { serviceQuene.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } //Deal operate(); //todo System.out.println("Done ONCE."); } }
- Dispatcher类
public void run() { ArrayList<PersonRequest> temp = new ArrayList<>(); while (true) { synchronized (waitQueue) { if (waitQueue.isEnd() && waitQueue.noWaiting()) { //todo System.out.println("Dispatch over"); synchronized (serviceQuene) { serviceQuene.notifyAll(); } return; } if (waitQueue.noWaiting()) { try { waitQueue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { temp.addAll(waitQueue.getRequests()); for (int i = 0; i < temp.size(); i++) { PersonRequest request = temp.get(i); synchronized (serviceQuene) { serviceQuene.changeUpdateState(true); serviceQuene.addRequest(request); serviceQuene.notifyAll(); } //todo System.out.println("Wait->Service:" + request.getPersonId() + // "-FROM-" +request.getFromFloor() + "-TO-" + request.getToFloor()); temp.remove(request); i--; } waitQueue.clearQueue(); } } } }
- InputThread类
public void run() { ElevatorInput elevatorInput = new ElevatorInput(System.in); String arrivePattern = elevatorInput.getArrivingPattern(); elevator.configPattern(arrivePattern); while (true) { PersonRequest personRequest = elevatorInput.nextPersonRequest(); synchronized (waitQueue) { if (personRequest == null) { waitQueue.close(); waitQueue.notifyAll(); return; } else { PersonRequest request = new PersonRequest(personRequest.getFromFloor(), personRequest.getToFloor(), personRequest.getPersonId()); waitQueue.addRequest(request); waitQueue.notifyAll(); } } } }
- Elevator类
-
- 最后一部分是电梯运行策略的选择,有FAFS、ALS、SCAN、LOOK等等诸多算法,推荐使用LOOK算法,该算法与日常生活中的电梯策略非常相近,即从低层到高层、再从高层到低层,路上捎带与运行方向相同的乘客,另外在性能上也还算有比较好的表现,我个人感觉也比效率低的ALS等算法更容易理解与实现。
第二次作业
- 第二次作业引入了多个电梯,与此同时OS课正好讲到进程管理部分,其中的许多算法给了我许多启示,虽然没能实现,但感觉也是一些不错且可行的想法,有以下几个关于多电梯调度的设想:
- 分层分段,每个电梯管特定几层。空闲的电梯可以提前到预定位置等待,节约一定时间
- 多级队列 分片时间段运人指定楼层数 强占式 按优先级调度
- 彩票比率 为每个乘客当作进程,随机分配固定长度的进程时间片
- 依照为乘客请求设定优先级的方式调度。如手动分配ddl,人性化设计,如等的时间长的提高优先级
- morning模式下的最优解:类似于100层大厦的扔鸡蛋问题。如一个电梯仅运1人至20层,争取和运了6人到10层的时间差不多,又和到4层运了两趟的时间差不多。
- 线程安全上,我认为需要关注下两个问题。一是死锁,如一个线程在synchronized(A)内部执行synchronized(B),另一个线程在synchronized(B)内部执行synchronized(A),这种情况有一定概率导致死锁的问题,应当尽量规避上述写法,在变量使用结束后及时归还锁;另一问题是线程的无限等待,即设定了一个触发线程wait的条件,但未能有其他线程将其唤醒,导致程序停滞运行但又未结束的现象。
- 在其余问题的实现上,如增加多部电梯,感觉并不复杂,只是略微改了创建电梯线程与其中Queue的个数即解决了问题,因此不过多赘述。
第三次作业
- 第三次其实希望实现换乘,不过我个人分析换乘的模式仅在部分极端状态下(如只能A类电梯大量请求)能够效率较优,最后也没有添加这一部分,但理论上要想达到最优解必须加一个阈值实现换乘。不过认真优化了一下请求分配的策略,加了诸多因子、权重、偏移量等,甚至设想了类似两层神经网络的双矩阵相乘计算方法,但手动构造参数的情况下提交上去与平均分配区别不大,并未实现较大的性能提升,不过我觉得在赋予优良参数的情况下性能能够非常优越。
- 另外由于没有实现换乘,因此改动也比较小,只是在请求分配的过程中把请求分配给特定电梯而已,工作量与第一次作业到第二次作业的改动差不多,因此花了一定研究分配请求的优化上,还算略有效果,在电梯运行策略不够完善的情况下,性能相对于平均分配等简单分配方式有少许提升,不过进一步优化采用更好的参数取值应当能达到更好的效果,此部分的代码如下:
private ServiceQuene allocRequest(PersonRequest request) { ArrayList<String> arriveTypes = checkRequestType(request); //int[] multFactor = {8, 9, 12}; double[] multWeight = {9, 8, 6}; //公因数72,保持8:9:12配比 double[] notFullWeight = {1, 0.2, 0.1}; //人未满时优先分配权重 double[] plainWeight = {18, 9, 0}; //常数偏移权重 double[] distanceWeight = {0.6, 0.4, 0.2}; //距离偏移权重 ServiceQuene minQuene = null; double min = 0; double multweight; double nfweight; double bweight; double dweight; double randweight; int sizeFac; int floorFac; for (int i = 0; i < elevNum; i++) { ServiceQuene thisSQ = serviceQuenes.get(i); if (i == 0) { multweight = multWeight[0]; nfweight = 1; bweight = plainWeight[0]; dweight = distanceWeight[0]; randweight = (Math.random() + 4.5) / 10; sizeFac = serviceQuenes.get(0).getRequests().size(); floorFac = Math.abs(request.getFromFloor() - BASIC_FLOOR) + Math.abs(request.getFromFloor() - request.getToFloor()); min = (sizeFac * multweight * nfweight + floorFac * dweight + bweight) * randweight; minQuene = serviceQuenes.get(0); continue; } if (arriveTypes.contains(thisSQ.getType())) { int tp = thisSQ.getTypeOfInt(); multweight = multWeight[tp]; nfweight = (thisSQ.isFullCarry()) ? 1 : notFullWeight[tp]; bweight = plainWeight[tp]; dweight = distanceWeight[tp]; randweight = (Math.random() + 4.5) / 10; sizeFac = thisSQ.getRequests().size(); floorFac = Math.abs(request.getFromFloor() - BASIC_FLOOR) + Math.abs(request.getFromFloor() - request.getToFloor()); double thisweight = (sizeFac * multweight * nfweight + floorFac * dweight + bweight) * randweight; //todo System.out.println("m:" + multweight + "; // nf:" + nfweight + "; b:" + bweight); //todo System.out.println("Weight of Elev" // + thisSQ.getId() + ":" + thisWeight); if (thisweight < min) { min = thisweight; minQuene = thisSQ; } } }
其他
-
个人感觉这次作业相比往年增加的”Morning“/”Night“/”Random“模式是一处很好的创新,但可以进一步增大区分度,具体改善没有成形的想法,但总体思路是使得隐性要求需要为每一种模式采用独立的策略才能够取得较好的运行结果,而不是所有模式下一种LOOK的电梯策略即可一次性解决问题。
- 个人认为本次三次作业的难度递进不够合理,0到第一次作业的跃进太大,1到2、2到3则几乎改动很小,可以考虑把增大区分度后的Morning、Night、Random等的实现往后移,而且第一次作业误导性地介绍了ALS捎带策略,而事实上三次作业在电梯运行策略部分不会有太大修改,在第一次作业实现一个运行策略良好的电梯策略非常重要,因此第一次作业可以重点着眼于电梯运行策略的实现和优化上。
- 仍然是上次的问题,希望博客作业能够减少硬性的内容要求,如规定画怎么样的图、从哪个角度分析等等(可以代替为推荐写作角度),而是给予一定的自由发挥空间,个人根据自身理解决定博客写作的重点。

浙公网安备 33010602011771号