OO第二单元电梯作业总结

OO第二单元电梯作业总结

一、总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系

三次作业当中用到的锁全都是synchronized锁,第一次作业几乎所有的锁都是对方法加了synchronized关键字,在第二次第三次作业中都是直接锁一个对象,不再对方法加synchronized关键字,避免一个线程在调用完共享对象的一个方法还需要继续调用的时候唤醒其他线程

1.1第一次作业

第一次作业只写了两个线程:电梯进程和输入进程,并没有把调度器单独作为一个进程。只是一个很简单的生产者—消费者模式。

所以这次只是把”盘子“锁起来了,也就是下面代码中的scheduler,虽然名字叫调度器但是事实上它只是个“盘子”,并且我把它设置成了线程安全类,除了scheduler类和电梯的run()方法中用到了synchronized锁,其他地方都没有用到。

//这是电梯的run()方法中的一部分,用到了synchronizied
//锁住scheduler然后调用方法对等待队列的状态进行判断,从而影响电梯的状态
if (direction == 0) { //如果处于静止状态
	synchronized (scheduler) {
    	while (scheduler.getRequestNum() == 0 && passengers.size() == 0) {
       		//如果暂时没有需求并且电梯没有人,等待
        	if (scheduler.isEnd()) {
        		return;
        	}
        	try {
        		scheduler.wait();
        	} catch (InterruptedException e) {
        		e.printStackTrace();
        	}
        }
    }
    //需求来了,得到运行方向
    direction = scheduler.getDirection(floor);
}

1.2第二次作业

第二次作业考虑到实现的复杂度和以后面对第三次作业的可拓展性,参照实验课上的代码进行了重构,把调度器改成了进程,所以第二次作业共有三个线程:输入、电梯、调度器

这次用到锁的地方就比较多了,因为共享对象增多了,我们一个线程一个线程来分析

输入线程

  • waitQueue(总等待队列,与调度器共享的对象),锁住等待队列避免多个线程对等待队列中乘客的改变导致不可知的错误,锁住等待队列后向其中加入乘客请求
  • elevators(电梯队列,与调度器共享的对象),锁住电梯队列避免调度器在进行乘客分配时突然加入新电梯而导致分配出错,锁住电梯队列后向其中加入新增电梯

调度器线程

  • waitQueue(总等待队列,与输入线程共享的对象),锁住等待队列避免多个线程对等待队列中乘客的改变导致不可知的错误,锁住等待队列后将队列进行调度分配到各个电梯中,同时调用waitQueue的方法判断调度器线程是否结束
  • elevators(电梯队列,与输入线程共享的对象),锁住电梯队列避免调度器在进行乘客分配时突然加入新电梯而导致分配出错,锁住电梯队列从而计算电梯中乘客的平均数从而分配乘客
  • eachElevator.getMissions(每个电梯的等待队列,与电梯线程共享的对象),锁住电梯的等待队列避免电梯和调度器同时对等待队列的乘客进行操作导致错误,锁住等待队列后向其中分配乘客,同时调用相关方法进行乘客的调度

电梯线程

  • waitQueue(电梯自己的等待队列,与调度器共享的对象),锁住电梯的等待队列避免电梯和调度器同时对等待队列的乘客进行操作导致错误,通过等待队列判断目前电梯的运行状态并且进行乘客的“接待”,同时调用相关的方法判断电梯线程是否结束

1.3第三次作业

第三次作业改动的部分并不多,难度并没有我想象的大,总的架构没有改变,所以同步块的设置和锁的选择与第二次作业相同

输入线程

  • waitQueue(总等待队列,与调度器共享的对象),锁住等待队列避免多个线程对等待队列中乘客的改变导致不可知的错误,锁住等待队列后向其中加入乘客请求
  • elevators(电梯队列,与调度器共享的对象),锁住电梯队列避免调度器在进行乘客分配时突然加入新电梯而导致分配出错,锁住电梯队列后向其中加入新增电梯

调度器线程

  • waitQueue(总等待队列,与输入线程共享的对象),锁住等待队列避免多个线程对等待队列中乘客的改变导致不可知的错误,锁住等待队列后将队列进行调度分配到各个电梯中,同时调用waitQueue的方法判断调度器线程是否结束
  • elevators(电梯队列,与输入线程共享的对象),锁住电梯队列避免调度器在进行乘客分配时突然加入新电梯而导致分配出错,锁住电梯队列从而计算电梯中乘客的平均数从而分配乘客
  • eachElevator.getMissions(每个电梯的等待队列,与电梯线程共享的对象),锁住电梯的等待队列避免电梯和调度器同时对等待队列的乘客进行操作导致错误,锁住等待队列后向其中分配乘客,同时调用相关方法进行乘客的调度

电梯线程

  • waitQueue(电梯自己的等待队列,与调度器共享的对象),锁住电梯的等待队列避免电梯和调度器同时对等待队列的乘客进行操作导致错误,通过等待队列判断目前电梯的运行状态并且进行乘客的“接待”,同时调用相关的方法判断电梯线程是否结束

二、总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互

2.1第一次作业

第一次作业并没有把调度器单独作为一个线程,它只是生产者—消费者模式中的“盘子”,并且被我设计为线程安全类,感觉没什么可说的,不再赘述

2.2第二次作业

