终于完成了传说中的电梯作业,着实被多线程虐的很惨。这篇博客主要分以下几个部分对这三次作业进行总结:多线程笔记总结、作业设计策略的分析,程序结构度量及分析,作业bug分析、心得体会。

一、多线程笔记总结

  • 多线程:指的是这个程序(一个进程)运行时产生了不止一个线程
  • 并行与并发:
    • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
    • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力

   线程有很多种状态,通过不同的方法可以相互转化,转化关系如下图。

 

     因为这三次作业我都选用了wait和notify的方法进行线程同步和互斥,所以重点学习了这两个方法,在写代码的过程中也踩了很多坑。wait/notify必须存在于synchronized块中。并且,这三个关键字针对的是同一个监视器(某对象的监视器)。这意味着wait之后,其他线程可以进入同步块执行。当某代码并不持有监视器的使用权时,即脱离同步块去wait或notify,会抛出java.lang.IllegalMonitorStateException。也包括在synchronized块中去调用另一个对象的wait/notify,因为不同对象的监视器不同,同样会抛出此异常。

   线程在Running的过程中可能会遇到阻塞(Blocked)情况。

  1. 调用join()和sleep()方法,sleep()时间结束或被打断,join()中断,IO完成都会回到Runnable状态,等待JVM的调度。
  2. 调用wait(),使该线程处于等待池(wait blocked pool),直到notify()/notifyAll(),线程被唤醒被放到锁定池(lock blocked pool),释放同步锁使线程回到可运行状态(Runnable)
  3. 对Running状态的线程加同步锁(Synchronized)使其进入(lock blocked pool),同步锁被释放进入可运行状态(Runnable)。

    此外,在runnable状态的线程是处于被调度的线程,此时的调度顺序是不一定的。Thread类中的yield方法可以让一个running状态的线程转入runnable。为了保证线程安全,Java还提供了一系列原子操作的容器,计划在下周学习一下这部分内容。

 

二、作业设计策略的分析

  这三次电梯作业依旧是难度递进式,我们分别实现了单电梯傻瓜调度,单电梯捎带调度和多电梯ss调度(死锁调度)。三次作业均使用了生产者--消费者模型,恰好同一时期我们的操作系统部分也在学习这一部分,所以通过动手实践自己也更好地理解了这个模型。三次作业我均采用了wait()和notify()的方法在线线程之间进行消息的传递,且没有在方法上加锁,只在访问共享对象时对某一代码段加锁,三次作业没有出现CPU超时和线程不安全的问题。

  • 单电梯傻瓜调度

     单电梯傻瓜调度较简单,我尝试用“分而治之”的策略去解决问题,为电梯和输入请求各开了一个线程,输入请求充当生产者的角色,电梯充当消费者的角色,二者通过调度器(也就是课上所讲模型中的“托盘”)进行沟通。电梯根据乘客的起始地和目的地运行即可,没有使用任何算法。

  • 单电梯捎带调度

     第二次电梯其实和第一次调度就相差了一个调度策略的问题,所以我在第一次作业的基础上进行了修改和扩展。我的原想法是让调度器通过判断需求控制电梯的运行,但在实践的过程中出现了一些偏差。我采用了look模式(虽然可能导致超时),在电梯运行的每一层检查需求,判断是否需要开门,进人或者出人,不断更新电梯的最低目的层和最高目的层,然后使电梯在设置好的最低层和最高层之间进行运转。但是在写代码的过程中,我在电梯类的内部直接判断需求并进行相应操作,所以在本质上并没有实现调度器对电梯的控制。这样实现虽然效果没有什么问题,但是电梯类的复杂度较高,这也是导致我这次作业出现失误的重要原因之一。

  • 多电梯ss调度

    第三次电梯作业的难度有了较大的飞跃,而且多电梯不同的可达楼层和新增的换乘需求也给调试带来了很多困难。本次作业我采用了分层调度的设计方式,设置了一个总调度器接受输入请求,然后对A、B、C分别设置监视器(Monitor)作为总调度器和电梯的交互接口。在调度策略上我仍旧沿用了第二次作业的方式,因为重构代码比较麻烦而且当时时间确实比较紧张,所以对需求的判断和决定运行的方向这两个功能,仍旧是在电梯内部完成的。这次的设计有几个失误,一是电梯内部过于复杂(在添加了换乘之后方法数暴增),二是我设计了三个不必要的子类(ElevatorA,ElevatorB,ElevatorC),完成作业后我重新检查了自己的代码,结果发现三个子类除初始化的一些属性不同外并无差异。没有提前做好设计,一边写一边打补丁,导致我第三次的作业代码非常乱,可读性也很差。

