BUAA_OO_Unit2 单元总结

BUAA_OO_Unit2 单元总结

整体概述

本单元聚焦的问题是多线程场景下如何进行程序开发以及维护线程的安全性问题。第一次作业中的通过模拟多部纵向电梯的运行来引入多线程的开发模式;第二、三次作业则在此基础上进行增量开发,涉及到了不同线程之间进行数据交互,以及由此可能带来的线程安全问题。

总的来说,本单元不同作业之间的迭代开发难度较上一单元降低,整体算法架构无需太大的变动;但是如何恰当的运用好同步块与锁以维护好多线程的正确运行是本单元的难点,作者也在这个问题上多次出现bug;

第一次作业

架构分析:

第一次作业中最为主要的架构就是采用了第一次实验中的生产者——消费者架构将请求进行动态分配处理。

本次作业中几个较为重要的类为:

1、InputThread:输入线程,也是生产者——消费者模型中的生产者,通过将输入进行预处理后生成一个Request对象以放入"托盘"以给消费者获取处理。

2、Dispatcher:调度器线程,作为生产者——消费者模型中的”共享托盘“,负责将输入线程传入的Request对象分给每个电梯进行处理

3、Elevator:电梯线程,生产者——消费者中的消费者,负责处理调度器发送给它的Request对象。这个类中有较多方法,电梯的开关门策略以及运行策略均整合在该类中。

就运行策略而言,采取了与生活中电梯运行模式很相似的look策略,即在运行过程中进行同方向的捎带,而在运行到顶层/底层时,将检测上方/下方还有无乘客请求,如果有将会继续保持原方向运行,否则调转运行方向或停止等待新的乘客请求。

同步块设置与锁分析:

本次作业中采取了synchronized关键字以及对象锁。采取的同步策略是当一个共享对象涉及到同时读写时进行上锁。为了简化代码结构,这次作业中上锁的方式基本都是对共享对象内部的一些方法进行上锁,只有在电梯的上下客方法中才加上了额外的synchronized同步块(此处因为涉及到了对于共享对象中的一个数组进行遍历与修改操作,即对共享对象进行读写)。

调度器分析:

本次作业的调度器线程为Dispatcher,但是由于第一次作业每栋楼仅仅有一部电梯,因此调度器起到的作用仅仅是将不同的Request中的楼座信息交给对应楼座之中的电梯,实现原理为通过数组存储不同电梯的侯乘表队列,通过数组下标来区分楼座。

Bug分析:

本次作业的bug集中在临界区的不正确设置上,即在上文提到的电梯的上下客方法中,因为涉及到对于一个数组的边遍历边修改,因此需要按照讨论区中介绍到的方法运用到集合来避免ConcurrentModification Exception;但在笔者按照方法进行了代码编写后仍然出现了该种异常;后来在bug修复时仔细思考,发现之前的写法只能避免单线程状态下的边遍历边修改,在多线程模式中,有可能修改来自于外部线程而并不是自己这个线程。在之前的书写方法中,我并没有将遍历部分也放入临界区中,导致了线程安全问题。

第二次作业

架构分析:

本次作业整体来说相比于第一次架构并无太大区别,仅仅是加入了涉及到横向电梯运行的相关类与策略;同时参考到了实验中的流水线模式,为下次作业打好基础,将调度器类由线程转化成了一个单例模式,取消了调度器与输入线程间的共享对象——请求存储队列,而是每产生一个新请求都会由调度器即时分配。由于本此横向请求与纵向请求还是独立的,不需要涉及到换乘,因此对于调度器来说,请求来源仍然只有输入线程。

对于多电梯来说,采取了自由竞争策略。这样做的好处是因为在多线程的编程中,线程之间会自己竞争同一个需要的锁,因此可以很好的模拟电梯的自由竞争模式,代码编写较为简单。策略的缺点是,当请求数不太多且相差楼层较大时,容易造成电梯的空跑以至于性能的下降。

同步块与锁分析:

本次的同步块与锁的方式与上次相同,在横向电梯类中新加的锁也与纵向电梯几乎相同,类比即可。

