面向对象设计与构造-第二单元总结

千年的反思 2024-04-20 17:52:02

面向对象设计与构造-第二单元总结


笔者窃以为,如果说第一单元讨论的是面向数据和行为的编程模型中的每一类数据及功能结构,那么第二单元讨论的则是面向并发和协同的编程模型中的每一类数据及功能结构。

面向并发和协同的模型中,最关键的两类数据应是共享的数据存储线程状态的数据

因此,笔者接下来就从这两类数据对第二单元作总结——

共享数据的逻辑组织

并发情境下,多个线程同时访问共享对象是造成线程行为不确定性的根本原因。

因此,识别共享数据是设计时的首要任务:

识别哪些数据是共享数据

识别哪些数据是共享数据,可以通过线程交互的架构来窥探一二:

共享数据的识别与线程交互的架构密切相关

  • 服务器架构:请求队列是共享数据

    • 集中处理的类需要从队列取出请求
    • 输入请求的类需要向队列新增请求
  • 流水架构:每个分段处理线程的队列队列中的请求是共享数据

    • 前一个阶段需要写入该队列
    • 后一个阶段需要从该队列取出请求
    • 外部观察者可能需要获取队列中某请求的当前处理进度
  • 事件驱动架构:请求队列和处理器内部的请求队列是共享数据

    • 分派器需要从请求队列中取出请求
    • 输入请求的类需要向队列新增请求
    • 分派器分配请求时,需要向处理器内部的请求队列新增请求
    • 处理器处理请求时,需要从自身的请求队列取出请求

电梯调度属事件驱动架构

电梯调度的事件有三种类型:PersonRequestElevatorRequestElevatorDCRequest,故电梯调度可采用事件驱动架构。

事实上也是只有两个阶段的流水架构。

那么盛放所有Request的全局请求队列,以及每个电梯的待处理队列便是共享数据了。

另外还有两个小细节:

  • 事件驱动架构中的处理器的当前状态可能也是共享的数据
  • 有的数据虽然可能被多个对象拥有,但其被修改时对顺序不敏感,不必联合保护

保护共享数据!

保护共享数据,就是要做好同步块的设置和锁的选择

Java支持的互斥办法

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

当然,在引入双轿厢电梯后,双轿厢电梯本身就有两个轿厢在并发执行,同时一个双轿厢电梯还需要一个自身的调度器来处理换乘。

线程状态的共享,只有在进行线程协作的时候才会得到使用:

事件驱动架构下线程的协作

笔者设计(第七次作业后)的线程协作关系应该如下图所示:

avatar

调度器、输入线程二者之间的协作非常简单,此处不加赘述。

事实上,笔者认为线程状态共享中最关键的部分就在于电梯的reset。调度器要将ElevatorRequest交给电梯,电梯在重置完成以后需要通知调度器。

在第6次作业时,笔者并没有考虑应该如何让电梯通知调度器——只是每次在电梯重置完成以后,如果调度器处于WAITING状态就唤醒调度器,让调度器自己在run()while(true)循环中判断电梯是否重置完成。

这样的设计在第7次作业变得难以为继,因为旧电梯重置为新双轿厢电梯时,旧电梯必须重置完成以后新的双轿厢电梯才能开始运行DCElevator.start()。调度器自己在循环中可以判断出电梯是否完成重置,但是并不知道电梯是在何时完成的重置,无法找到合适的使新的双轿厢电梯开始运行的时机。

因此,在研究了第四次课上实验后,笔者借鉴了exp4_1_publicPipeline.java中的public static void finishWork(Worker worker)方法:处理器调用调度器的静态方法来通知调度器! 正在笔者为线程的交互感到十分的困惑之时,课程组安排的课上实验立刻就提供了解决问题的方案——在此笔者必须要向课程组表达十分的敬意!

双轿厢的协作

由于笔者并没有足够强的能力完成双轿厢的影子电梯等等复杂的调度算法,因此只是在给双轿厢电梯分配乘客后由该双轿厢的简单协作将乘客送抵目的楼层。在这样简单的设计中,双轿厢之间协作的实现最关键的内容便在于实现双轿厢的两个轿厢不碰撞

