首页 > 其他 > 详细

OO第二单元总结博客

时间:2021-04-24 16:46:18      阅读:13      评论:0      收藏:0      [点我收藏+]

第二单元总结博客

19373069 刘川枫

一、同步块的设置和锁的选择

多线程程序设计必须要考虑到不同线程同时对同一个对象进行访问,产生冲突的情况,因此就必须要进行加锁,防止同时访问产生错误,而在哪块加锁就是程序设计者最需要考虑的问题。我的加锁原则是:在保证正确的情况下,锁住的部分越少越好。

①第一次作业

第一次作业,我采用的是synchronized锁。第一次作业只有两个线程:Elevator(电梯线程),InputThread(输入线程);整个程序只有一个等待队列People。这时,能产生冲突的情况只有一种:Elevator和InputThread同时写People队列(更准确的说,电梯需要从队列中删除元素,InputThread需要向队列中添加元素),那么只需要保护这个队列People,保护的方法也比较简单,不需要锁住People,只需要对两个类访问People的方法加锁就可以了。另外由于不允许轮训读入,需要使用wait-niotifyAll模式读入,我在电梯类里面进行了如下操作,当等待队列People为空的时候使电梯直接wait,当InputThread读入数据或者终止读入时notifyAll,降低CPU使用时间,这里我引入了一个没用的全局变量fake作为被锁住以及发出wait和notifyAll信号的对象。

②第二次作业

第二次作业是多部电梯,读写冲突扩展到多部电梯和InputThread对等待队列People同时读写产生冲突的情况。这次我仍然采取synchronized锁,被锁的对象是global(全局变量,包括等待队列People和输入是否终止的型号status)。这次由于电梯不止一部,可能有多部电梯同时调用一个方法操作global.People,因此,紧紧锁住方法是不够的(当然方法该锁还是要锁的),我才去的策略是:把Elevator类和InputThread中需要操作People的代码部分作为临界区加锁,但是这个临界区不能太大,即临界区代码可以在瞬间执行完毕,并且与其他线程无关,防止发生死锁。

