OO第二单元总结博客
前排感谢第五周的第三次课上实验🙏为本单元的架构设计打下了良好的物质基础。
本单元作业主要由以下6个类构成:
- 3个线程类
- 输入线程
InputThread
- 调度器线程
Scheduler
- 电梯运行线程
Process
- 输入线程
- 2个共享对象类
- 总等候队列
WaitQueue
- 电梯候乘表
processingQueue
- 总等候队列
- 1个主类
- 主类
MainClass
- 主类
(1) 总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句之间的关系
- 据完全统计,三次作业中没有用到任何同步方法,所有出现
synchronized
的地方,均为同步代码块。这样设计的优势是,在确保线程安全的前提下,尽可能地缩小被锁的共享对象所控制的代码范围,加快不同线程对同一共享对象的锁的使用频率,最大限度确保多线程思想、“并行”思想的落实。 - 三次作业中同步块的设置和锁的选择基本一致,以下按照三个线程类——输入线程
InputThread
、调度器线程Scheduler
、电梯运行线程Process
分别说明。
输入线程InputThread
输入线程主要负责请求的输入,具体而言,就是读入请求+放入总等候队列WaitQueue
。因此,在run()
方法的循环读入过程中,主要对WaitQueue
上锁。此外,若请求为加电梯请求,则会在WaitQueue
同步块内部再对电梯候乘表processingQueue
上锁,通过这一共享对象的属性来告知调度器线程Scheduler
:此电梯可调用。
调度器线程Scheduler
调度器线程主要负责请求的分配,具体而言,就是从总等候队列WaitQueue
中取出请求+分配请求+将请求放入电梯候乘表processingQueue
。因此,在run()
方法中需要对WaitQueue
和processingQueue
上锁。
电梯运行线程Process
电梯运行线程主要负责请求的执行,具体而言,就是根据捎带策略从电梯候乘表processingQueue
中取出请求+执行请求。因此,在run()
方法中主要对电梯候乘表processingQueue
上锁。此外,若所有请求全部读入完毕(即waitQueue.isEnd() == true
)、所有请求分配完毕(即waitQueue.noWaiting() == true
)且当前电梯候乘表processingQueue
中的请求全部执行完毕,则结束此线程。因此,还需要在processingQueue
同步块内部再对WaitQueue
上锁。
(2) 总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
- 三次作业中,均采用集中式调度机制,调度器均作为独立的线程出现。调度器主要负责:
- 若总等候队列
WaitQueue
不为空,则从总等候队列WaitQueue
中取出请求 - 核心调度算法——计算请求应该被分配给哪台电梯(在二、三次作业中出现)
- 将请求放入对应电梯的电梯候乘表
processingQueue
中,供电梯运行线程Process
执行请求。
- 若总等候队列
- 下面具体谈谈核心调度算法
- 第一次作业
- 因为就一台电梯……所以……这调度器,不提也罢。
- 第二次作业
- 根据电梯候乘表
processingQueue
中的请求数量分配,优先将请求分配给电梯候乘表最“短”的电梯。(够傻瓜吧?) - 但是!但是!每个请求开始执行时,请求会从电梯候乘表中取出,此时电梯正在执行任务,并非空闲状态。由于一般的for循环具有如下特点:先将第0项(的下标)赋值给变量
lowestIndex
,之后从第1项开始循环查找,若没有请求数量更小的,就不更新lowestIndex
。因此,索引值越小的电梯越容易被分配新请求,索引值较大的电梯容易畅游太平洋,尤其当请求数量较少时(即沿时间轴来看,请求出现得比较“稀疏”时)。 - 于是,想到如下对策:保存每次被分配请求的电梯索引值
index
,每次分配新请求开始前给变量lowestIndex
赋初值为index
的下一项,以求将请求分配得尽可能均匀。具体代码如下所示:private void chooseOneElevator(int numOfElevators) { int lowestIndex = (index + 1) % numOfElevators; int lowestNumOfRequests = processingQueues.get(lowestIndex).size(); for (int i = 0; i < numOfElevators; i++) { synchronized (processingQueues.get(i)) { if (processingQueues.get(i).size() < lowestNumOfRequests) { lowestNumOfRequests = processingQueues.get(i).size(); lowestIndex = i; } } } index = lowestIndex; }
- 是不是有点眼熟?没错,这一调度原则的精髓借鉴了OS课中的内存分配算法——循环首次适应算法(Next Fit)。
- 根据电梯候乘表
- 第三次作业
- 本次作业用了双层列表
private ArrayList<ArrayList<Integer>> choicesForEachType;
保存不同类型的电梯的索引值。由于没有实现换成机制,每次分配请求时,根据请求的起始楼层、到达楼层,在可直达的前提下优先选择运行速度最快的电梯类型。之后,在储存对应类型电梯的索引值的列表中,选择“下一个电梯”(此处同第二次作业的调度器,用private ArrayList<Integer> lastIndex;
存储上一次调用该类型电梯时的索引值)。具体代码如下所示:private void chooseOneElevator(PersonRequest request) { if (isC(request)) { chooseNext('C'); } else if (isB(request)) { chooseNext('B'); } else { chooseNext('A'); } } private void chooseNext(char type) { ArrayList<Integer> list = choicesForEachType.get(type - 'A'); int lowestIndexInList = (lastIndex.get(type - 'A') + 1) % list.size(); synchronized (processingQueues) { int lowestNumOfRequests = processingQueues.get(list.get(lowestIndexInList)).size(); for (int i = 0; i < list.size(); i++) { if (processingQueues.get(list.get(i)).size() < lowestNumOfRequests) { lowestNumOfRequests = processingQueues.get(list.get(i)).size(); lowestIndexInList = i; } } } index = list.get(lowestIndexInList); lastIndex.set(type - 'A', lowestIndexInList); }
- 由于没有实现换乘机制,所以强测遇到一个特殊数据点
2021_homework_7_strong_data_14
——乘客请求均为1/2/3层、18/19/20层之间反复横跳,额外添加了一台A类电梯、一台B类电梯——此时我的调度器会把所有请求都分配给唯一一台C类电梯,导致性能得分0/20。。。
- 本次作业用了双层列表
- 第一次作业
(3) 从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性
- 我的电梯实现了功能设计与性能设计的完美平衡——功能实现稳定可靠,性能表现十分优异。换句话说,菜得均匀。
- 运行算法比较完善,但可扩展性稍弱。
- 调度算法相对简陋,而可扩展性较强。
- 具体的架构展示如以下3张图所示:
- UML类图
- UML协作图
- 主类
- 调度器线程
- 主类
- UML类图
(4) 分析自己程序的bug
由于架构设立良好,本次作业未出现线程安全、死锁之类的bug。再次感谢第三次课上实验🙏
- 第一次作业
一开始写了一个傻瓜电梯,有多傻呢?一人一趟……最大的bug是慢。
后来:
好家伙赶紧重写,用我能想到的所有捎带策略,终于完成了一个还算快的可捎带电梯。
经过后两次作业的弱智调度器的测试发现,我这优秀的捎带策略在性能表现中起到了何其关键的作用…… - 第二次作业
没啥bug,基本都是小错,不说了。 - 第三次作业
此次作业致敬了前利物浦门神卡利乌斯。。。发生了超级超级巨大巨大的失误。。。都赖本泽马。。。
失球发生在电梯运行线程Process
的fullNum()
方法,用于获取不同型号电梯的乘客数量上限。。。
嗯。。。千里之堤。。。
(5) 分析自己发现别人程序bug所采用的策略
本单元的互测阶段,我未发现别人程序的任何bug。经过分析,主要有以下3种可能:
- 我在A房间,别人没bug
- 我用目测法,看不出bug
- 我没进互测,发现个🔨
(6) 心得体会
- 关于线程安全
- 🔒死了,🔑吞了。
- 注:前半句不可写作“死🔒了”,否则后果自负。
- 关于层次化设计
- If I have seen further, it is by standing on the shoulders of Giants. —— Isaac Newton
- 上面这句话的中文翻译是:感谢第三次课上实验🙏