其他分享
首页 > 其他分享> > BUAA OO 第二单元

BUAA OO 第二单元

作者:互联网

BUAA OO 第二单元总结

线程架构

以下为各个线程类的作用及含义:

类名含义
inputThread 输入线程,将输入乘客加入waitQueue
schedulerThread 调度线程,通过调度策略,将waitQueue分配到多个processQueue中
elevatorThread 处理processQueue,若支持换乘,则将需要换乘的乘客加入waitQueue

同步块设置和锁的选择及调度器设计

同步块设置和锁的选择

我对同步块的设计是采用synchronized​关键字,并用​wait-notifyAll​模式。我加锁的对象是共享对象,这次作业中为​waitQueue, processQueue​。在对这两个对象进行操作时,都要在synchronized块内进行,保证操作的原子性。此外,要尽量减少synhronized块的长度,增加线程间的并发程度。

在同步中块中要合理使用wait,notifyAll。wait使用不当会导致轮询,死锁,无人唤醒等异常情况,notifyAll要在同步块的最后使用,但是每个同步块都加上notifyAll会导致不必要的开销,加少了也可能导致死锁,无人唤醒等情况。

上述叙述可能有点空泛,下面为调度线程的关于同步部分的伪代码:

synchronized (waitQueue) {              // 访问共享对象waitQueue需要加锁
   if (waitQueue关闭并且waitQueue为空) {
       synchronized(processQueue) { // 访问共享对象processQueue需要加锁
           关闭processQueue
          processQueue.notifyAll; // 通知处理队列关闭
      }
  } else if (waitQueue为空) {
       waitQueue.wait(); // 执行wait避免轮询
  } else {
       /*如果调度中不用访问processQueue,不加锁使同步块尽量短*/
       执行调度,选择一个处理队列processQueue1,将乘客加入其中
       synchronized(processQueue1){ // 访问共享对象processQueue1需要加锁
      processQueue1.notifyAll; // 通知processQueue1有新乘客加入
      }
  }
   // 不用加waitQueue.notifyAll
}

调度器的设计

共享对象使用共享对象的线程
waiQueue​ inputThread, schedulerThread​
processQueue schedulerThread, elevatorThread​

由于第五次作业只有一部电梯,所以调度策略很简单,只需将waitQueue中的乘客依次加入processQueue,并进行processQueue.notifyAll,由于调度线程与电梯线程共享processQueue,通过processQueue就可以实现与电梯线程的交互,通知电梯有新乘客来了。

此外调度器还要与输入线程交互,当waitQueue为空时,进行waitQueue.wait,等待被输入线程唤醒,当输入线程获得新乘客或者读到NULL时,就会进行waitQueue.notifyAll,进而唤醒调度器,因此调度器也实现了和输入线程的交互。

同步块设置与锁的选择

由于第五次作业我就采用了调度器,所以五六次作业架构基本相同。所以同步块设置基本相同,不再赘述。

调度器设计

第六次作业同步块改动部分在于如何处理ADD电梯的请求,我的是实现方法是,每个processQueue对应一部电梯,当输入线程读到ADD请求,通过synchronized​块访问processQueue将其对应的电梯开启,并通知(notifyAll),所以共享内容也发生了变化:

共享对象使用共享对象的线程
waiQueue​ inputThread, schedulerThread​
processQueue inputThread,schedulerThread, elevatorThread​

输入线程也需要共享processQueue,进而通知电梯线程,使电梯线程开始运行,所以输入线程也要与电梯线程交互。

同步块设置与锁的选择

由于我的第七次作业增加了换乘功能,所以对于同步块与锁的部分增加了许多内容,其中如何安全结束线程成为了一个重点,而这个重点中就对锁的运用有比较高的要求,新共享信息如下:

共享对象使用共享对象的线程
waiQueue​ inputThread, schedulerThread,elevatorThread
processQueue​ inputThread,schedulerThread, elevatorThread​
changeQueue inputThread, schedulerThread, elevatorThread​

新增了changeQueue了来保存要换乘的乘客,调度线程需要共享changeQueue是因为要将需要调度的乘客加入changeQueue;电梯线程需要共享changeQueue是因为当换乘乘客到达他最终目的地后,要将其从changeQueue中移去;输入线程要共享changeQueue是因为当读到NULL要判断changeQueue为空才能将waitQueue关闭。

电梯线程需要共享waitQueue,以便将需要换乘的乘客待其下电梯后加入waitQueue中。

我的waitQueue关闭条件是读到NULL并且waitQueue与changeQueue都为空,所以输入线程需要同时获得waitQueue和changeQueue的锁。

调度器调度时很显然需要同时获得waitQueue和processQueue的锁。

电梯线程将到达最终目的地乘客从换乘队列中移走时需要同时获得processQueue和changeQueue的锁,这样就有可能出现循环死锁,如下:

调度器设计

由于新增加换乘功能,所以调度策略发生改变,我的调度策略是基于概率的算法,每类乘客有不同概率分配到不同类型电梯,有不同概率是否换乘,由调度器中的dispatch()函数来实现。