笔者实现双轿厢的两个轿厢不碰撞,实际上只是在双轿厢电梯自身的调度器中设置一个java.util.concurrent.atomic包中的原子化Boolean类型isOccupied,用于表示换乘层是否已被占用:

  • 当轿厢尝试抵达换乘层时,如果换乘层已经被占用(isOccupiedtrue),那么则等待wait();如果未被占用,则将isOccupied设为true再进入换乘层

  • 当轿厢离开换乘层时,先离开换乘层,再将isOccupied设为false。此时,如果另一个轿厢处于WAITING状态,则唤醒另一个轿厢。

笔者能力不足,尚不能证明上述方法一定能够保证两个轿厢不发生碰撞。但在面向数据点的样例测试中,并没有出现发生碰撞的情况。


在面向对象的设计中,分析完了面向并发和协同方面的内容,也应该略微分析面向数据和行为方面——

OCP原则和作业架构的逐步变化

OCP原则提出,无需修改已有实现(close),而是通过扩展来增加新功能(open)。

Any software entities should be open for extension, but closed for modification.

笔者认为这个原则不难理解——尝试修改(modify)已经完成了的部分的代码是一件非常困难的事情,这往往意味着长时间的debug。因此不修改现有实现总是一个很不错的选择。

因此当第七次作业引入了双轿厢电梯时,笔者便意识到,似乎需要用到继承了。

继承总是一个达成OCP的重要手段,它在这里也显得非常的容易理解——因为双轿厢电梯里其中一个轿厢的运行逻辑基本与单轿厢电梯相同,都是到达楼层-重置(如果需要)-开门(如果需要)-移动(如果需要)。唯二的不同仅在于①到达楼层仅限于换乘层的上/下;②无需重置。

因此,双轿厢中其中一个轿厢可以很好的直接继承原本的单轿厢电梯类,只需要做出额外的规定:

  • 保证两个轿厢在换乘层的互斥
  • 实现乘客的换乘。
  • 轿厢在换乘层时,下一步必须离开换乘层。

两个轿厢在换乘层的互斥在上文已经阐述;乘客的换乘在bug修复中也已经加以阐述。

轿厢处在换乘层时,下一步必须离开换乘层则是由轿厢的运行策略决定的。那么,只要在原有的策略上为轿厢设计一个新策略,只新增加一个功能——轿厢在换乘层时,下一步必须离开换乘层——就可以达成目的了!

这个新策略只新增加了一个功能,因此同样采用继承就可以完成!

本身看上去非常重量级的第七次作业,就优雅地利用OCP原则加以解决了。

当然,笔者个人能力尚不足以写出更高效的调度算法。

下图是第六次作业的UML协作图。与上方第七次作业后的形成对比,很容易发觉Car类、DoubleCarRequestDoubleCarElevatorDoubleCarStrategy类都是继承了第六次作业旧有的类:

第六次作业的UML协作图


未来方向 & 致谢

在第一单元时,笔者承认我的水平不足以在不参加先导课的情况下,迅速完成“面向对象”原则指导下的java工程。事实上,到第二单元,笔者仍认为自己的作业仍没有很好地满足“面向对象”的原则,但经过笔者仔细的阅读和观察后,还是能够惊喜地发现自己的代码架构得到了很大的改善,很多地方已经符合面向对象的思想原则了。

当然,笔者也明白自身的缺陷所在——我虽没有在多线程并发的问题上出bug,但在电梯实现的算法上频繁出错:不必换乘的乘客参与了双轿厢的换乘、跳过正在reset的电梯分配乘客导致RTLE...

另外,笔者并没有足够的能力设计出高效的调度算法,对于Look策略、影子电梯、量子电梯等等高大上的算法均无研究——毕竟研究如何设计更好的程序架构和debug便已经花费了我大量的时间了。

最后,十分感谢课程组、老师们和助教们的辛勤付出!

...全文
67 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