301
社区成员
发帖
与我相关
我的任务
分享笔者窃以为,如果说第一单元讨论的是面向数据和行为的编程模型中的每一类数据及功能结构,那么第二单元讨论的则是面向并发和协同的编程模型中的每一类数据及功能结构。
面向并发和协同的模型中,最关键的两类数据应是共享的数据和存储线程状态的数据。
因此,笔者接下来就从这两类数据对第二单元作总结——
并发情境下,多个线程同时访问共享对象是造成线程行为不确定性的根本原因。
因此,识别共享数据是设计时的首要任务:
识别哪些数据是共享数据,可以通过线程交互的架构来窥探一二:
服务器架构:请求队列是共享数据
流水架构:每个分段处理线程的队列及队列中的请求是共享数据
事件驱动架构:请求队列和处理器内部的请求队列是共享数据
电梯调度的事件有三种类型:PersonRequest、ElevatorRequest和ElevatorDCRequest,故电梯调度可采用事件驱动架构。
事实上也是只有两个阶段的流水架构。
那么盛放所有Request的全局请求队列,以及每个电梯的待处理队列便是共享数据了。
另外还有两个小细节:
保护共享数据,就是要做好同步块的设置和锁的选择。
Java提供了很多互斥的方法,主要包括以下列举的:
synchronized锁
| 使用情景 | 执行代码块时的互斥情况 |
|---|---|
synchronized(obj){...} | 任何时刻只允许一个线程访问obj对象 |
synchronized(Class.class){...} | 任何时刻只允许一个线程访问Class类 |
synchronized method(){...} | 针对static方法使用时,实际是锁定了整个.class类;针对非static方法使用时,实际时锁定了this对象 |
java.util.concurrent.atomic包中的原子化类型
java.util.concurrent.locks包中的lock类型
lock类型 | 执行lock()和unlock()之间代码时的互斥情况 |
|---|---|
lock.readLock() | 读锁同一时间可以有多个线程获得,一旦有线程获取读锁,将不会有线程获取到写锁直到读锁被释放 |
lock.writeLock() | 写锁同一时间只允许一个线程获得,且一旦有线程获取写锁,将不会有线程获取读锁直到写锁被释放 |
虽然lock类型效率更高,但是由于其不易编写(需要新建对象)并且可读性较差(分了读锁和写锁需要区分),因此笔者并未采用lock类型。
synchronized method(){...}执行会锁定整个.class类或整个this对象——然而事件驱动架构中处理器内部请求队列的互斥并不意味着要将整个类或者整个对象锁定——因而同步范围过大,笔者并未采用此办法。
故,为充分遵循临界区最小化的原则,笔者除将部分如Boolean的基本类型改为相应的原子化类型外,其余的同步互斥完全采用synchronized(obj){...}的形式。并且一切临界区中不再嵌套其他synchronized块,以避免死锁。
笔者感到相当自豪的一点在于,笔者的程序没有出现过线程安全方面的问题。就结果而言,笔者或有自信宣称这一单元的学习收获颇丰!
分析完了共享的数据,接下来就应该分析存储线程状态的数据了——
笔者采用的事件驱动架构下,实际只有三类线程:分派器(调度器)、输入线程和处理器(电梯)。
另外,还有主线程
MainClass。
当然,在引入双轿厢电梯后,双轿厢电梯本身就有两个轿厢在并发执行,同时一个双轿厢电梯还需要一个自身的调度器来处理换乘。
线程状态的共享,只有在进行线程协作的时候才会得到使用:
笔者设计(第七次作业后)的线程协作关系应该如下图所示:
调度器、输入线程二者之间的协作非常简单,此处不加赘述。
事实上,笔者认为线程状态共享中最关键的部分就在于电梯的reset。调度器要将ElevatorRequest交给电梯,电梯在重置完成以后需要通知调度器。
在第6次作业时,笔者并没有考虑应该如何让电梯通知调度器——只是每次在电梯重置完成以后,如果调度器处于WAITING状态就唤醒调度器,让调度器自己在run()的while(true)循环中判断电梯是否重置完成。
这样的设计在第7次作业变得难以为继,因为旧电梯重置为新双轿厢电梯时,旧电梯必须重置完成以后新的双轿厢电梯才能开始运行DCElevator.start()。调度器自己在循环中可以判断出电梯是否完成重置,但是并不知道电梯是在何时完成的重置,无法找到合适的使新的双轿厢电梯开始运行的时机。
因此,在研究了第四次课上实验后,笔者借鉴了exp4_1_public中Pipeline.java中的public static void finishWork(Worker worker)方法:处理器调用调度器的静态方法来通知调度器! 正在笔者为线程的交互感到十分的困惑之时,课程组安排的课上实验立刻就提供了解决问题的方案——在此笔者必须要向课程组表达十分的敬意!
由于笔者并没有足够强的能力完成双轿厢的影子电梯等等复杂的调度算法,因此只是在给双轿厢电梯分配乘客后由该双轿厢的简单协作将乘客送抵目的楼层。在这样简单的设计中,双轿厢之间协作的实现最关键的内容便在于实现双轿厢的两个轿厢不碰撞。
笔者实现双轿厢的两个轿厢不碰撞,实际上只是在双轿厢电梯自身的调度器中设置一个java.util.concurrent.atomic包中的原子化Boolean类型isOccupied,用于表示换乘层是否已被占用:
当轿厢尝试抵达换乘层时,如果换乘层已经被占用(isOccupied为true),那么则等待wait();如果未被占用,则将isOccupied设为true,再进入换乘层。
当轿厢离开换乘层时,先离开换乘层,再将isOccupied设为false。此时,如果另一个轿厢处于WAITING状态,则唤醒另一个轿厢。
笔者能力不足,尚不能证明上述方法一定能够保证两个轿厢不发生碰撞。但在面向数据点的样例测试中,并没有出现发生碰撞的情况。
在面向对象的设计中,分析完了面向并发和协同方面的内容,也应该略微分析面向数据和行为方面——
OCP原则提出,无需修改已有实现(close),而是通过扩展来增加新功能(open)。
Any software entities should be open for extension, but closed for modification.
笔者认为这个原则不难理解——尝试修改(modify)已经完成了的部分的代码是一件非常困难的事情,这往往意味着长时间的debug。因此不修改现有实现总是一个很不错的选择。
因此当第七次作业引入了双轿厢电梯时,笔者便意识到,似乎需要用到继承了。
继承总是一个达成OCP的重要手段,它在这里也显得非常的容易理解——因为双轿厢电梯里其中一个轿厢的运行逻辑基本与单轿厢电梯相同,都是到达楼层-重置(如果需要)-开门(如果需要)-移动(如果需要)。唯二的不同仅在于①到达楼层仅限于换乘层的上/下;②无需重置。
因此,双轿厢中其中一个轿厢可以很好的直接继承原本的单轿厢电梯类,只需要做出额外的规定:
两个轿厢在换乘层的互斥在上文已经阐述;乘客的换乘在bug修复中也已经加以阐述。
轿厢处在换乘层时,下一步必须离开换乘层则是由轿厢的运行策略决定的。那么,只要在原有的策略上为轿厢设计一个新策略,只新增加一个功能——轿厢在换乘层时,下一步必须离开换乘层——就可以达成目的了!
这个新策略只新增加了一个功能,因此同样采用继承就可以完成!
本身看上去非常重量级的第七次作业,就优雅地利用OCP原则加以解决了。
当然,笔者个人能力尚不足以写出更高效的调度算法。
下图是第六次作业的UML协作图。与上方第七次作业后的形成对比,很容易发觉Car类、DoubleCarRequest、DoubleCarElevator和DoubleCarStrategy类都是继承了第六次作业旧有的类:
在第一单元时,笔者承认我的水平不足以在不参加先导课的情况下,迅速完成“面向对象”原则指导下的java工程。事实上,到第二单元,笔者仍认为自己的作业仍没有很好地满足“面向对象”的原则,但经过笔者仔细的阅读和观察后,还是能够惊喜地发现自己的代码架构得到了很大的改善,很多地方已经符合面向对象的思想原则了。
当然,笔者也明白自身的缺陷所在——我虽没有在多线程并发的问题上出bug,但在电梯实现的算法上频繁出错:不必换乘的乘客参与了双轿厢的换乘、跳过正在reset的电梯分配乘客导致RTLE...
另外,笔者并没有足够的能力设计出高效的调度算法,对于Look策略、影子电梯、量子电梯等等高大上的算法均无研究——毕竟研究如何设计更好的程序架构和debug便已经花费了我大量的时间了。
最后,十分感谢课程组、老师们和助教们的辛勤付出!