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()方法中需要对WaitQueueprocessingQueue上锁。

电梯运行线程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类图
      image
    • UML协作图
      • 主类
        image
      • 调度器线程
        image

(4) 分析自己程序的bug

由于架构设立良好,本次作业未出现线程安全、死锁之类的bug。再次感谢第三次课上实验🙏

  • 第一次作业
    一开始写了一个傻瓜电梯,有多傻呢?一人一趟……最大的bug是慢。
    后来:
    image
    好家伙赶紧重写,用我能想到的所有捎带策略,终于完成了一个还算快的可捎带电梯。
    经过后两次作业的弱智调度器的测试发现,我这优秀的捎带策略在性能表现中起到了何其关键的作用……
  • 第二次作业
    没啥bug,基本都是小错,不说了。
  • 第三次作业
    此次作业致敬了前利物浦门神卡利乌斯。。。发生了超级超级巨大巨大的失误。。。都赖本泽马。。。
    失球发生在电梯运行线程ProcessfullNum()方法,用于获取不同型号电梯的乘客数量上限。。。
    image
    嗯。。。千里之堤。。。

(5) 分析自己发现别人程序bug所采用的策略

本单元的互测阶段,我未发现别人程序的任何bug。经过分析,主要有以下3种可能:

  • 我在A房间,别人没bug
  • 我用目测法,看不出bug
  • 我没进互测,发现个🔨

(6) 心得体会

  • 关于线程安全
    • 🔒死了,🔑吞了。
    • 注:前半句不可写作“死🔒了”,否则后果自负。
  • 关于层次化设计
    • If I have seen further, it is by standing on the shoulders of Giants. —— Isaac Newton
    • 上面这句话的中文翻译是:感谢第三次课上实验🙏
posted @ 2021-04-27 21:59  Mayday777  阅读(86)  评论(1编辑  收藏  举报