调度器分析:

本此调度器的不同也在架构分析中提到,更换成了单例模式,以更好的实现类似第二次实验中的”流水线架构“。调度器分配Request的对象是各个楼座的一个整体侯乘表,这个侯乘表被楼座类同类型电梯所共享。

Bug分析:

本次作业的bug是在参考第一次实验时留下的不好的习惯导致的——无脑的进行notifyAll,且在wait()处并没有采取一个循环进行唤醒后的判断而导致轮询问题以至于CPU时间过长。

第三次作业

UML图:

 

 

时序图:

 

 

架构分析:

第三次作业在之前的基础上新加入了流水线模式,用来处理拆分需要跨楼座的请求。对于所有的请求,我采取的是一种类似于动态拆分的方法,即通过SwitchRecord类记录下当前的楼座之间的换乘信息。在分配器处理新请求时,将根据已有的换乘信息决定本次的运行目的地。在该请求运行到目的地之后,将更新自己的所在楼层楼座信息,并于最终目的地比较,若未到达,将会被返回上层调度器重新进行请求的分配。在这个过程中,由于换乘信息是可以更新的,因此这是一种类似于动态换乘的方法。

同步块与锁设置:

本次作业新加上的同步块与锁主要集中在SwitchRecord类上,这个类的实例化对象是输入线程与调度器的共享对象,为了防止在调度器查询换乘信息的过程中对整个对象有修改,需要加上锁来保护线程安全。

调度器分析:

本次的调度器相比之前实例化了一个新的对象——SwitchRecord对象,用于记录所有楼座之间的换乘信息,在每次分配请求时,调度器都将查询SwitchRecoed以确定Request的目的地,再将Request分配到相应的电梯的等候队列中。

除此之外,本次的调度器还承担了计数器的功能——即参考实验中的方法进行任务完成的统计。每个请求在被电梯运动到了当前的目的地时,将会和最终目的地比较,如未到达目的地,则会将请求再次返回调度器进行分配;如果到达了目的地,则会在调度器中记录完成了一个任务。当输入线程输入结束且所有任务完成时,调度器会调用setEnd方法,结束所有电梯的运行。

Bug分析:

本此强测和互测中出现的bug还是轮询问题。在借鉴了助教提供的查询CPU超时的代码后,发现是在调度器与输入线程的共享对象处出现了轮询现象。三次作业的bug以来基本上都是对于同步块与锁的处理不够恰当造成的。由此可见对于多线程安全的控制还有很多需要学习的地方。

寻找他人Bug的策略

对于寻找他人bug的测略,我的整体思路还是和第一单元差不多,即采用随机数据 + 特殊样例的方法来进行构造。因为随机数据的构造是通过自动评测机来进行,在此就不过多赘述。关于特殊样例的构造,首先我会从自己的出现的bug出发,构造出容易产生线程安全问题或者死锁的数据;除此之外,我还会针对评测机的评测要求构造一些边界数据,如在最后的1-2秒内投入较多请求,看看程序会不会超时等。

心得体会

总得来说,经过第一单元的学习,个人对于java语言的掌握与了解也在不断加深,感觉在第二单元中遇到的难度没有第一单元那么大。但是第二单元学习到的这种多线程编程的思想是第一次接触到。表面上来说,我们三次作业中的多线程可能只是通过一些特殊的语句实现了,但是我们从中学习到的并不是简单的语法知识,而是一种解决任务的新手段与新思想,这是我认为收益颇丰的地方。

除此之外,如何在多线程场景下良好的维护线程安全以及提高性能也是一个重要的体验。三次作业中的bug都集中在同步块与锁的设置部分,可见需要维护多线程的安全还是十分需要思考的,有很多的细节需要思考处理。在这三次的作业中,为了程序的正确性,我仅仅使用了synchronized关键字来进行加锁,这样的加锁性能其实是十分低的。所以虽然完成了这三次作业,但也希望能在以后写出更好的多线程程序。

posted @ 2022-05-02 14:09  wodsk  阅读(44)  评论(1)    收藏  举报