OO第二单元总结
OO第二单元总结
同步块与锁
在第一次作业的第一个版本中,由于最开始思路不清晰,我给一个电梯开了3个线程,分别是计算线程、电梯运行线程和乘客上下线程,三个线程之间需要相互通信,共享数据杂糅导致我第一个版本的电梯对4,5个共享对象分别加了锁,形成了很多层锁的嵌套。而为了解决死锁的问题,我希望每个synchronized代码块最外层的锁是相同的,因此又加了很多锁,导致最后代码里的锁非常混乱,线程安全也依旧没法保障。最终我放弃了这个版本。但通过总结这个版本失败的原因,我发现加锁的对象应该越少越好。线程安全不应该依靠对各种情况分类讨论打补丁,而应该尽量让共享数据更少,数据之间的关系更清晰。
每个栈内存都是由线程独立占有,因此从栈中分配内存不需要加锁;多个线程都有可能同时从堆中申请内存,因此在堆中申请内存需要加锁。Java中如果是基本数据类型,且如果是在方法中声明,则存储在栈中,其它情况都是在堆中(比方说类的成员变量就在堆中)。此外,由于在第一个版本加锁的过程中我遇到了很多奇葩的情况,比如A对象里面有一个B对象的属性,线程1对A对象加锁的方法和线程2对B对象加锁的方法并不会冲突。这是因为A对象里面的B对象属性只是引用,B并不保存在A的地址区域中,因此对A加锁不会影响B。
在解决死锁问题时,我当时采用的是规定每一层锁的顺序,保证最外层的锁相同的方法。比如线程1中有
synchronized(a) {
synchronized(b) {
//
}
}
线程2中有
synchronized(b) {
synchronized(a) {
//
}
}
就会导致死锁,我的解决方案是保证对外层的锁都是a,即把线程2中改成:
synchronized(a) {
synchronized(b) {
synchronized(a) { }
}
}
后来上课的时候介绍了ReentrantLock,我发现这个问题可以通过ReentrantLock解决,不需要分别对a和b加锁。
调度器设计与架构模式
第一次作业
第一次作业的第二个版本,为了减少共享对象,我将所有需要共享的数据放在一个Information类里,其中包括该电梯的等候列表、电梯上的人、电梯当前所在的楼层和电梯的运行方向等。所有的计算都在Information的方法中定义,这样只需要对Information的对象加锁,保证了线程安全。因此第二个版本只有五的电梯线程和一个输入线程。电梯线程里只有电梯开关门和上下行的方法,其它计算和乘客进程均通过调用Information里的方法完成。
采用了生产者—消费者模式,其中输入线程是生产者,电梯线程是消费者。
电梯运行策略
电梯里没人
-
设电梯楼层为curFloor,乘客初始楼层为fromFloor,先在电梯当前所在楼层到原本运行的方向顶点的区间上查找。如果方向向上,则寻找fromFloor属于[curFloor, 10];如果方向向下,则寻找fromFloor属于[1, curFloor].
-
设电梯方向为eleDir,乘客方向为pasDir,遍历候乘表,寻找pasDir和eleDir相同的乘客。(离得越近越好)
-
如果没找到,则找pasDir和eleDir相反的乘客。(离得越远越好)
-
如果还没找到,则表示电梯当前区间没有乘客。
-
-
更换电梯运行方向,改变查找区间
-
重复上面前两步操作。
-
如果还没找到,则说明没有等候的乘客,需要wait输入
-
电梯里有人
-
前提:电梯里所有人的方向与电梯运行方向相同,捎带乘客的方向也与电梯运行方向相同,捎带乘客的出发楼层必须在电梯运行方向上。
-
寻找电梯里面乘客toFloor和捎带乘客fromFloor的最近处
UML类图
UML协作图
bug分析
这一次作业由于前后写了三个版本,耗时太长,最后没怎么测试,过了测评机就没管。结果有一个地方没有考虑限乘6人的条件,导致最后强测错了好多。以后一定会自己多做测试!
第二次作业
第二次作业在第一次的基础上增加了调度器线程,调度器线程和输入线程之间的共享数据是requestQueue,和电梯之间的共享数据是information,它既是输入线程的消费者,也是电梯线程的生产者。
输出的类采用单例模式,解决输出的安全问题。
电梯运行策略
-
横向电梯运行
-
由于电梯沿环路运行,所以接反方向的乘客虽然可能浪费一定的电梯资源,但在某些情况下也可以节省时间
-
因此本电梯允许顺时针和逆时针方向的乘客都可以上电梯,电梯获取nestDes的时候先判断电梯里的所有人是否有人在当前层下,再判断外面是否有人上
-
如果当前楼层不需要停,则选择顺指针走一座,或逆时针走一座。当电梯里有人时,朝最早到达乘客的目的地方向运行。当电梯里没人时,去接最早等待的乘客。
-
-
纵向电梯分配
-
如果该电梯里的人和等候的人超过6人,则不再接受新的乘客
-
只接受在当前运行方向,且当前区间的乘客
-
如果所有电梯都不接受,按顺序选择一个电梯接受
-
-
横向电梯分配
-
如果该电梯里的人和等候的人超过6人,则不再接受新的乘客
-
只接受同方向的乘客
-
如果所有电梯都不接受,按顺序选择一个电梯
-
UML类图
UML协作图
bug分析
这次互测中没有出现bug,也没有找到别人的bug. 自己测试采用手动构造数据的方式,分别测试单个电梯的行为和多个电梯之间协作的行为。
第三次作业
本次作业采用了流水线的设计模式,在输入请求时就将一个乘客分为3个或以下procedure,每个procedure与上次作业passenger的属性和方法都相同,而passenger仅有ArrayList<procedure>
一个属性,且它所有的方法都调用procedures[0]的对应方法。每当乘客出电梯的时候,删除procedures[0]并判断这个乘客的procedures是否为空,若不为空,则重新将passenger加入调度器中进行分配。
电梯运行策略
-
不同电梯之间分配策略:为使整个程序的时间更短,应保证所有电梯在任何时刻等候列表和电梯里的人总数大致相同。因此我的调度器根据剩余可承载人数分配请求,这也同样适用于运行速度不同的电梯。
-
不同楼层之间分配策略:先遍历从起点到终点之间的所有楼层,如果有多个符合条件,则选择承载能力更大的楼层;再遍历剩余区间的楼层,选择最近的符合要求的楼层。
UML类图
UML协作图
bug分析
在自己测试的时候通过手动构造测试数据,分别测试单个电梯运行、多个电梯分配、任务拆分和进程结束。强测出现一个bug,原因是出现了死锁,一个线程里A套B,另一个线程里B套A
分析线程安全问题可以将所有带锁的方法和代码块列出来,分析他们的嵌套关系。重点看A对象里面包含B对象,且B对象里包含A对象的情况。
心得体会
这一单元的三次作业中,我对第一次作业投入的时间最长,一共写了三个版本。前两个版本都因为给每个电梯开了三个线程而且共享数据划分的太乱,导致出现了很多线程安全问题,每个线程的run方法里几乎全都是synchronized代码块,而且有很多锁的多层嵌套。当时我花了一整天,对各种可能的情况分类讨论,但最后还是无法保证线程安全。我上网查了分析线程安全的方法,将所有线程需要使用的共享数据列出来,看它们是否能构成一个回路。但最终我发现线程安全不应该建立在对各种情况分类讨论的基础上,而应该优化架构、减少使用共享数据、避免出现线程安全问题的可能。最后我将所有需要共享的数据放到一个类里,使得代码思路变得清晰,彻底杜绝了线程安全的问题。