1 同步块的设置和锁的选择
我对同步块的设计是采用synchronized
关键字,并使用wait-notifyAll
模式。本次作业中的请求队列RequestQueue
可以同时被输入线程、调度器线程和电梯线程访问,因此存在线程安全问题。因此在RequestQueue
中,我采用了对方法加锁的方式,将其中涉及到成员变量读写的方法全部加了锁,这可以保证每次只有一个线程来访问请求队列。
尽量减少synchronized
块的长度,增加线程间的并发程度;在同步块中合理使用wait
,notifyAll
,使用不当会导致轮询、死锁等现象。
保证锁的一致性,在第三次作业中,对换乘层是否被占领,最初采用的wait-notifyAll由于锁的不正确性,导致错误。所以在wait-notifyAll中一定要先判断清楚锁是什么,再对方法上锁。
其中,请求队列RequestTable
可以同时被输入线程和电梯线程访问,因此存在线程安全问题。在本次作业中,我主要是采用了”对方法加锁“的方式,将RequestTable
涉及到成员变量读写的方法全部加了锁,这样就可以保证每次只有一个线程来访问请求队列。
2 调度
2.1 调度器设计
我的调度器采用单例模式,在全局中只实例化一个调度器对象,使调度器能够更好地统筹全局,同时,在调度器中新建双轿厢电梯,使用单例模式有利于DCReset的实现。
单例模式采用的是饿汉式单例模式的设计,仅在首次调用时才创建单例实例。这意味着实例只在需要时才被创建,可以节省资源。最初考虑的懒汉式实现需要考虑线程安全问题,因为在多线程环境下,可能出现多个线程同时调用 getInstance() 方法,导致创建多个实例的问题,所以最终采用的是饿汉式单例模式。
public class Controller extends Thread { private static volatile Controller instance; public static Controller getInstance(RequestQueue allRequests, HashMap<Integer, RequestQueue> eleRequests) { if (instance == null) { synchronized (Controller.class) { if (instance == null) { instance = new Controller(allRequests, eleRequests); } } } return instance; } }
在调度器中,有以下属性:
private RequestQueue allRequests; private HashMap<Integer, RequestQueue> eleRequests; private int curPersonRequestNum = 0;
其中,allRequest
是所有电梯的总请求队列,eleRequests
是以电梯id
为Key
的电梯各自请求的HashMap
,curPersonRequestNum
表示当前未完成的请求个数,用于线程的结束。
2.2 调度器如何与程序中的线程进行交互
2.2.1 与输入线程InputThread
-
关于调度
输入线程中也有
allRequest
,用于接收乘客请求,当输入请求为PersonRequest
,则加入allRequest
,调度器会不断从allRequest
中取出请求,并分配电梯给该乘客。
-
关于线程的结束
当不再输入请求,
InputThread
会将allRequest
的isEnd
设为true
,提示调度器输入的结束。在调度器中,调度器线程结束的条件是allRequests.isEnd()
、所有电梯不在Reset状态, 以及curPersonRequestNum == 0
,即所有乘客请求都结束。
2.2.2 与电梯线程ElevatorThread
-
关于调度
调度器不断从
allRequest
中取出请求,采用平均分配的策略分配给电梯 i ,则从eleRequests
取出电梯 i 的RequestQueue
,将此请求分配到该电梯的请求队列。 -
关于线程结束
当调度器达到本线程的调度条件,就会将所有电梯请求队列的结束标志
isEnd
设为true,提示电梯请求队列线程的结束。 -
关于Reset
第三次作业是在电梯线程中实现DCReset的,当电梯得到DCReset的标志时,会开始时清空电梯、清空
RequestQueue
、利用单列模式的调度器创建双轿电梯等一系列工作。在调度器中实现双轿电梯的建立,启动,并将其的请求队列加入eleRequests
进行统一的调度管理。public synchronized void createDcElevator(DoubleCarResetRequest dcRequest) { /**********双轿电梯的建立*********/ FloorManager floorManager = new FloorManager(id,transferFloor); ElevatorThread elevatorA = new ElevatorThread(); ElevatorThread elevatorB = new ElevatorThread(); /**********加入请求队列**********/ int num = eleRequests.size(); eleRequests.put(num + 1, elevatorA.getRequestQueue()); eleRequests.put(num + 2, elevatorB.getRequestQueue()); /***********双轿电梯启动*********/ elevatorA.start(); elevatorB.start(); }
2.3 调度策略
在第二、三次作业中我使用的是平均分配策略,具体实现方式(以第三次作业为例)如下:
其中,lastId作为一个全局变量,记录上次检索到的电梯号,若 lastId++后越界,会更新为1。
若返回的电梯id为0说明没有一辆合适的电梯可以接此乘客,则让线程休眠,防止轮询。
while (true) { int elevatorId = elevatorIdForPerson(person); if (elevatorId != 0) { eleRequests.get(elevatorId).addRequest(fromFloor, person,false); break; } try { sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } }
2.4 调度策略是如何适应时间、电量等多个性能指标
-
出现五个电梯同时Reset的情况
通过限制每个电梯的请求列表个数,防止在五个电梯同时Reset时,调度器将这期间的所有请求都分配给未Reset的电梯。
-
思考
最开始,采用根据电梯的楼层与方向等属性估算一个时间值,择优选择电梯,具体时间值的实现是在电梯线程中进行,因为加入synchronized
上了锁,所以该方法的实现会电梯的其他方法的影响,而电梯中的大量方法涉及开关门、移动等,得到时间值的方法等待的时间长,所以获得每一部电梯的时间值会经历一段相当长的时间,导致我的调度策略实现时间过长,在Controller
类中,调度过慢。
所以后来使用了平均分配的方法,平均分配的好处就是防止电梯请求的分配不均,使得有些电梯的运量过大等,在平均的一个状态下完成调度。但是带来的坏处也是很显然,因为平均分配的随机性,可能会错过一更好的分配方式,导致性能分的丢失。
3 架构
3.1 第五次作业
在第五次作业中,我的代码架构基本形成与定型,其中包括输入线程、调度线程、请求线程、电梯线程、策略类等。
-
RequestQueue请求线程
-
在本次作业中,我实例化了七个
RequestQueue
对象。其中allRequests
是总的请求队列,HashMap<Integer, RequestQueue> eleRequests
是以电梯id
为key
的请求列表的HashMap
。 -
此线程有属性:
private HashMap<Integer,ArrayList<Request>> requests = new HashMap<>();//<出发地,此出发地的所有请求>
其为以出发地为
Key
的乘客请求列表的HashMap
,存储该请求队列的请求个体。
-
对于
allRequestQueue
,设置了两个方法popRequest()
和addRequest()
。 -
popRequest()
目的是遍历 personRequests 集合中的每一个元素,找到第一个非空的ArrayList<PersonRequest>
集合,并删除其中的第一个元素,然后返回这个元素。同时,如果删除元素后集合为空,还会将该键值对从personRequests
中删除。(迭代器)
-
-
InputThread输入线程
-
该线程中有属性:
private RequestQueue allRequests;
-
在本线程中,不断读取输入的请求,加入总的请求队列,等待调度器给电梯分配请求。
-
当没有新的请求,将总请求队列的结束标志
isEnd
设为true
,提示输入线程结束。
-
-
Controller调度线程
-
该线程有属性:
private RequestQueue allRequests;//所有的请求 private HashMap<Integer, RequestQueue> eleRequests;//<电梯号,电梯的对应请求>
-
若
allRequests.isEmpty() && allRequests.isEnd()
,总请求列表没有请求且输入结束,说明进程的结束,此时要将所有电梯的请求列表的结束标志isEnd
设为true
,提示电梯线程结束。 -
按照PersonRequest的要求分配电梯,将此请求加入对应电梯的请求列表。
int eleId = request.getElevatorId(); eleRequests.get(eleId).addRequest(request);
-
-
elevator电梯线程
-
在Main类中实例化六部电梯,各自独立运行。
-
该线程有属性:
private Strategy strategy = new Strategy();//电梯的决策者 private int id; private int curNum = 0;//当前人数 private int curFloor = 1;//当前楼层 private boolean direction = true;//方向 private RequestQueue requestQueue;//电梯自己的requestTable private HashMap<Integer, HashSet<Request>> destMap = new HashMap<>();//电梯内乘客的目的地Map
可以看出,每一个电梯都有自己的
RequestQueue
请求队列、strategy
决策类,还有一个以目的地为Key
的电梯内乘客的HashMap
。 -
电梯在
while(true)
中不断使用策略类来做下一步的决定:public void run() { while (true) { Advice advice = strategy.getAdvice(curFloor, curNum, direction, destMap, requestQueue); if (advice == Advice.OVER) { break; //电梯线程结束 } else if (advice == Advice.MOVE) { move(); //电梯沿着原方向移动一层 } else if (advice == Advice.REVERSE) { direction = !direction; //电梯转向 } else if (advice == Advice.WAIT) { requestQueue.waitRequest(); //电梯等待 } else if (advice == Advice.OPEN) { openAndClose(); //电梯开门 } } }
-
move操作
-
电梯沿原方向移动一层,若
direction == true
,则curFloor++
,否则,curFloor--
-
移动后,若
curFloor == 11
或者curFloor == 1
,则直接转向,这样可以节省一步判断。
-
-
openAndClose操作
-
遵循先出后进,根据
destMap
将以本层为目的地的乘客全部送出,再次利用Advice advice = strategy.getAdvice(curFloor, curNum,direction, destMap, requestQueue);
判断电梯是否转向,若需要转向,则直接转向。最后在没有超载的情况下,根据eleRequestQueue加入以本层为出发地且同向的乘客。 -
在进出乘客之间插入一次策略的询问,在出乘客后电梯没人,但是电梯外乘客的到达楼层不在电梯运行方向的情况下,可以减少一次电梯的开关门。
-
-
waitRequest()操作
采用wait-notifyAll模式,在RequestQueue中,增加waitRequest()方法:
public synchronized void waitRequest() { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }
在addRequest()操作后增加notifyAll(),来唤醒等待线程。
-
-
Strategy类
每个电梯都有自己的策略类,即策略与运行分离。它可以根据目前电梯的状态给出策略,我采用的是LOOK算法。
-
首先为电梯规定一个初始方向,然后电梯开始沿着该方向运动。
-
到达某楼层时,首先判断是否需要开门
-
如果发现电梯里有人可以出电梯(到达目的地),则开门让乘客出去;
-
如果发现该楼层中有人想上电梯,并且目的地方向和电梯方向相同,则开门让这个乘客进入。
-
-
接下来,进一步判断电梯里是否有人。如果电梯里还有人,则沿着当前方向移动到下一层。否则,检查请求队列中是否还有请求(目前其他楼层是否有乘客想要进电梯)——
-
如果请求队列不为空,且某请求的发出地是电梯"前方"的某楼层,则电梯继续沿着原来的方向运动。
-
如果请求队列不为空,且所有请求的发出地都在电梯"后方"的楼层上,或者是在该楼层有请求但是这个请求的目的地在电梯后方(因为电梯不会开门接反方向的请求),则电梯掉头并进入"判断是否需要开门"的步骤(循环实现)。
-
如果请求队列为空,且输入线程没有结束(即没有输入文件结束符),则电梯停在该楼层等待请求输入(wait)。
-
如果请求队列为空,且输入线程已经结束,则电梯线程结束。
-
-
3.2 第六次作业
本次作业的框架与上次作业一致,以下根据新增加的Reset请求,说明架构的迭代部分:
-
RequestQueue请求队列线程
-
由于电梯线程的不安全性,由最初的在电梯线程中加入
Reset
标志改为在RequestQueue
中加入Reset
标志。 -
增加属性:
private ResetRequest resetRequest = null;
resetRequest
的最初值是null
,当电梯被reset
时,此ResetRequest
会更改为相应的请求,在Reset结束之后,会重新更改为null
,所以可以根据resetRequest == null
来判断电梯是否在Reset阶段。
-
-
InputThread输入线程
因为前期通过
InputThread
调用调度器来对电梯请求队列进行setResetRequest()
操作,出现了线程不安全的问题(可能与当时其他部分代码的错误有关),所以我将这一步直接让InputThread
线程来操作,减少对调度器的调用:if (request instanceof ResetRequest){ //ResetRequest int elevatorId = ((ResetRequest) request).getElevatorId(); eleRequests.get(elevatorId).setResetRequest((ResetRequest) request); }
-
Elevator电梯线程
-
在
getAdvice()
中增加参数resetRequest
,若resetRequest
不为null
,则返回Advice
为RESET
,电梯进入Reset
阶段,否则与第五次作业一样。 -
增添
Reset()
方法实现电梯的Reset
:public synchronized void reset() { //清空电梯,电梯内人出去 clearElevator(); //清空requestQueue allRequestQueue.addRequestQueue(requestQueue); //RESET this.capacity = newCapacity; this.speed = newSpeed; //stop this.requestQueue.setResetRequest(null); }
-
clearElevator();
经历了"开门 - 乘客出去 - 关门"阶段,其中已到达目的地的乘客直接清除,若有未到达的乘客,将其的出发楼层改为先楼层,加入
allRequestQueue
总请求队列,等待调度器继续为他分配电梯。 -
清空requestQueue
将本电梯的请求队列全部加入
allResetQueue
,等待调度器继续为未完成的请求分配电梯。
-
-
-
关于线程的结束
-
当输入结束,输入线程
InputThread
会将allRequest
的结束标志设为true
。 -
在调度器线程中,当满足
allRequests.isAllEmpty() && allRequests.isEnd()
即所有请求分配结束,且不再有输入,不一定意味着结束,因为在满足此条件后,有可能发生电梯在Reset阶段将自身的一些请求仍回总表的事情,所以要添加一次判断是否所有的电梯都不在Reset阶段。while (true) { if (allRequests.isAllEmpty() && allRequests.isEnd()) { if (isAllNotReset()) { for (int i = 1; i <= 6; i++) { eleRequests.get(i).setEnd(true); } return; } else { try { sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
可以注意到,若还有电梯在Reset阶段,我添加了一个sleep环节,是因为,若还有电梯在Reset阶段,在while(true)中会一直连续不断地判断,导致轮询,根据电梯Reset的时间1200ms,取了一个接近的中间值,让线程sleep500ms,解决了轮询的问题。
-
3.3 第七次作业
本次作业的框架与上次作业一致,以下根据新增加的DCReset请求,说明架构的迭代部分:
-
RequestQueue请求队列线程
-
增加属性:
private DoubleCarResetRequest doubleCarResetRequest = null;
doubleCarResetRequest
的最初值是null
,当电梯被DCReset
时,此doubleCarResetRequest
会更改为相应的请求,在DCReset结束之后,会重新更改为null
,所以可以根据doubleCarResetRequest == null
来判断电梯是否在DCReset阶段。
-
-
FloorManager类
这是用于管理换乘层的类,一对双轿电梯对应一个FloorManager。
-
Controller调度器线程
-
由于DCReset需要创建双轿电梯,并且考虑到Controller是统筹全局的一个单例,所以在本次作业中,我将Controller设为饿汉式单例模式。
-
调度策略的调整(见上文)。
-
在Main主类中第一次创建,在之后的其他地方就无需创建新的Controller线程,保证了线程的安全性。
Controller controller = Controller.getInstance(allRequests, eleRequests);
-
增加了创建双轿厢电梯的方法:
public synchronized void createDcElevator(DoubleCarResetRequest dcRequest) { //双轿厢电梯以及相应的FloorManager的建立 FloorManager floorManager = new FloorManager(id,transferFloor); ElevatorThread elevatorA = new ElevatorThread(); ElevatorThread elevatorB = new ElevatorThread(); //将两部双轿厢电梯的请求列表加入eleRequests的HashMap中 int num = eleRequests.size(); eleRequests.put(num + 1, elevatorA.getRequestQueue()); eleRequests.put(num + 2, elevatorB.getRequestQueue()); //电梯启动 elevatorA.start(); elevatorB.start(); }
-
-
ElevatorThread电梯线程
-
增加了双轿厢电梯的相关属性:
private boolean isDoubleCar = false;//是否为双轿电梯 private int transferFloor = 0;//换乘楼层 private FloorManager floorManager; private char dcAorB;//是双轿电梯的A梯还是B梯
-
增加了dcReset方法,完成DoubleCarResetRequest:
其中步骤基本和NormalReset相似,增加了引入Controller的单例模式来创建双轿厢电梯。
public synchronized void dcReset() { //清空电梯,电梯内人出去 clearElevator(); //清空requestQueue allRequestQueue.addRequestQueue(requestQueue); //创建双轿电梯 Controller controller = Controller.getInstance(null,null); controller.createDcElevator(dcRequest); //stop this.requestQueue.setDoubleCarResetRequest(null); this.isEnd = true; //本电梯线程结束 this.requestQueue.setEnd(true); //本电梯的请求列表结束 }
创建完双轿电梯,我就将原电梯的结束标志设为true,将原电梯的请求列表线程的结束标志也设为true,终结原电梯的运行。
-
双轿厢电梯的换乘处理——move()操作(以A梯为例)
如果下一层的楼层不是换乘层,则执行原有的move操作;
如果下一层的楼层是换乘层,首先判断换乘层是否被占,其中运用了floorManager对换乘层的管理:
while (floorManager.isOccupied()) { //换乘层被占 try { sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } }
当换乘层不被占,则跳出循环,来到接下来的操作:
//占领换乘层 floorManager.setOccupiedState(true); curFloor++; direction = !direction;
到达换乘层后,有本层的请求要进,或者目的地在换乘层及换乘层上的要出,需要实现换乘操作:
if(this.requestQueue.getThisFloorRequest(curFloor)!= null||!isDestMapEmpty()) { transferA(); }
换乘操作包括"开门 - 出客 - 进人 - 关门",其中,出客指的是换乘层及换乘层以上的乘客全部出去,若已经到达目的地,说明请求已经完成,若未到达目的地,修改该乘客的出发楼层,将其重新加入
allRequest
,等待其他电梯的分配。换乘结束后,需要立即离开换乘层,不占用换乘层,这也是我的解决相撞的一个方法。
curFloor--; floorManager.setOccupiedState(false);
-
3.3.2 双轿厢不相撞处理
首先,我设置了一个floorManager类来管理换乘楼层:
public class FloorManager extends Thread { private boolean occupied = false; public synchronized boolean isOccupied() { return this.occupied; } public synchronized void setOccupiedState(boolean occupied) { this.occupied = occupied; } }
每一对双轿电梯有同一个floorManager
,对于双轿厢电梯的运行,我的解决方法如下:当下一个楼层是换乘层的话,若换乘层被占,则等待对方电梯离开后占领换乘层,在完成换乘操作后,直接离开换乘层,不长时间占用换乘层,大致步骤如下:
while (floorManager.isOccupied()) { //换乘层被占 try { sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } //不被占 //占领换乘层 floorManager.setOccupiedState(true); curFloor++; direction = !direction; //换乘 transfer(); //离开换乘层 curFloor--; floorManager.setOccupiedState(false);
其中对换乘层被占的解决,我没有采用wait-notifyAll()
模式,是因为由于我的不当使用,造成死锁、轮询,心在细想,是由于我锁住的对象不统一造成的,在最开始的代码中,我在电梯线程中使用synchronized
块,实际上,这个锁是电梯线程本身,并非floorMagager
,所以在floorManage
r中无法唤醒线程。所以,在最终的版本,我采用了sleep()
。
3.4 可扩展性、稳定和易变
3.4.1 可拓展性
如果要更改楼层限制,只需修改常数MAX_FLOOR,MIN_FLOOR,增加电梯总数可以修改MAX_NUM,修改初始电梯数量修改INITIAL_NUM,容量CAPACITY等等这些常数。
如果要更换调度策略或者电梯运行策略只要新加调度函数即可,换成策略改变也可以新增换乘函数等等。
3.4.2 稳定与易变
-
电梯的运行LOOK策略、线程的交互都是稳定的,在三次作业中基本变化不大。
-
易变的是调度策略,若需要性能更好的调度策略,我们需要在调度器类重新编写。
-
同时根据两次不同的迭代,需要在不同的类增添不同的属性与方法来适应题目新的要求。包括一些电梯的具体运行虽然在总体上基本不变,但是还需要针对不同的要求进行适当的修改。
4 bug & debug
4.1 第五次作业
在电梯openAndClose()
开关门的方法中,我使用了一个循环内删除的方法:
for (Request r : curIn) { destMap.get(destination).add(r); requestQueue.deleteRequest(r); }
RequestQueue
中deleteRequest()
方法:
public synchronized void deleteRequest(Request r) { requests.get(fromFloor).remove(r); }
可以看出这是一个跨类的删除操作,由于没有加入迭代器,出现了ArrayList
的ConcurrentModificationException
异常。最后放弃跨类删除,直接在Reuest类中完成迭代器删除操作。
4.2 第六次作业
4.2.1 Reset不及时
最初我将电梯Reset的标志属性存储在电梯线程中,由于电梯线程的复杂性与不安全性,经常出现Reset
不及时的问题,即RESET-ACCEPT
到RESET-BEGIN
这段时间过于长,所以我将电梯的Reset
标志属性以resetRequest
的形式存储在RequestQueue
中。
4.2.2 关于何时reset
最初我是在Controller
即调度器中实现Reset
操作,即Input
输入线程读入ResetQuest
会调用调度器对该电梯的RequestQueue
中的ResetQuest
作出设置。
但是,那个时候,我写的调度策略是采用根据电梯的楼层与方向得到一个时间值,择优选择电梯,具体时间值的实现是在电梯线程中进行,因为加入synchronized
上了锁,所以该方法的实现会电梯的其他方法的影响,而电梯中的大量方法涉及开关门、移动等,得到时间值的方法等待的时间长,所以获得每一部电梯的时间值会经历一段相当长的时间,导致我的调度策略实现时间过长,在Controller
类中,调度过慢,导致调度器对该电梯的RequestQueue
中的ResetQuest
作出设置的反应时间慢。
当时没有意识到这个问题,以为是Input
线程调度Controller
线程有时间差,没有进行及时的Reset
,所以,我选择在Input
线程中一读到ResetRequest
请求就设置Reset
标志。
4.2.3 调度策略
正如上文所说,最开始的调度策略,我是根据电梯的楼层与方向估算一个相应的时间值,选择用时少的电梯,具体得到时间值的方法由于要用到电梯的一系列性质,我选择在电梯线程中实现,因为加入synchronized
上了锁,所以该方法的实现会电梯的其他方法的影响,而电梯中的大量方法涉及开关门、移动等,得到时间值的方法等待的时间长,所以获得每一部电梯的时间值会经历一段相当长的时间,导致我的调度策略实现时间过长。
最初没有意识是电梯线程的问题,所以放弃了这个方法,直接在Controller调度类中实现平均分配策略。
4.2.4 轮询问题
以下是Controller调度器类中的结束代码:
while (true) { if (allRequests.isAllEmpty() && allRequests.isEnd()) { if (isAllNotReset()) { for (int i = 1; i <= 6; i++) { eleRequests.get(i).setEnd(true); } return; } } }
若allRequests.isAllEmpty() && allRequests.isEnd()
,但是还有电梯还在Reset
状态,则会不断进行判断,造成轮询状态,所以根据电梯Reset
的时间为1200ms,取了相对平均的中间值500ms,让线程休眠一会儿,再进行判断,避免轮询。
while (true) { if (allRequests.isAllEmpty() && allRequests.isEnd()) { if (isAllNotReset()) { for (int i = 1; i <= 6; i++) { eleRequests.get(i).setEnd(true); } return; } else { try { sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }
4.2.5 五个电梯同时reset问题
当五辆电梯同时在Reset状态时,如果在同一时间有大量的请求,调度器会把这些请求分配给同一部电梯造成运行超时。所以为了避免这种情况发生,我限制了每辆电梯的请求个数,当超过这个限制,则不会再给这部电梯分配新的请求,只好等下一次的调度。
同时可能发生轮询情况,因此我同样利用sleep,让电梯休眠一段时间。
while (true) { int elevatorId = elevatorIdForPerson(person); if (elevatorId != 0) { eleRequests.get(elevatorId).addRequest(fromFloor, person); break; } try { sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } }
4.2.6 电梯参数修改未使用
因为这个bug我强测的分数低的可怕,没有进入互测,在判断乘客进电梯的时候,还是沿用第五次作业capacity为6的情况,导致错误,细节没有认真检查。
4.3 第七次作业
4.3.1 双轿厢电梯相撞问题
在上文说过,我是在电梯线程中wait,在floorManager中notifyAll,两个方法的锁不一样,自然无法唤醒。最初没有深入思考,所以选择投机取巧,选择了让线程sleep。
while (floorManager.isOccupied()) { //换乘层被占 try { sleep(200);//本来是wait() } catch (InterruptedException e) { e.printStackTrace(); } }
4.3.2 乘客请求未完成线程就结束问题
由于所有线程的结束条件是输入结束且分配完毕且没有电梯在reset阶段且电梯所有请求完成,由于加入双轿电梯,当最后一个请求是需要换乘的双轿厢电梯,换乘之前,总请求队列分配完毕且结束,换乘后,未完成的请求加入总请求无意义,造成乘客请求未完成。
所以在总请求队列中我加入了未完成情求个数的统计,只有乘客请求到达指定的目的地,未完成情求个数才能减一,在判断线程是否结束的过程中,我们需要追加判断未完成请求个数是否为0,即可解决。
4.4 debug方法
-
主要采用的是官方包的Timable.println()方法,对线程每一个步骤进行实施跟踪,分析时序。
-
面对轮询问题,在每一个while循环的开头打印相关标志,准确定位轮询位置。
-
对乘客进行跟踪,利用测评机的分析功能,有些乘客进出电梯后不换乘或者进入电梯后不出电梯,都可以通过测评机检测出来
5 心得体会
-
线程安全
三次作业由于线程安全的问题产生了很多bug,我梳理了一下几点:
-
加锁要慎重
对共享对象做同步处理不意味之要将所有的方法都加上锁,会极大影响并发效率。要尽量减少
synchronized
块的长度,增加线程间的并发程度;在同步块中合理使用wait
,notifyAll
,使用不当会导致轮询、死锁等现象。 -
保证锁的一致性
在第三次作业中,对换乘层是否被占领,最初采用的wait-notifyAll由于锁的不正确性,导致错误。所以在wait-notifyAll中一定要先判断清楚锁是什么,再对方法上锁。
-
-
层次化设计
在最初接触电梯作业时,没有形成完整的流程和功能模块的分析,导致最初的代码写得磕磕绊绊。在面对多线程编程时,要首先根据需要实现的功能,分成多个不同的功能模块,利用前驱图,调度关系,梳理各个线程之间的同步与互斥关系,避免死锁、无人唤醒等异常情况。并规定好不同模块之间的接口,利用共享对象传递信息,然后再自上而下依次实现。
第一次接触多线程编程,面对第五次作业有点无从下手,实验代码的代码架构为我提供了思路。在实验架构的基础上,我增添了调度策略算法、电梯运行策略分析,实现一定的层次化设计。
层次化设计有利于迭代开发,三次作业的架构基本没有变化,只需根据新的要求,分析新的流程,增添新的功能模块。所以,在最初对作业进行层次化设计是很有必要的,不仅使思路更加清晰,提高debug的效率,对后续的迭代开发也是很有帮助的。
-
体会
继上次第三次作业求导细节错误导致没有进互测屋,第六次作业也发生了相似的情况,即电梯进客时采用的容量仍为6,导致超载,细节方面还是没有认真检查,让我非常懊悔。希望在之后的单元中,一定要注意审题,检查细节,提高自己的架构设计能力,养成封装,层次化设计的习惯与能力。