OO第二单元小结
总体架构与线程间协同
- 总体架构
- 线程间协同
总体架构在三次作业中基本稳定,基本架构就是生产者消费者模式的架构。
生产者即InputThread类和schedule分配器在三次作业中的逻辑基本一致,变化的部分主要是电梯线程中的逻辑。具体而言,电梯的整个运行逻辑不变,均保持getNextFloor
-> moveTo
-> arrive
/pass
-> getNextFloor
模式。变化的是根据需求的增加在计算求得下一个目标楼层的限制和到达某一楼层中对于换乘楼层的特殊处理。
互斥与同步控制
我认为关于互斥与同步控制及锁的使用,应该是多线程编程中的核心内容,即确保线程安全。
互斥:是为了保证共享资源在被多线程访问的安全性,避免数据竞争。
同步:是为了协调多个线程之间的执行逻辑,使它们按照确定的顺序相互协同。
第一次作业的互斥与同步控制建立的过程是最为坎坷的。由于不了解synchronized,wait,signal等的具体使用方法,只知道了其具体的语法和大概的使用模式,导致程序完全不能正常运行并且导致了线程的轮询。具体来说,
-
object.wait()
方法的调用对象应该是共享对象,而不是线程对象,效果是将调用该方法的线程加入关于该对象的等待队列中,同时释放该线程已经持有的该对象的锁。所以,该处就要求在调用该对象的wait()方法时,必须要持有对象的锁。 -
Thread.sleep()
方法是Thread类中的一个静态方法,所以在某个线程中直接调用sleep()方法后效果是直接使当前线程加入阻塞队列,到了休眠的时间后线程自动苏醒。 -
object.notifyAll()
方法对应于wait()方法,调用对象同样是线程的共享对象,调用后将所有关于该对象阻塞队列中的线程唤醒,即将所有阻塞线程加入就绪队列,并通过JVM调度一个线程执行。调用notifyAll()方法后不会立即释放锁,离开同步块时才会释放。 -
synchronized
块,进入后持有对象的锁,从同步块退出后自动释放对象的锁,不是一定要调用notifiAll方法。
第二次作业中由于新增了reset请求,故我新增了ResetInfo类,其对象作为电梯和分配器的共享对象。因此电梯等待和唤醒的对象就变成了人请求和重置请求,为了实现两个不同的对象唤醒一个线程,我使用了ReentrantLock和condition机制。在这里由于同时混用了synchronized同步和lock.lock()方法进行同步,导致了难以预料的数据竞争,总结如下
synchronized
机制和ReentrantLock
是两种不同的同步机制
- 具体来说,如果一个线程调用了共享对象的一个synchronized同步方法,同时另一个线程调用使用ReentrantLock控制的方法时仍然可以进入该方法。
- 关于同步控制,必须使用统一的控制方法,要么全用synchronized要么全用lock
电梯实现和调度
电梯核心逻辑
电梯行为建模:
getNextFloor
-> moveTo
-> arrive
/pass
-> getNextFloor
电梯等待:
- 电梯内外都没有人 && 没有重置请求 && (等待队列没有结束 || 重置请求没有结束)
- 电梯内没有人 && 外面有人但不属于自己的请求 && 没有重置
电梯唤醒:
waitRequests.addPassenger()
resetInfo.renew()
waitRequests.setEnd()
ResetInfo.setEnd()
双轿厢电梯:
双轿厢电梯的实现需要两部电梯之间进行信息通讯,就是关于换乘层的占用情况。在运行到换乘楼层前需要判断另一部电梯是否在换乘楼层。我的实现方式是创建一个共享对象,作为双轿厢电梯的一个属性。
电梯在将要到达换乘楼层之前循环调用get方法获取换乘楼层的状态,如果可以到达换乘楼层,则将atTrf设置为true,同时将car设置为对应的电梯轿厢号,否则电梯将会休眠。电梯离开换乘楼层时,会将atTrf状态设置为false,让出换乘楼层的控制权。
这里面需要注意的是电梯必须要避免长时间占用换乘层,换句话说,电梯必须在到达换乘层完成乘客的接送后立刻离开换乘层。根据我的电梯的建模方式,电梯会长时间霸占换乘层的情况只能是在电梯内外都没有乘客且电梯最后一次到达的楼层恰好是换乘层。我在循环判断电梯下一步需要到达的楼层的函数中加入判断,如果getNextFloor() == 0 && curFloor == transfer
直接调用passFloor()
方法,让电梯主动下或上一层让出换乘层。
电梯的调度:
电梯调度使用均匀分配的方案,效果和随机分配基本保持一致。关于重置后电梯内原本乘客的分配问题,我采用的方法是原本是谁的乘客还由谁来运送。之所以没有采用将乘客重新放入总的请求队列中是考虑到类间的耦合问题。如果这样做的话,就必须把总的请求队列作为电梯的一个属性,而原来总的请求队列这个共享资源只是属于输入和分配器这一对生产者和消费者,同时这对于线程的结束也会带来新的问题。分配器线程的结束必然会依赖于电梯的状态,这就又要求分配器必须要遍历所有电梯,这又需要将电梯作为分配器的属性,增加了类间的耦合。电梯本身使用look策略,符合实际电梯的运行情况,在开关门的处理上采用了关门的最后一刻人在进的方式,也是最大化了可捎带的乘客人数,提高了性能。
关于bug
bug主要都是关于线程间的数据竞争和死锁。
数据竞争主要是check-then-act式的对共享资源的访问,由于其他线程同时对共享资源的修改,导致未最后落入任何一个逻辑分支中。这个问题主要出现在第一次作业中,是由于还没有对同步有正确的理解。
而死锁主要是在后面的作业中出现,主要是电梯在休眠后无法被唤醒的问题,最后是根据电梯的状态逻辑重新确定了电梯等待和唤醒的逻辑,补充了之前电梯被唤醒的条件,解决了这个问题。
debug方面,多线程的debug本身就是一个比较复杂的问题。它具有不容易复现和不可循环调试的特征。其中我认为最为困难的就是不可循环调试。在多线程中的打断点方式几乎失效,而我采用的方法主要就是打印输出,将一些关键点处的状态通过打印输出来判断是否符合预期。关于线程的死锁,我借助了idea的调试功能中查看所有线程的功能,这样就能在死锁时轻松发现是哪个线程陷入了死锁。
心得
首先这个单元感觉是有很大收获的,最大的收获就是学习了多线程编程,这对我来说是全新的知识。同时也掌握了实现线程安全的一些具体手段,对于多线程有了一个比较直观的认识和理解。
在这个过程中,对于线程间临界资源的同步互斥,临界区的设置确实是自已一步一步探索出来的。虽然能查到网上的很多资料,但没有具体的实践自然很难理解到其具体的应用过程。从第一次作业的照猫画虎,结果是到处数据竞争,轮询,到最后的较为轻松的识别和设置同步与互斥,这是一个不断尝试,试错,再尝试解决的过程。开始的时候确实比较艰难,但在实践中确实学会了新的东西并且积累了经验。
对于架构设计,我认为其实这也是个不断尝试的过程。因为需求的不确定性和问题本身的复杂性,往往难以一把就将架构规划的合理清晰。我在这个过程中是先将大体的框架先设计好,对于下层的逻辑是在实现的过程中根据具体情况再进行设计,修改之前的结构,慢慢地让整体架构成型。
总而言之,经过这一单元的学习,除了学到新的知识外,我感觉对于写代码这件事比以前的手忙脚乱变得更得心应手了一些。