本次OO第二单元实现的主要任务为模拟了一个多线程电梯运行的基本场景,基于“生产者-消费者模式”实现了满足不同的电梯调度以及运行策略达到满足用户上下楼、换乘等请求的电梯。
不得不说比起往届的博客,可以看出这届的助教还是收手了的,让我们在这一单元的编程充实快乐但又不失必要的痛苦,体会到多线程独特的魅力,对此我深表感激(doge)。
由于本单元的一些问题比较共性,主要集中在线程安全一块,所以本次作业将不采用分作业分析的形式,改用按点总结。
在HW 5和HW 6中我仅采用了实验中给出的同步控制方法:
public synchronized void addRequest(Request request) { requests.add(request); notifyAll(); } public synchronized Request getOneRequest() { if (requests.isEmpty()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (requests.isEmpty()) { notifyAll(); return null; } /*=====balabala=====*/ notifyAll(); return /*=====balabala=====*/; }
为了保证线程安全,利用synchronized对增加和拿取请求的方法进行修饰,保证同一时刻只能有一个线程对共享队列(调度器向总请求队列 / 调度器向楼层或楼座的队列 / 电梯向楼座或楼层的队列)进行访问。
在电梯或者调度器索取请求的时候,假如队列为空,若此时isEnd()为true,则直接结束线程,否则等待被setEnd()方法notifyAll后结束线程或者被addRequest()方法notifyAll后处理该请求,如此一来在没有请求到来时电梯和调度器即可保持wait,避免了轮询的情况。
笔者在这一单元的作业中把对共享队列的每一个操作全部独立了,没有存在方法中互相调用的情况,原因是怕写着写着莫名其妙地死锁了。后来查阅发现java中线程获得对象的锁是以线程为单位的,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。这多好,大大降低了我们死锁的情况,或者说写出了死锁说明我们逻辑太菜了。
然而,用synchronized修饰方法同时约束了读和写操作,一定程度上降低了程序的效率。这是我们不希望的,于是我在HW 7中在对迪杰斯特拉算法的图进行维护的时候采用了ReentrantLock的读写锁机制:
public int[][] getSpath() { int[][] updatedPath; rlock.lock(); try { updatedPath = spath; } finally { rlock.unlock(); } return updatedPath; } public void initial() { wLock.lock(); try { /*=====balabala=====*/ } finally { wLock.unlock(); } }
由于在笔者的算法里,对每个请求都需要利用迪杰斯特拉求得的最短路径数组来获得最短路径,而该数组又仅在增加横向电梯的时候需要更改,使用synchronized修饰显然会使请求之间相互阻碍,因此采用读写锁机制控制。读时不能写,写时不能读,平时可以同时读,提高效率。
HW 5:
HW 6:
class Building extends Thread { private ArrayList<Elevator> elevators; /*=====利用count计数实现均分=====*/ if (request instanceof PersonRequest) { elevators.get(count).addOutRequest((PersonRequest) request); count = (count + 1) % num; } }
HW 7:
public class Schedule extends Thread { private static final Schedule SCHEDULE = new Schedule(); public static Schedule getInstance() { return SCHEDULE; } public void initial() { /*=====对调度器各项数据进行初始化=====*/ } public synchronized void addRequest(RequestTable requestTable) { this.waitQueue.addRequest(requestTable); notifyAll(); } }
HW 5
HW 6
HW 7
UML协作图:
HW 5:
HW 6:
HW 7:
/*===============横向电梯轮询bug时拿取请求的逻辑==================*/ public synchronized RequestTable getRowElevatorRequest(ArrayList<RequestTable> inElevator,char curBuilding, int capacity,char floorDes, int switchInfo) { if (requests.isEmpty()) { return null; } Iterator<RequestTable> iterator = requests.iterator(); while (iterator.hasNext()) { RequestTable request = iterator.next(); RequestTable tmp = request.readFirst(); int check = (1 << (tmp.getFromBuilding() - 'A')) | (1 << (tmp.getToBuilding() - 'A')); /*=====判断条件:可达 + 楼座符合 + 容量没超=====*/ if (tmp.getFromBuilding() == curBuilding && inElevator.size() < capacity && ((check & switchInfo) == check)) { iterator.remove(); notifyAll(); return request; } } return null; }
与第一单元表达式作业相比不同的是,第二单元的电梯作业为对简单场景的模拟,侧重点在多线程的安全问题,电梯的运行逻辑非常简单,以至于基本不会出现运行逻辑正确性的问题。本单元的测试应当着重于有关线程安全的调度策略,即重点构造数据针对可能出现的死锁和轮询进行测试。
测试策略以手搓构造数据为主、自动化测评为辅。笔者在这一单元第一次尝试写测评机,由于对Python代码十分不熟悉,因此写得磕磕绊绊。利用自动化测评,成功找到了同组一个数组越界的bug,推测是在增加电梯时对内部队列的初始化问题。
测评机逻辑:
OO真是太好玩辣!
实验的代码太香辣!
“故天将降大任于是人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为,所以动心忍性,曾益其所不能。” ——《孟子》
艰辛的第二单元多线程编程已经过去,不应该停滞不前,向前看吧,努力认真完成后面的作业,以这句话与诸君共勉。