三、程序度量及分析

  • 第5次作业

  从图中可以看出,本次作业的架构基本上是可以的,但对Elevator的一些功能,如openDoor,closeDoor这种功能和特性没有做到很好的抽象,所以Elevator内部的run方法复杂度偏高。

 

 

 

  • 第6次作业

   第6次作业因为直接选择了在第5次作业上进行扩展,所以架构是相同的,但因为要在电梯内部实现调度策略,所以电梯内部的复杂度更高了(手动捂脸。不过有一点进步的是,本次作业对更多的方法进行了抽象和分类,更加面向对象了。

 

 

  • 第7次作业

        为了让图更清楚所以我去掉了类之间的连线,Dispatcher为一级调度器,Monitor为二级调度器。鉴于方法复杂度分析的表格过于复杂所以就不写在博客里了,关于类的设计在下图已经可以体现的较为清楚,但是还是存在方法过于复杂的缺陷。同时这次作业因为提取出来的方法过多,导致代码的逻辑较为分散,私下里和同学交流的时候对方也指出了我的这个问题。

 

四、作业bug分析

  • 第5次作业没有什么可记录的,大家基本都不会有什么问题,互测强测也非常和平。
  • 第6次作业是最值得我反省的一次。

     第二次作业我取用了第一次作业的大部分代码,但在设计时没有考虑判断电梯停止时的条件已经发生了变化,导致了致命bug(电梯中还有乘客但外部停止输入时,电梯不应立即退出),强测只得了20分,没有进入互测,后来只多加了一个判断条件就修复了所有问题,真的着实令人遗憾。这次作业翻车有很多原因,最重要的还是在于学习态度出了问题。因为当时的时间安排不合理,自己眼高手低,外加前几次自己的程序没有被测试出bug,所以这次过了中测之后对于测试就过于懈怠,过于依赖自动评测(谁又能想到自动评测写出了bug呢),没有检查出这个很低级的错误。看到连互测都没有进入真的特别沮丧,不过也算是给自己敲响了警钟,学习还是要谦虚谨慎,脚踏实地。

  • 第7次作业强测和互测都没有被测出bug。

   虽然本地测试别人的程序会出现线程不安全的问题,但交了几次评测机并没有成功hack。鉴于多线程测试的困难,且这次作业代码量真的比较大,所以还是用了自动评测,并没有怎么读别人的代码,计划下周学习一下优秀作业的代码。

五、心得体会

  不要放弃,不要放弃,不要放弃。

  我第7次作业查bug查到了周一凌晨,当时连交两次中测都WA了最后一个点,心态爆炸。最后在同学的帮助下发现自己调试用的注释没有删(蠢哭),虽然至今非常疑惑为什么前面的点连调试信息没删都没有测试出来,但是因为中测没过被迫读代码检查出来了线程不安全的问题和几处笔误,算是因祸得福,否则可能又一次进不了互测。虽然OO这门课程很难,但是计算机学院有什么课是不难的呢多思考、多和同学交流真的能受益匪浅。这也是一个发现自身不足的机会,除了代码,我们要学习的还有很多。