若dispatch返回需要换乘,那么就要访问共享变量changeQueue,向其中加入该乘客,但是不需要notifyAll。因为共享changeQueue的三个线程只有输入线程需要进行changeQueue.wait,当输入线程读到NULL时,changeQueue不为空,就需要进行changeQueue.wait。而向changeQueue增加乘客对判断结束没有帮助,所以不必notifyAll,只有电梯线程将乘客从changeQueue移走时需要notifyAll,以通知输入线程。

通过changeQueue共享变量,实现了电梯线程与输入线程的交互。而调度器对于changeQueue只是互斥访问,并没有wait或notifyAll操作,没有实现同步与交互。

第七次作业架构设计的可扩展性

UML类图

UML协作图

BUG分析

这单元作业吸取了第一单元的经验教训,在本地测试比较充分,所以强测和互测中没有被找到BUG,但是还有一些本地测出来的BUG。

在Night模式下,我采取的策略是当调度器调度完所有乘客请求之后再唤醒电梯线程,即读到NULL。但是在支持换乘的情况下,如果只有当读到NULL才唤醒,那么对于换乘乘客加入第二站电梯时,就没有人唤醒电梯了。就会出现这样一个情况,电梯线程无限等待,虽然processQueue不为空,但没人唤醒;输入线程无限等待,读到NULL,但是changeQueue不为空,不能关闭waitQueue,最终使线程无法结束。之后我放弃了Night模式下最后唤醒的策略,修复了BUG,即使这样做性能也和之前的策略相差无多。

该BUG所在类为调度器,也是一种死锁BUG,双方无限等待,无人唤醒。如果设计支持换乘的电梯,那么如何正确安全结束所有线程是一个很重要的线程安全问题。

DEBUG策略与HACK策略

由于多线程HACK比较玄学,并且我也没有搭建电梯的评测机,所以我的HACK策略只是把我自己本地测试的一些自己的测试样例还有一些边界样例来HACK,还算优点收获,在第六次作业中HACK到了两个。

我没有去阅读别人的代码来发现别人线程安全问题进行HACK,但是针对线程安全问题,我想说一下我自己的DEBUG策略,讨论区也有同学介绍了IDEA的多线程调试工具,但是由于线程指自己控制,有时候复现不了BUG。所以我采用了print大法,在wait,notifyAll等重要节点前后打印信息,以实现DEGUB,效果还不错。要实现复现BUG,就要实现定点投放,我参考了讨论区用管道加上c程序实现的定点投放,感谢一下讨论区的大佬们。

我是自己手动构造的测试集,所以覆盖面可能不会很全,所以除了测试自己的数据集,我还用了硬看DEBUG方法。这在换乘电梯线程如何结束的部分还是有点用的,这次电梯单元作业最常见的BUG就是无限等待或者死锁导致线程无法继续进行或结束,所以聚焦在线程结束以及wait-notifyAll语句出现较多的地方,发现BUG的几率还可以。

本单元与第一单元测试策略有比较大的区别,第一单元主要错误是答案不对,第二单元只要错误是线程卡死不结束以及CPU_TLE。所以在本地测试时,我也偷了一些懒,测试每个数据时只看能不能结束,因为第二单元的结果正确性确实不那么好判断,对于正确性判断就交给中测了。。。可能这是懒狗的做法吧,不过第二单元电梯出现正确性错误的概率比较小。当然测试相比第一单元要实现定点投放,之前也提过了,DEBUG方法也很不同,我采用的时print大法。

心得体会

线程安全确实是这单元的难点与重点。我对于这方面的体会是在下笔前一定考虑全面,可以在纸上也画一下前驱图,调度关系之类的东西,避免死锁、无人唤醒等异常情况。不然到时候测试的BUG可能是因为好几个BUG叠在一起,就算用print大法也要调好一阵,重要是梳理各个线程之间的同步与互斥关系,利用共享对象传递信息,搞清不同线程对共享对象的使用。

层次化设计在这单元我也深有体会。万事开头难,第一次接触多线程编程面对第五次作业有点无从下手,还好有实验代码救命,实验代码的代码架构比较清晰,也让我有了上手的思路。我的架构基本采用实验代码的层次结构,对于调度器调度算法,电梯调度算法都封装在两个方法里,如果要该换策略,就直接新写调度方法即可,不用在代码中大改,实现一定的层次化设计。层次化设计对于迭代开发也很有帮助,我的三次作业都是用的最开始的架构,每次改动很少,没有像第一单元那样进行重构,只有第三次加了换乘后改动比较多。所以层次化设计在我们以后代码开发中还是很有用的,尤其对于迭代开发很有帮助。

最终也算比较圆满完成了第二单元的任务,也希望在之后的单元中,尽量提高自己的架构设计能力,养成封装,层次化设计的习惯与能力,有助于今后的迭代开发。

标签:OO,调度,BUAA,电梯,线程,changeQueue,processQueue,waitQueue,单元
来源: https://www.cnblogs.com/bowenzzz/p/14694464.html