一、同步块的设置和锁的选择
同步块的设置是保证线程安全性非常重要的一种方法。多个线程访问同一组共享资源时,设置同步块可以保证互斥访问,避免线程不安全导致的错误。
在本单元中:
·我首先重点对总的等待队列Waitlist
进行了同步块设置。第一次和第二次作业使用了ArrayList
,对于该类下的所有方法都加了synchronized
,保证添人和取人线程安全。第三次作业将ArrayList
改为了ArrayBlockingQueue
,这种类型自带锁,用起来更加方便。
·在调度器类Scheduler
中,我使用synchronized
,对应的Object设置为了电梯对象elevator
,用来notify
和标记输入结束(IsEnd
)。
·我也使用了synchronized
来对电梯类进行了同步块处理操作,原因是调度器对电梯内部的队列有写操作,与此同时电梯本身又对电梯内部队列有读取的操作,对于涉及到电梯内部队列的方法,我均上了锁,设置为同步块。run方法中,我也同样对电梯内部队列设置为Object,在内部判断,满足某一条件时进行wait
,从而避免轮询。此外,经过助教在大群里的提醒,对于电梯类我又增加了一个LOCK属性,用来对arrive
,move
等方法带有输出语句的方法进行上锁操作,这样可以避免时间出现大时间先于小时间输出的情况。
上述重点设置为同步块的地方,基本是生产者-消费者模式下的共享数据区。
从上文也可以看出,我的锁的选择,基本是使用synchronized
来实现,感觉这个比较方便,但是也确实在第二次作业debug过程中发现了死锁的现象。de了非常长的时间才改了出来。
二、调度器设计
第一次作业只有一个电梯,我没有写调度器。第二次作业才写了调度器。第三次作业在调度策略上则加入了换乘的考虑。
第二次作业调度器设计主要以三个电梯当前人数为调度方案,每次将人送进队列都放到电梯人最少(电梯内部等待队列+电梯内人数)的电梯。这样不会有电梯处于赋闲状态,让每个电梯都能动起来。(但这并不代表着性能一定高,有许多时候一个电梯拉完某些请求反而更快。
第三次作业调度方法的代码行数大大提升。一方面是三个电梯各自条件是否允许加入,且先判断C能不能进,再判断B能不能进,然后考虑换乘;另一方面是换乘策略的书写,其中学习并使用了回调函数的写法。最后再分配给A类电梯。
在调度器与线程的交互上,我由于书写了Waitlist
类来存储读入的乘客,因此与输入线程并没有直接的交互。调度器与电梯有着深度的交互,通过获取每个电梯的信息来实现调度。故主要的交互就是与电梯线程的交互,通过调度给电梯投放请求。
三、第三次作业的可扩展性
类图如下。
协作图如下。
时序图如下。
·功能与性能
架构上,较第二次作业几乎没有重构。功能上,我认为我对于类的封装,在类与类之间的耦合上是相对较低的,对电梯类也单独写了装载电梯等待队列的电梯等待队列类和电梯内部调度类,电梯内部调度类算是工具类,而我在课上实验后发现,我写的电梯等待队列类其实可以改为private class
改到电梯类内部,作为私有类,因为电梯等待队列对外部也不应该可见。性能上主要是体现在调度策略上。我花了许多时间写调度,结果却实现了“负优化”,还是计算的不够好,我没有考虑换乘的时候,换乘的第二部电梯还要花费一定时间到达换乘楼层,这一时间成本是导致我调度策略时间慢的直接原因。
·可拓展性
由于耦合度较低,我认为具有一定的可拓展性。电梯种类想要什么样的基础属性,直接将参数传入构造器即可,就可以打造出新种类的电梯。调度策略上,我认为还是得具体问题具体分析,所以调度策略的可拓展性较差。调度策略也是单独写了方法。改变策略,这些方法将不可重用,必须重新写。
四、分析自己程序的bug
第一次和第二次都出现了RTLE
的错误,不过最终没有出现线程不安全的问题,这也得益于我苦苦修bug了好几个小时···
自行测试阶段出现死锁的原因是仿照课上实验代码写了synchronized
套synchronized
,而且我也没有调出来为什么会出现死锁,最后将WaitList
的成员类型将ArrayList
换成ArrayBlockingQueue
,少了一层同步块设置,才将程序调好。
synchronized(A) {
synchronized(B) {
SOMETHING();
}
}
synchronized(B) {
synchronized(A) {
SOMETHING();
}
}
第一次作业出现的bug全部是random类电梯出现的运送超时,原因在于我的无脑往电梯塞人,没有考虑方向问题。因此会有一些反方向的人被早早的塞到了电梯里占了本可以快快送到的人的位置。
第二次作业则是有两组180秒才输入的数据,因为我的程序调度不够好直接RTLE。
第三次无。
互测并无bug。(大家都不太积极
五、测试策略
进行了非常基础的测试,但是是比较有针对性的。第一次作业我其实也测了自己相对担心的Random类,但是指令数不多,所以速度慢的问题没有凸显出来。后两次作业我关注到了night类电梯一次发出非常多的指令,这种情况最考验线程安全(可能只是我的代码的线程安全),所以着重测试的都是night类的指令,也果然在第二次作业出现了死锁的问题,在第三次作业出现了在一层反复开门关门的情况···
互测没有积极尝试,只读了读同学的代码,自己分析死锁等可能的线程不安全情况的能力还是不足。且由于bug时常会有难以复现的情况,对于一组数据,一套代码,不是每一次执行都会出错。
线程安全上,我在公测阶段确实没有出现过这样的问题,只有在自己提交前测试的时候出现了死锁的情况,经过修改代码可能大概也许是调好了。
第一单元的测试数据比较好构造,比这个单元构造测试数据要简单的多,在两个单元互测的积极性上就可以体现出来。这一单元要测试出线程不安全问题,我认为重点就是night类的数据类型(也就是一次输入非常多指令的类型数据),不能只跑一次,构造多组数据,每组数据都多跑几次,看看会不会出现问题。如果次数达到一定时,有较大的把握认为没有线程不安全的问题。
六、心得体会
本单元我主要了解到了多线程访问共享数据时要注意的问题,并重点学习并进行了同步块的使用,学习了wait、notify的机制避免轮询。此外,也自行了解了BlockingQueue
这一JAVA库函数自带的线程安全类,应用到我的代码中。但是很显然我对于多线程测试和调试的方法学习得很不到位,对于锁的相关知识也没有深入理解。
这单元的作业比较遗憾的就是我的调度策略写的始终不怎么好,尤其是random类往往非常慢。
这单元我学习到了一点就是对于外部的类也要学会在自己的代码中再构造一个类来封装,这样就不会过多的依赖于外部的类,可拓展性也会大大提高。在第三次作业时由于我要实现回调,不得不重新再写一个类来为PersonRequest增加属性,这样花费的时间会比较多。
这三次作业我没有进行大规模的重构,对比第一单元来说是一个巨大的突破。在层次化设计上也有了一定的长进。另外,对于“面向对象”这个思想,在第二单元我才有了相对深刻的认识(第一单元我自己做的时候还是感觉是面向过程)。