第二次作业开始调度器单独作为一个线程,根据我的设计,调度器与输入线程共享总等待队列的对象,调度器与每个电梯共享电梯自己的等待队列的对象,即调度器是输入线程和电梯线程之间的桥梁,从输入线程接收到乘客的需求,然后分配到各个电梯进行处理,这样一来,电梯就只需要根据自己的等待队列进行运行,调度器的任务就是根据总等待队列和每个电梯的等待队列综合分配乘客到各个电梯,具体的调度方法也很简单很暴力:算出所有电梯的等待队列的人数和总等待队列的人数和,进而算出平均人数,然后把总等待队列中的乘客分配到每个电梯中使得每个电梯中的等待队列的人数达到平均人数

2.3第三次作业

第三次作业调度器的任务和第二次相同,分配总等待队列中的乘客到各个电梯的等待队列中,但是这次作业似乎不能像第二次作业那样暴力分配了,调度器的复杂度好像更高了。不过我进行了一些更改,新增了一个Person类,它拥有Personrequest类的所有属性以及一个新增属性—type,在Person类的构造方法中根据乘客的起始楼层和到达楼层判断乘客应该上哪种类型的电梯,这样一来调度器就又可以像第二次作业那样“暴力分配”了,至于换乘,本来是有设计的,但是怎么设计都不如不换乘的效果好,所以索性不换乘了。。。

三、从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性

  • 第一次
  • 第二次
  • 第三次

基本架构如上所示

第三次作业的功能能够很好的实现,性能得分也比较理想,但是由于本次作业的电梯类型设置,并没有找到很好的换乘模式,使得在随机生成数据点的情况下,性能优于不换乘的电梯,所以提交的作业为不换乘的版本。 虽然在这次作业能够保证程序的正确性,也能够得到一个比较理想的性能得分。但是也正是因为这种面对作业的架构设计(因为是最后一次电梯作业了,不会再拓展了orz),使得程序的可扩展性很差。事实上,在架构合理的情况下,程序应当具有很好的可扩展性,比如A电梯改成只能到达偶数层,或者更进一步,任意提供三种电梯能够到达的楼层,只要这些楼层涵盖了1-20层的所有楼层,仍然可以实现电梯的功能,但是这些在我目前的架构下显然是无法实现的,连功能都尚且无法实现,就更不用说性能了

四、分析自己程序的bug

第一次和第三次公测和互测都没有问题,第三次的互测也没有问题

第三次公测出现了RTLE的问题,其实这并不是bug,出现问题的原因在于数据比较极端,180s冒出来了一堆乘客。。。线程的不确定性加上调度器的“暴力分配”导致电梯的等待队列分配并不合理,导致电梯运行时间超过了210s。debug也很简单,原封不动提交了一遍就过了,甚至比本地跑的还快,多线程的不确定性真奇妙

死锁问题确实遇到过,是在第二次作业,当时刚完成重构,对于临界区什么的还不是很懂,问题在于临界区太大了,很多不该放在临界区的操作都放进去了,导致死锁,经过修改,只把必要放进临界区的操作放进去,就不再出现死锁了,并且是在中测截止前解决了这个问题

五、分析自己发现别人程序bug所采用的策略

  • 测试策略及有效性:评测机不会写,但是数据随机生成还是比较简单的,另外手工构造一些简答的数据,有效性靠用眼睛看来保证,但是这样实在没有效率,第一次好运找到了一个,第二次第三次真的找不到。。。
  • 线程安全的问题基本靠看代码,但是代码太多了看不过来,也找不到bug,每次都是看了两三份就不想看了,希望互测屋人数能够减少一下
  • 第一单元的测试用的是评测机,很轻松,另外手工构造一些多重嵌套的怪数据,并且找到bug就是找到了,错了就是错了,同样的数据程序跑多少遍都是相同的结果。但是本单元的电梯作业就不一样了,评测机怎么写根本就没有思路,只能靠程序随机生成一些数据然后靠眼睛看是否正确,又因为多线程的不确定性,找到了bug交上去也不一定能复现,有朋友本地找到了一个bug,交了七八次愣是没能复现

六、心得体会

6.1线程安全

  • 线程安全的问题主要体现在对于线程间共享对象的访问和修改,如果对于共享对象只有读操作,那么就不需要设置锁,但是一旦涉及到对于共享对象的写操作,就一定要注意线程安全问题,线程对于共享对象进行读写操作时一定要加锁。
  • 死锁问题,出现死锁问题的原因是,在临界区的互相调用,导致同时等待对方释放锁,从而无限等待出现死锁,出现死锁问题就要想办法一层一层解锁,必要时还需要修改架构设计。结合自身经历,出现死锁的原因很可能是把操作无脑放进临界区导致死锁,临界区内的操作一定要少之又少!!!

6.2层次化设计

  • 层次化设计是指在一个大型设计任务中,将目标层分解,在各个层次上进行设计的方法。

  • 在我们看完指导书后分析需求,就可以把作业的实现拆分成几个层次,输入层次 -> 调度层次 -> 电梯层次,先读入需求,再调度需求,再实现需求

  • 在以后面对的各种问题当中我们也可以利用这种层次化设计的方法,把目的需求拆分成几个层次,每个层次单独实现功能,然后实现层次与层次之间的交互,从而最后实现我们的目的需求

posted @ 2021-04-21 23:22  神樂坂清清  阅读(132)  评论(2编辑  收藏  举报