三次作业中锁的设计
第一次作业
本单元作业我采用的均是生产者-消费者模型,生产者即为输入线程Input,消费者是六个Elevator线程,具体流程是Input先将得到的乘客请求输入到公共等待队列waitmap中,然后通过调度器Manager线程将waitmap中的乘客分配给每个Elevator各自的等待队列waitlist中。
第一次作业中,只需要考虑input写入waitmap与manager读取waitmap的冲突以及manager写入电梯的waitlist和电梯的读取自身的waitlist的冲突即可,所以我直接给WaitList类的读写方法加了synchronized控制块,同时,在某些需要多次访问WaitList类的地方加上了synchronized控制块。
Waitlist的读写方法如下:
public synchronized void addPerson(Person person) {
waitList.add(person);
notifyAll();
}
public synchronized Person getOnePerson() {
if (waitList.isEmpty() && !isEnd) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (waitList.isEmpty()) {
return null;
}
Person person = waitList.get(0);
waitList.remove(0);
notifyAll();
return person;
}
可以看到,在确保线程安全的同时,应尽量缩小同步块的范围,以提高程序的执行效率。这意味着同步块内的代码应紧密关联于锁对象,例如,在使用`waitmap`锁时,应确保被锁住的代码块内主要进行与`waitmap`相关的读写操作。通过此种方式,可以最大限度地减少线程间的竞争,提高程序的并发性能。
第二次作业
由于本次作业与上次作业在基本架构方面未发生改变,所以上次的同步控制块基本不用加以修改。主要修改的部分是对于Manager类和Input类的控制,由于Reset指令需要修改Manager类的电梯队列,因此这个类被动成为共享对象,所以我在这个类中加上了读写锁来保证线程安全。
除此之外,由于在Maintain之后需要修改被踢出乘客的出发地,因此Person也可能出现线程冲突,所以我在该类内部也加上了读写锁。
public void setStart(int newstart) {
lock.writeLock().lock();
try {
start = newstart;
} finally {
lock.writeLock().unlock();
}
}
第三次作业
本次作业增加的内容主要是在调度策略上实现的,因此同步块及锁基本与上次保持一致,只是在CountController中注意严格的线程互斥即可,同时在修改调度策略时注意对waitmap和waitlist的保护。
调度器与调度策略
第一次作业
本次作业我的调度器即为Manager类,它通过waitmap和Input与Elevator进行交互,Input向waitmap中写入数据,Manager从中取出,然后按照调度策略分配给相应电梯的waitlist。由于第一次作业指定了电梯ID,因此不需要对于电梯的调度有专门的对策设计。
第二次作业
第二次作业不再指定电梯ID,且增加了Reset指令。这次作业我采用的调度策略也较为简单,为了充分利用电梯的空间,我采取的策略是如果请求乘客的目标方向与某一电梯的运行方向相同,并且该电梯还未到达请求乘客的出发层,且它的内部乘客和等待乘客之和不满6人,则将该电梯视作一个备选电梯,在所有备选电梯中选择一个离请求乘客最近的电梯作为最终决定的电梯。如果所有电梯都不满足备选电梯的条件,那么就按照指导书推荐的乘客序号(非输入编号)模6的方式进行分配。
public WaitList getWaitList(Person person) {
int des = person.getDes();
int start = person.getStart();
int distance = 20;
WaitList waitList = null;
for (Elevator elevator : elevators) {
int dir = elevator.getDirection();
int pos = elevator.getPos();
int innum = elevator.getInnum();
WaitList waitList1 = elevator.getWaitList();
}
}
if (waitList == null) {
waitList = waitLists.get(sum % 6);
}
sum++;
return waitList;
}
}
第三次作业
本次作业我对调度器的调度策略进行了大幅度的改动,首先是对于无换乘的情况,依然沿用之前的最近同向电梯的思路,但是考虑了更多的细节问题。
1、当电梯中没有人时,如果请求乘客和电梯运行方向相同,只有当该乘客上电梯和下电梯的楼层都在电梯目前位置和去接的乘客上电梯的位置之间时才可以捎带。
2、当电梯中没有人时,如果请求乘客和电梯运行方向相反,则看请求乘客目的方向是否和电梯去接的乘客方向相同,若相同,看该乘客上电梯的位置是否在电梯去接的乘客上电梯的位置之后。如果符合,则该电梯可以做备用电梯,但是距离等于电梯到它要接的人的距离加上它要接的人的距离到请求乘客上电梯位置的距离之和。
三次作业框架变化及拓展能力
UML类图
总体来看,这三次作业其实整体的架构并没有发生太大的变化,从第一次到最后一次只在第三次作业增加了DoubleElevator、ABElevator和ABStrategy类来处理双轿厢调度,由此可见,生产者-消费者模型的可扩展性是很强的,这也说明了层次化设计的重要性。
经过几次作业的迭代和优化,我的电梯调度系统已经实现了DoubleElevator、ABElevator和ABStrategy等核心类的集成,以解决双轿厢电梯的调度问题。在我的设计中,我采用了多线程并发处理的方式,将Schedule和normalElevator(A/BElevator)作为独立线程运行。Input线程负责接收ElevatorRequest和PersonRequest,并将它们分别传递给Elevator和Schedule线程进行处理。
Elevator线程会查询是否有重置指令,并根据Schedule线程的分配获取PersonRequest进行处理。在电梯重置的过程中,如果是普通重置,被分配的PersonRequest会被放入preWaitQueue队列中,等待重置结束后再统一进行处理。而如果是DCErest重置,除了将请求放入preWaitQueue中,还会新建一个双轿厢电梯对象来处理该请求。
为了保证数据的一致性和安全性,在修改电梯的HashMap时,我采用了适当的保护措施。然而,尽管我已经尽力进行了优化和改进,但当前的架构仍然存在一些可扩展性方面的问题。由于前期准备不足和时间紧迫,我在实现过程中可能无法完全遵循“高内聚、低耦合”的设计原则。这导致在后续迭代过程中,一些低内聚、高耦合的问题逐渐暴露出来,如死锁等。
线程之间的协作关系
第一次作业UML协作图
第二次作业UML协作图
第三次作业UML协作图
三次作业中稳定和易变的内容
通过前两次迭代,我们可以看出,如果要增加对电梯运行的限制,大多数情况下我们只需要修改Elevator中的内容即可,如果需要修改调度策略,只需要修改Manager中的内容即可,整体架构并不需要进行改动。这三次作业中,稳定的内容是电梯的运行方式,例如上行、下行、开门、关门、上下乘客等都没有发生较大改变,易变的内容就是电梯的运行限制条件以及电梯的调度策略。
第三次作业中是如何实现双轿厢的两个轿厢不碰撞的
主要实现思路为为每部电梯设置一个标志位,标志位没有被置位时电梯可以进入换乘楼层,若某个电梯进入换乘楼层则置位,此时另一个电梯就无法进入了。
程序出现过的bug以及自己面对多线程程序的debug方法
第一次作业比较简单,修正了一些轮询问题。
第二次作业在设计上出现了有关各个线程何时结束的问题。由于第一次作业时电梯队列在Input结束后就已经设为了end,导致这一次Reset的电梯放出去的人可能没有别的电梯可坐。为了解决这个问题,在每个电梯运送完所有乘客后都唤醒一次Manager,让Manager判断一次是否可以结束程序。同时,在Input输入结束时也应该唤醒一次Manager,再判断是否可以结束。这样就可以保证无论Input何时结束,程序都可以在运送完所有乘客后正常结束。
第三次作业中,我出现了死锁的问题,线程共享数据中大量的锁和终止条件的设置出现了读入了在某个队列移除但未新加入的中间态结果而提前结束的问题,但修改的过程中微小的改动都会造成新的地方出现问题,架构初期考虑不不周导致了后面错误连篇。
本单元debug最大的障碍在于线程运行的异步性,导致无法准确获知各线程的运行状况。
采用了在每个线程循环的开始处打印一些信息,如果某时刻输出了一堆信息,就说明该线程在不断轮询。如此一来,调试就变得非常轻松了。
心得体会
在本次作业中,深刻的认识到层次化设计的重要性。好的架构在遇到问题时只需要根据单一职责对局部内容进行修改,不会出现出走一颗积木就整盘崩塌的问题。而在面向对象的过程中,我们需要对每个对象的职责进行拆分,比如reset不应该是一条普通的顺序响应指令,而是一个电梯的命令,在考虑到不同需求的层次时才能更好的设计。而如果能考虑到未来可能增加的功能,可扩展性也能得到保证。
线程安全则更为重要,如何能在最小同步块中保障功能的实现,在同步块外,读和写之间可能存在的状态改变,在这些都是我在完成之处考虑的并不周到的问题。如果现在问我能不能设计出线程安全的程序,我可能比最初更有信心一些,但也没有完备的思考方法,可能还是需要更多的练习以及充分的思考时间。