OO_Unit2_分析与总结
1. 前言
第二单元学习以 “多线程” 为主题开展,而在 “多线程” 中最重要的就是处理好线程安全问题。因此每次作业中除去性能优化,需要合理地安排 同步块 和 锁 的设置。
2. 各次作业分析
2.1 hw_5
2.1.1 同步块的设置和锁的选择
- 在此次作业中由于整体架构比较简单,我选择设置了八个线程。即六个
Elevator
电梯线程,一个InputHandler
输入线程,一个Scheduler
调度器线程。事实上第一次作业并不需要调度器线程,只是为了后续迭代方柏霓提前构造好了框架和Elevator
类对Scheduler
类的调用接口,这样后续迭代时只需要更改Scheduler
类中的调度策略即可,不需要增改其他内容。有关线程设置方法使用的是实现了Runnable
接口,因为这种实现方法可拓展性更好。除此之外有关锁的设置,在第一次作业中仅有RequestSet
这个类是作为共享数据池来使用的(例如InputHandler
将数据打入一个new RequestSet
,然后Scheduler
从这个RequestSet
中取得Request
)。因此为了方便,我对RequestSet
中的所有方法都上了锁,使用的是关键字synchronized
。这样由于每次只能有一个线程能获得锁,其他线程会被阻塞,由此确保了InputHandler
与Scheduler
同时放入和取出Request
的安全性问题。
PS:由输入线程放入请求,电梯线程处理请求的模型可以抽象成一个对生产者无约束的生产者-消费者模型。
2.1.2 架构
Elevator
Class
run() \\电梯运行,包含END、MOVE、OPEN、REVERSE动作
openAndClose() \\电梯开门到关门,包含人员进出的一系列动作
move() \\电梯移动一层
Adivice
Enum
END,
MOVE,
REVERSE,
OPEN
InputHandler
Class
run() \\读入数据
Person
Class
getId() \\获取人的ID
getFromFloor() \\获取人的等待楼层
getToFloor() \\获取人的目标楼层
RequestSet
Class
addRequestToWait() \\添加请求到等待分配的请求集合
addRequestToElevator() \\添加请求到电梯运行的请求集合
getOneRequestAndRemoveWait() \\从等待分配请求集合中获得一个请求并移除
getOneRequestAndRemoveElevator() \\从电梯运行请求集合中获得一个请求并移除
setEnd() \\设置集合结束输入状态
isEnd() \\查看集合是否结束
isEmpty() \\查看集合是否为空
Contains() \\查看集合是否包含key为特定值的元素
Scheduler
Class
run() \\调度器分配请求
Strategy
Class
getAdvice() \\获取策略
hasReqInOriginDirection() \\查看电梯当前方向前方楼层是都有请求
canOpenForIn() \\查看电梯是否能携带新乘客
canOpenForOut() \\查看电梯是否需要开门放行
2.1.3 一些细节
当我想要从一个集合获得请求但实际上这个集合空且未结束应该等待,如下
if (RequestSet.isEmpty() && !this.isEnd) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
在第一次作业中这样就达成了阻塞效果不会轮询。
2.1.4 代价
主要复杂度来自于openAndClose
方法和Strategy
类,在第一次作业中这是符合实际情况的。
2.1.5 UML类图
2.1.6 UML协作图
2.1.7 优化
仅电梯运行策略可做优化
选择的是LOOK算法
2.2 hw_6
2.2.1 同步块的选择和锁的设置
基本一致。但此次由于有reset
指令的加入,我将原本的Elevator
类拆成了Elevator
属性类和ElevatorThread
运行线程类,其中对Elevator
类中的方法均上锁,以确保共享对象数据安全。除此之外,由于此次电梯调度策略选取 影子电梯 策略,需要在ElevatorThread
类中deepclone
一些属性,我对此方法也上了一把锁,以确保复制的正确性。
2.2.2 架构(仅添加和修改部分)
Advice
END,
MOVE,
REVERSE,
RESET,
OPEN,
WAIT
Elevator
resetElevator() \\重置电梯
getId() \\获得电梯ID
getSpeed() \\获得电梯速度
getCapacity() \\获得电梯容量
deepClone() \\深克隆
ElevatorThread
reset() \\重置并将已到乘客送出,未到乘客重新发入等待队列
ProcessRequest
interface
isEmpty() \\请求是否为空
deepClone() \\深克隆
PersonRequest
addPersonRequest() \\添加一条人员请求
getPersonRequests() \\获得楼层所有人员请求
implement ProcessRequest
ResetRequest
addResetRequest() \\添加一条重置指令
removeResetRequest() \\删除一条重置指令
getCapacity() \\获得指令中的目标容量
getSpeed() \\获得指令中的目标速度
contains() \\查看某ID电梯是否有未完成的reset指令
implement ProcessRequest
RequestSet
getOnePersonRequestAndRemove() \\获得一条人员请求并从等待队列移除
setReset() \\将电梯请求队列状态设置为正在重置
isReset() \\电梯是否正在重置
hasReset() \\电梯是否含有未完成重置指令
deepClone() \\深克隆
myWait() \\仅wait不做其他事
Scheduler
resetNum() \\获得正在重置的电梯数量
notHasReset() \\是否所有电梯都没有未完成重置指令
notIsReset() \\是否所有电梯都不在重置
Strategy
containReset() \\是否等待队列中有该电梯的重置指令
ShadowElevator
一个加入一条待评估人员指令后锁死电梯队列并自主运行的电梯,运行代码基本同ElevatorThread
,所有sleep
处更改为计数增加,最后获得该计数并进行比较,六个电梯用时最少者获得该请求。
2.2.3 一些细节
重置指令由InputHandler
直接输入到电梯线程,避免调度耗时和与人员请求调度冲突。
如果一个电梯仅有一个人还在队列且调度器队列已经空时获得一条重置指令则需吐回,但按照第一次作业的实现此时调度器已经setEnd并且kill了自己,因此设置了notHasReset
和notIsReset
来增加对setEnd的限制以防止此情况。
除此之外,假如有五个电梯在重置,此时输入大量人员请求,都会被分配到未重置的电梯导致超时。因此设置resetNum
<= 4的请求调度限制条件以防止上述情况发生。
2.2.4 代价
Scheduler
与ShadowElevator
中部分方法复杂度过高,但符合实际情况。
2.2.5 UML类图
2.2.6 UML协作图
2.2.7 优化
采用影子电梯方法进行策略优化。
2.2.8 有关bug
其一是影子电梯不安全导致的问题,后续通过加限制条件在运行上体现为没有问题,但实际上仍有可能出现安全性问题,故第三次作业更换为随机策略。
其二是有五个电梯在重置,此时输入大量人员请求,都会被分配到未重置的电梯导致超时。因此设置resetNum
<= 4的请求调度限制条件以防止上述情况发生。
2.3 hw_7
2.3.1 同步块的选择和锁的设置
此次作业除去之前的锁还增加了ReentrantLock
锁,目的是训练学过的内容,同时也能确保换乘的安全。
2.3.2 架构
Advice
END,
MOVE,
REVERSE,
NORMALRESET,
OPEN,
WAITFORABDICATE,
WAIT,
DCRESET,
ABDICATE
ElevatorThread
dcReset() \\重置为双轿厢电梯
abdicate() \\给双轿厢电梯的另一个电梯让位
Elevator
getLower() \\获得最低楼层
getUpper() \\获得最高楼层
getTransferFloor() \\获得换乘楼层
dcReset() \\双轿厢重置
getType() \\获得是A电梯还是B电梯
Occupy
enum
OCCUPYA,
OCCUPYB,
EMPTY
dcResetRequest
Class
getElevator() \\获得重置电梯
getTransferFloor() \\获得换乘楼层
getCapacity() \\获得新容量
getSpeed() \\获得新速度
implement ProcessRequest
TransferFloor
Class
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class TransferFloor {
final Lock lock = new ReentrantLock(true);
final Condition condition = lock.newCondition();
Occupy occupy;
public TransferFloor(Occupy occupy) {
this.occupy = occupy;
}
public void tryAccess(Occupy newOccupy) throws InterruptedException {
lock.lock();
try {
while (this.occupy != Occupy.EMPTY) {
condition.await();
}
this.occupy = newOccupy;
} finally {
lock.unlock();
}
}
public void leave() {
lock.lock();
try {
this.occupy = Occupy.EMPTY;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
2.3.3 一些细节
如果我在换乘,那么调度器不应该将电梯状态设置为end,因此增加了isTansfer布尔量来进行限制判断。除此之外,为了防止A电梯未离开换乘楼层B电梯就进入的情况,设置了TansferFloor
类,使用ReentrantLock
和condition.await
来进行阻塞和安全性保证。
2.3.4 代价
此次迭代代价不尽人意,此后的学习中还应该优化自身代码风格。
2.3.5 UML类图
2.3.6 UML协作图
2.3.7 优化
此次未做优化
2.3.8 bug
bug同hw6(删掉影子电梯又错了)
2.4 双轿厢碰撞问题
如2.3中所说,设置TransferFloor
类,通过双轿厢共享该类,上锁并进行阻塞来确保双轿厢不会碰撞
代码如下
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class TransferFloor {
final Lock lock = new ReentrantLock(true);
final Condition condition = lock.newCondition();
Occupy occupy;
public TransferFloor(Occupy occupy) {
this.occupy = occupy;
}
public void tryAccess(Occupy newOccupy) throws InterruptedException {
lock.lock();
try {
while (this.occupy != Occupy.EMPTY) {
condition.await();
}
this.occupy = newOccupy;
} finally {
lock.unlock();
}
}
public void leave() {
lock.lock();
try {
this.occupy = Occupy.EMPTY;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
2.5 稳定和易变的内容
在三次作业中,输入进程的处理,单电梯内部的策略是很稳定的。
易变的部分主要体现在调度的策略上面。
第六七次作业,需要实现一个调度策略。如果是使用影子电梯的话,在第六次作业到第七次作业的迭代中,会展现出很大的变化。因为电梯种类的增加和分离请求的出现,会导致困难的同步问题。由于时间不足,最终没有继续实现。
2.6 debug方法
2.6.1 线程安全问题
就直接排查的,因为共享数据不多,只用美枚举所有可能出现问题的地方即可,而且根据wa情况其实很容易锁定在一小块代码块中。
2.6.2 TLE
RTLE一般是策略问题或者是wait未被唤醒,这个很好判断。
CTLE一般都是轮询或者死锁导致的可以使用IntelliJ Profiler
方式运行,在出现明显CPU使用增长后继续运行一段时间,然后终止程序并查看结果,可以查看到各个线程和方法的CPU用时,从而很容易定位到问题所在。
2.7 心得与体会
多线程编程是一个挑战,容易出现各种线程安全问题,但通过不断学习和实践,我已经能够更好地识别和解决这些问题了。确保在多线程共享对象时考虑线程安全是一个很好的习惯,有助于预防潜在的问题。
另外,我对层次化设计的理解也更加深刻。将程序分为不同的层次,确保它们相互作用但又互不干扰,可以提高代码的可维护性和可扩展性。我的实践经验也展示了这种设计方法的实际价值,特别是在修改和迭代过程中的灵活性。例如在第七次作业中紧急将调度策略由影子电梯改成随机分配,需要更改的部分就仅有调度策略,其他部分都不用修改。继续贯彻低耦合、高内聚的设计原则将有助于我构建更清晰、易于维护的程序。