北航面向对象设计与构造2021第二单元作业总结
本单元任务是用多线程模拟电梯。在依次回答所要求回答的问题之前,先放几张研讨会分享PPT的截图,使读者对我的设计有大致的了解。
-
总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系。
答:由于使用了
java.util.concurrent
库所提供的实现BlockingQueue<T>
接口的容器,三次作业中均未显式设置同步块和锁。调度器(生产者)使用了自动阻塞的put()
方法向PriorityBlockingQueue
中放乘客,效果是由于容器中ReentrantLock
的存在,PriorityBlockingQueue
内部被锁时,调度器将在put()
时被阻塞。电梯(消费者)可以使用自动阻塞的take()
方法从PriorityBlockingQueue
中取乘客,效果是由于容器中ReentrantLock
的存在,PriorityBlockingQueue
内部被锁时,电梯将在take()
时被阻塞。由于电梯大部分时间处于繁忙状态,我的设计中电梯并未使用自动阻塞的take()
方法,而是使用了取不到就返回null
的poll()
方法,这样电梯在某层取不到乘客时不必等待,而是可以继续运送已取到的乘客,提高效率。 -
总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互。
答:三次作业中调度器的职责基本没有变化,均只负责读取输入数据,往候乘表中放乘客,通知电梯开始/结束工作。
为什么调度器要通知电梯结束工作?这是因为,如果候乘表不用
BlockingQueue
而用普通容器+synchronized
同步块时,不会再有新乘客到来时,调度器需要调用一次notifyAll()
告知电梯;而使用封装好的BlockingQueue
时,没有notifyAll()
这样的机制,不会再有新乘客到来时,调度器只能向候乘表末添加一个 Dummy Passenger 代表结束,当电梯获取到这个 Dummy Passenger 就可以结束工作了。需要注意的是,如果BlockingQueue
为空,只能得知当前没有乘客到来,不能得知不会再有乘客到来了。就像货架空了,你只能知道现在买不到货了,不能知道是未来还会补货、还是未来再也不卖了。但在我的设计中每层都有候乘表,由于结束时电梯的位置并不确定,需要往每层的候乘表中都放一个Dummy Passenger,我觉得这种方法并不是很优雅,于是为每个电梯建立了一个
LinkedBlockingQueue
作为开始/结束工作的命令。由于LinkedBlockingQueue
有takeLock
和putLock
两把锁,因此支持“同时”读写,如T1-T2-T1-T2-T1-T2-T1-T2-T1-T2......-T1-T2
,比只有一把锁、不支持“同时”读写的ArrayBlockingQueue
效率更高,如T1-T2-[T1-T1-T1-T1-...-T1]-[T2-T2-...-T2]-T2-T1
。LinkedBlockingQueue
JDK 源码截图:ArrayBlockingQueue
JDK 源码截图: -
从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性。
答:图见下方。功能设计上,满足基本功能;性能设计上,满足基本性能要求。由于未针对不同调度策略做类的派生、继承等,扩展性上略显不足。
-
分析自己程序的 bug。
答:由于使用了
java.util.concurrent
库所提供的实现BlockingQueue<T>
接口的容器,bug 均与线程安全无关,集中在程序设计的基础控制流(分支跳转条件)、边界值(>=
还是>
、<=
还是<
)。这些错误过于低级,写在这里太丢脸,今后再忙写程序也要头脑清醒,否则浪费大量时间去de很低级的bug很不值,令人懊恼qaq。 -
分析自己发现别人程序 bug 所采用的策略。
答:由于本月冯如杯 DDL 如同催命,实在没精力去 hack 别人的 bug。我知道和线程安全有关的 bug 不易复现,所以若要 hack,除了分析对方的代码,还需写一个高并发的评测机。
-
心得体会。
答:通过这次作业,我对线程安全和层次化设计有了更深入的了解和认识,相关能力得到了提高。我从中得到了确定:不重复造轮子是很有必要的,我的选择是正确的。此外,也警示我要头脑清醒、避免低级错误。