举例说明一下“不能太大”:
synchronized (global) { for (int i = 0; i < global.getPeople().size(); i++) { if (check(global.getPeople().get(i))) { PersonRequest temp = global.getPeople().get(i); room.add(temp); intemp.add(temp); global.getPeople().remove(i); i--; } } updatemain(); global.notifyAll();

我们给global上锁(global中包括队列People),因此任何时刻只能有一个线程访问People,而当某个电梯线程拿到锁之后,临界区的代码要做的事,可以很快完成,并且与其他线程无关,完成后立即释放锁。

③第三次作业

第三次作业,整体上和第二次作业类似,由于我很菜设计了三个等待队列(每个电梯的类型对应一种等待队列),我的调度器是“组合逻辑”,它是一个静态的方法,不是一个线程,因此一旦一位乘客被InputThread读入,他就会被立即分配到一种电梯的等待队列里面,只有这种电梯可以接这位乘客并为他提供服务。

三种类型的电梯用不同的类表示手动继承,又有三个等待队列QueueA、QueueB、QueueC,分析一下,不难得出:冲突在于:InputThread可能会对QueueA、QueueB、QueueC进行添加操作(写),ElevatorA类可能会对QueueA进行读写操作,以此类推,因此需要分别保护三种等待队列。这里最开始我仍然延续了第二次作业的synchronized方法,但是交上去的点有一个会RE,当时还很奇怪,因为测试中并没有标准异常信息,后来研讨课上经过同学讲解,得知RE有可能实际上是CTLE……总之之后我采取了新的思路:读写锁模式,即对需要读取队列的部分加读锁,需要对队列进行修改的部分加写锁,就顺利完成了这次作业的全局变量保护任务。

这里简单说一下我个人理解的ReadWriteLock模式优于synchronized模式的原因吧:首先,我们需要明白:读读不会产生冲突,但是读写、写读、写写都会产生冲突。如果采取synchronized模式,虽然保证了线程安全,但是读读也会被禁止,降低了效率,而读写锁不会。其次,synchronized释放的锁有可能被同类甚至自己抢到,从而产生很难被发现的死锁,但读写锁一般不会。总之,读写锁逻辑清晰、使用方便、效率更高。

二、调度器设计

三次电梯作业我采用的策略是集中式调度策略。第一次和第二次作业所有电梯共享一个等待队列,因此没有设计单独的调度器。
第三次作业尽管有三个等待队列,但实际上是同类型电梯共享同一个等待队列,因此还是集中式调度,只不过需要在InputThread读入乘客时就立即将乘客分配到某一类等待队列里,分配方法是:如果C电梯可以将其运送到,则分配到QueueC里,若不行则考虑B电梯,B电梯也不可以时分配到A电梯。调度器为InputThread中的一个方法:
public void alloc(PersonRequest item)

调度器不是线程,因此可以内嵌到InputThread中。

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

①UML类图

技术分享图片

②UML协作图

技术分享图片

③分析

因为比较菜,我的第三次作业没有设计换乘,而是采取静态分配的方式,总体上继承了第二次作业的思路,两类线程:电梯类和读入类。这里有个比较失败的设计就是设计了三个电梯类:ElevatorA,ElevatorB,ElevatorC手动继承太傻了,当时是准备写一个在第二次作业原有的Elevator类上做继承的,但是遇到了一个问题:Elevator类里的变量设为private还是package public的问题,我们的checkstyle不建议使用package public,因此最后我就采取了手动继承Elevator类的傻瓜方式。

现在复盘一下,可以有两种更好的解决方案:
(1)仅设置一个Elevator类,在Elevator类里面新增type变量标记电梯种类,但是这种做法可能会导致run()方法过于复杂,因此我当时尽管首先想到了这种方法也没有采用这种方法。
(2)把电梯类设置为抽象类,或者设置一个电梯运行方法接口,ElevatorA、ElevatorB、ElevatorC作为子类继承抽象电梯类或者使用电梯运行方法接口,这样可以大幅提高我的代码的可扩展性和可读性
技术分享图片

如果按照上述第二种方法改进优化我的代码,那么程序的可扩展性就会提高很多。如果我们需要设计新类型电梯,只需重写run()方法即可。第三次作业采用的是可重入读写锁模式,因此即使未来有新需求,需要加入动态调度器(线程),也只需要单独对调度器线程内加读写锁即可,新增的等待队列都可以放入到Global类里面,可扩展性相对较高。

④复杂度分析

技术分享图片

电梯类里的pick()方法复杂度有点高,原因是我把到达一个楼层之后检查、接人、放人、更新mainRequest等工作都集成到了这个方法中,为了降低耦合度,可以将pick方法拆分。

四、自己的bug分析

(1)第一次作业
第一次作业有点惨,因为时间原因,最后我采取的不是ALS策略,而是一种改良版的傻瓜电梯策略,效率大概是ALS的一半,因此强测很惨,互测被抓的bug也都是RTLE。因此在第二次作业发布之前,我就自己重写了一份第一次作业的ALS版。
(2)第二次作业
第二次作业采用ALS算法,效率大幅提升,强测全部通过,互测被发现了两个bug,其实这两个bug也挺神奇的我觉得不能算bug吧,都是RTLE,攻击者知道我没有单独处理Morning和Night,因此采用了一种极端数据(ALS killer)导致时间超时。
(3)第三次作业
强测和互测均没有被hack。
(4)关于死锁和bug反思
三次作业我特意考虑了死锁问题,所有测试点并没有出现死锁,但是后来分析,使用读写锁模式更容易避免出现死锁。
我的算法是纯ALS策略,集中调度,因此效率不是很高,但是保证正确性是没有问题的。如果要做到十全十美,那么就必须改进捎带策略和调度算法了。
(5)写作业过程中遇到的一个bug
这个bug我在中测的时候就发现了,没有带入到强测和互测,但也拿出来分享一下吧:

(已修复)当出现诸如5->10 10->15这样的需要捎带的情况,到达第十层,第一位乘客出去,
第二位乘客进入,但是没有更新mainRequest,导致出现bug(我之前更新主请求的条件是:主请求在电梯中,进入的乘客到达目标超过主请求,但是这种情况下,由于主请求离开了电梯,不会更新,导致电梯认为它已经运送完一波乘客,停在终点第十层,新乘客被困在电梯里)。

五、de other‘s bugs

我采取的互测战略是随机数据,重点检查是否实现捎带策略,是否考虑到电梯容量超限。此外,我特意去hack了一个问题,这是我在写作业时遇到的自己的一个bug,就是诸如1->5,5->10这样的捎带,是否更新了mainrequest。
很遗憾,hack了好多次,没有发现bug。
本单元测试的入手点相比第一单元多了很多,可以从捎带策略、效率、满员等极端情况进行hack,当然更好的办法是去寻找死锁和线程不安全的情况,可以阅读别人的代码,看他/她加锁是否足够。

六、心得体会

本单元是多线程编程,训练的是加锁的能力。加锁的时候逻辑一定要清晰,仔细分析会不会出现死锁的情况,加锁的原则是“在保障线程安全的情况下,锁的部分越少越好”,另外synchronized(monitor){}模式很多情况下不如ReentrantReadWriteLock模式好用。不同的锁,要多加尝试、灵活运用。

相对于第一单元,这个单元让我真正理解了什么是面向对象编程,现在思维已经完全转换过来了。面向对象编程思维比面向过程思维更好用!

OO第二单元总结博客

原文:https://www.cnblogs.com/19373069lcf/p/14693295.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!