二单元博客作业
(1)总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系
第五次作业
调度策略:用集中式架构调度一台电梯
共享资源:总的请求队列
waitQueue
、电梯处理请求队列RunQueue
改写
waitQueue
:Scheduler
、InputThread
、读
waitQueue
状态:Elevator
改写
waitQueue
:Schedule
、InputThread
改写
RunQueue
:Schedule
、Elevator
根据共享资源,封装了线程安全的两个队列
waitQueue
、RunQueue
,对所有对其两个对象的private ArrayList<PersonRequest> personRequests;
对象进行状态访问、添加删除元素的方法都加上了synchronized
,并且由于我的请求队列只是一个ArrayList
,因此,只有在添加、移除元素,以及读取其状态的时候,会在使用这两个资源的代码中用同步块。对于Elevator
而言,读取waitQueue
只是为了判断读入线程是否已经结束,因此在电梯类中,不设置用waitQueue
上锁的同步块。因此只有调度器在将请求总总队列移入电梯队列中时会存在同步块嵌套问题,所以此处需要注意的就是需要时刻牢记在这里的顺序,在之后的增量开发中,如果进入了waitQueue
上锁的同步块,最好要避免对电梯队列对象的调用。
第六次作业
整体思路和第五次一模一样,用集中式调度多台电梯,但是减少了电梯对
WaitQueue
的访问,而是通过调度器告诉电梯队列withoutResource()
即所有的输入结束,任务全部分发完毕,电梯通过电梯队列的状态判断替代原有对全部等待序列的判断,将同步块的可能集中在了调度器对各个资源的交互中。
第七次作业
调度策略:用分布式架构调度多态台电梯
共享资源:总的请求队列
WaitQueue
改写
WaitQueue
状态:ElevatorA
、ElevatorB
、ElevatorC
、InputThread
(因为我取消了调度器...),一个是请求队列的改变,还有一个记录所有电梯是否为空的标记需要加锁。
共享的资源是总的请求队列,而这一队列是20层楼的请求队列的队列,因此,每次从标准输入或者电梯换乘中进入一个请求,则将该层的队列加锁,用
sychronized
设置该层队列加入元素方法,同理,每一个有关每一层楼的队列的方法都加锁。但是每次对某楼层的队列加请求操作的时候,都会WaitQueue.notifyAll()
,因为所有的电梯通过判断总的请求队列为空和电梯队列为空后waitQueue.wait()
。此外还有一个公用设置来记录是否可以结束线程的数字在使用时需要加锁。见下文分析。
(2)总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
第五次作业
调度器本身是一个线程,调度器通过控制总请求序列和电梯序列,每当标准输入进入请求时,调度器一次性将所有总请求移至电梯请求中,而当调度器发现总请求队列为空后,就
waitQueue.wait()
,直到请求再次进入,而当标准输入结束后,会再次对所有waitQueue.wait()
唤醒,且将waitQueue
状态设置为close
,进而可以使得调度器在这一轮唤醒中全部清空了总队列后,唤醒所有runQueue
,结束线程。同时,电梯也会在自己的队列清空且waitQueue
状态也为close
时结束进程。
第六次作业
和第五次思路想相同,也是将进入总队列的请求在
while
一轮之后全部清空,只是每一个请求会根据调度策略放进不同的队列中,且在最后调度器结束之前将所有的电梯队列的状态设置为runQueue.withoutResource()
再runQueue.notifyAll()
进而唤醒所有电梯后结束进程。
第七次作业
没有调度器,*竞争且电梯可以换乘的情况下,只有在所有电梯中的人空了,且总请求队列空了,标准输入也结束了的情况下,电梯才能结束进程,此时就需要通过一个公用的值来标注出电梯中的人是否空。此时有一个很面向过程的做法就是将这个公用的值放在
WaitQueue
中,每次电梯进人,就将这个值加一,出去就减一,如果此人需要换乘则再加一。因为在总队列加入请求的时候,都只会用该层队列的锁,因此需要在WaitQueue
中加入synchronized (this)
防止冲突。而在电梯出人的时候,也会调用这个值减一。
(3)从功能设计与性能设计的平衡方向,分析和总结自己第三次作业的架构设计的可扩展性
UML类图
UML协作图
用工厂类创造电梯,在主函数中创造了输入线程和初始的A,B,C三类电梯,另外,在输入线程中如果出现了增加电梯请求则通过工厂类创造电梯。
性能方面,总体上,由于抢人策略会使得所有电梯的运力在每时每刻、同一时刻都会接近其最大值,会更加平均,且可以将抢人策略看做是一种更加动态、实时的分配,而并不是集中式分配的静态分配,因此效率会比较接近最大值。但是,也有可能有数据导致几个电梯抢一个人 ,或者因为这次电梯的特殊性,以及换乘策略不够全面,还是会有电梯的运力达不到合适的范围,偶尔会拉低速度。
扩展方面,出了工厂类制造电梯,我的电梯没有一个更加统一抽象的类来管理,也没有使用策略模式或者状态模式对电梯的各种策略进行管理。因此此后每加一个电梯,就需要写一个专属于这个电梯的类型,而且需要根据这个电梯行进的特点,写专属于它的三种状态的策略,同时可能会更改其他电梯的调度策略。因此从逻辑上讲,我的作业很简单很直观,但是总体可扩展性比较差。
(4)分析自己程序的bug
分析未通过公测用例和被互测发现的Bug:特征、问题所在的类和方法
第五次作业强测0分,首要原因是由于我不会用标准输入导致的,我在
Main
函数、InputThread
函数里面分别创建了标准输入,两个线程同时启动,导致有的数据尤其是和模式有同一时间戳的请求无法接收,此外,由于对于线程当中的死锁问题理解得不够,且同步块规划的很凌乱,并且没有意识到在Schedule
里面,从等待队列中转化的时候,先用锁WaitQueue
对其进行循环访问,再用锁RunQueue
二重循环判断是否加入请求,而在电梯中,又先用锁RunQueue
判断其状态再嵌套锁WaitQueue
来Wait()
,这样一个交叉的部分导致了死锁。同步块各种嵌套还有锁不一致的情况。即WaitQueue
和RunQueue
两个导致的死锁。所以所有的输出不是完全为空的就是TLE了。
第六次作业第8.9个点由于性能问题导致RTLE,这个和我自己在初期在
Schedule
中分配人的策略有关系,我选择将电梯序列最多等待人数多于最少等待人数6个时才会将请求直接流向人最少的电梯,这导致电梯分配不均匀,一台电梯的压力过大。
第七次作业的bug是玄学,大概率是死锁,但是我不会复现,又在本地跑了很多次错误点和随机大数据,都正常结束了,然后我就又提交了原来的代码,然后过了。真的不太会de这种无法复现的bug,也不知道是哪里出错了,目前来推断一个不够全面谨慎的地方在于,我的电梯在将需要换乘的人退出去的时候,是先将计数器减一,再在总的等待队列里面加入这一请求,如果正好此人是所有请求中最后的那个请求,那么在恰好在计数器为0,且总等待队列为0的时刻,另外两个电梯结束了进程,不再拉人,而对于这个电梯而言,它无法接送这个人且因为总等待队列不为空,它将一直等待下去从而导致RTLE,所以需要做的就是将推出的步骤调换,先打印出
out
信息,在将请求放回总队列,再将人从电梯队列中取出,虽然这样做并不符合现实规律,却可以在整个出电梯过程不加同步块的情况下保证线程安全,所以其实最佳做法是在出电梯部分用WaitQueue
加锁设置同步块,这样既能符合物理规律也能防止那段空隙被其他电梯抢去结束了进程,只是需要额外注意不要因为同步块嵌套问题。
(5)分析自己发现别人程序bug所采用的策略
采取的其中一个策略是用大的数据去测试别人的代码,如果没有正常结束,则说明出现线程的问题,但是我用了近600条随机的请求去测试,没有找到别人的bug。
此外就是用一些比较偏极端的情况去测试,比如同一层楼有很多条请求,上下行方向不同,因为我自己的电梯是必须接受相同行进方向的请求才能保证正确送达,否则会导致电梯卡在最高或最低楼层无法行动。
还有就是搜索
sychronized
关键字,一旦有两个或者出现同步块嵌套则检查是否有顺序出错的问题。这些方法主要是基于我自己debug的时候所想到的方法,针对不同同学的运送和调度策略,是很局限的,所以并没有测出来。
第一单元的作业所找出来的bug都是静态可复现的,一旦锁定了一个地方存在逻辑或者处理问题,就一定能够通过相关的数据hack这个bug,而这次的作业,除非是遇到了死锁这种极易复现的bug,因为一旦数据量增大,死锁的问题大概率会浮现,而在小的空隙的出现的思虑不周全的地方,bug复现就成为了小概率事件。
(6)心得体会
我理解的线程安全是让多道线程有条不紊的对数据进行操作,并且在相互制约、交互的线程中能够保证整个程序的运行逻辑、运行顺序正常的概念。
数据的安全可以通过独立所有共享数据读写、写写操作来保证。这次作业中体现的就是通过
synchronized
设置同步块,并且通过自己设计的线程安全的Queue
,即为方法加锁,可以避免线程中的数据冲突。而逻辑运行的准确,则是需要通过线程之间相互挟制的条件、公用的信号来辅助实现。这次实验需要放置轮询占用CPU的资源,所以需要通过
wait()
、notifyAll()
来控制,其中最容易错误的点是如何保证最后一轮唤醒电梯可以使得电梯运完所有的人,这就需要对等待队列的人数以及其他情况进行分类。我在最后一次采取的方式是直接用电梯去接第一个人的逻辑去判断是否还有人可以接,这样就避免了对各种模式的分类。层次化设计方面,这个单元的前两次作业我采取的是中心调度,而最后一次作业我采取的是分布式。虽然第二次的增量开发真的很方便,很快捷,第三次我还是决定重构,没能实现三次作业不重构的增量开发,但确实是想尝试着提高性能,然后试了试抢人。
中心调度一个方便的地方在于,每个电梯只用考虑它自己的乘客该如何调度的问题,所有的分配交给调度器。因此只需要一个类,即调度器就可以集中的考虑所有电梯共同的调度策略,然后按照这些电梯的特点调整即可,可扩展性更强,并且对于电梯类而言,思路会更加单一、清晰。缺点在于,可能会由于调度器调度的时刻的条件并不是真的满足接到人的时刻的最佳条件,所以
Random
模式下,分配策略大概率不如实时抢人的策略好。顺便一提课上的另一种调度器的思路,就是让调度器去根据电梯的状态去排列等待队列的请求,让电梯去按顺序拿请求,以及在完成这些主动拿走的请求的时候完成捎带,一个一个完成等待序列。分布式调度时,确实是我没有想到如何将调度器放在其中进行调度,所以直接取消了调度器、取消了电梯自己的等待序列,让所有电梯共享一个等待序列,实时抢人,并且为了防止多部电梯提前设定好接的第一个人,然后在同一层楼的不同的人被不同的电梯接走,反而会浪费电梯资源,我取消了设定一个人只能由其标记好的那个电梯接走,任电梯自己去抢,在空电梯的情况下,电梯每走一层楼就去判断去接的第一个人是谁,这样就可以实时更新更加符合它去接的人,所以效率会提高,但是CPU的实际运行时间肯定也会提高。缺点在于,由于少了调度器这样一个管辖四方的法官,所有电梯的策略都必须考虑到和另外几种电梯的配合的方式,比如在晚上模式,我会让最慢的电梯从下往上接,让去奇数层的电梯从上往下接,并且如果是从三楼以上的楼层下来了,则停放至三楼,不然停到一楼,诸如此类由于各类电梯不同的特点而导致写电梯A却要去考虑B,如果增加一类电梯就得重写所有电梯的调度策略,非常的不科学。
所以如果是已知了类型很少很固定,不会轻易改动了的话,分布式调度确实会很好,但是如果已知了电梯是要开发出花样的,中心调度,减少工作量、耦合度、复杂度,更加面向对象,挺好。
前期理解线程真的很困难,但是这三周下来,虽然只是学到了浅浅的一点线程运用的知识,但确实是看到自己的电梯跑起来了,把纸片人运来运去的,挺有意思,挺有成就感。