同步块和锁
反思
首先我得反思自己这三次作业中对于多线程学习的不足之处。本次的三次作业我的同步块设置非常之简单粗暴,我全程用的都是synchronized,没有读写锁,没有讨论区同学用的重入锁,甚至第三次作业没有用到信号量。这样的代码很臃肿并且效率低,而且debug效率很低。这是不得不反思的地方。究其原因,一是害怕加入新东西后代码bug会太多无法debug,二当然是懒惰心理作祟,懒得改进,懒得debug。这在以后的学习中应该改。
HW5
我前面也说过我全用的synchronized,但是这部分我个人感觉用的还是比较顺条斯理的。我牢记的是只有共享对象的写-写,读-写这样的情况下才会出现线程之间的冲突。我专门构建了一个RequestTable类用来存放所有的共享对象,但是这么做的好处是少有冲突的bug,坏处是每次都是对这个对象里面的东西访问时候可能会导致时间上的效率低。RequestTable类只有存了所有乘客需求的信息。所有对乘客的操作,比如进电梯,出电梯等都在这里。
HW6
HW6的方法也和HW5一样,只不过区别就是我重新加入了一个方法是重入需求队列,因为电梯的维修会导致乘客要重返需求队列重新等待电梯。
HW7
HW7的一个重要扩展就是一层楼之间的电梯不能挤在一起太多。我身边几乎所有同学都采用了信号量,当然这也是上机中所学到的,学以致用也是自然的。但是由于我写的时候在周五上机之前,我没有提前了解到信号量的知识。于是我人工构造了一个Floor类,这也是一个共享对象,里面存储的数据就是一个大小为11的int数组,作用就是存储每一层楼的现在电梯正在服务的数量,每次电梯到某个楼层让其抢这个共享对象然后判断是否该层楼已经被塞满电梯。没被塞满的话就对该楼层的int数组++,反之就进入wait等待某个电梯的notifyALL。当然了,如果某个电梯离开了这楼层,也要先抢占这个Floor共享对象然后让这层楼的int数组减去1.我当时是看《java多线程图解》看到了厨师-顾客模型的时候看到的这种方法,但其实用信号量就可以解决这个问题.
项目架构
HW5
UML图:
协作图如图
可以看到我的构造相对来说比较简单。首先是最开始的main线程,创建了request线程与producer线程后一次创建六个elevator线程。也就是说,main是所有线程的父线程。首先在做完三次作业后我才发现,这种做法是不对的,究其原因是我做第一次作业的时候甚至不知道原来线程还可以创建子线程。我以为线程之间只有平行关系没有父子关系。所以不得不构造出这么一个“四不像”的东西。这种做法的扩展性就不强,也因此在第二次作业中增加了增减电梯的需求后,我立刻着手更改了我的架构。而且由于我只想到本次作业的六部电梯,所以直接一股脑创建了六个线程,而不是采取for循环的方式,总之反思就是这种架构非常不理智且无太高的拓展性。
HW6
UML图
时序图
本次的架构如图所示,首先main线程创建RequestTable和producer线程,在producer线程中我们先初始化创建了六部电梯,之后我们根据输入的需要增减电梯线程。这次吸取了之前的教训,增加了扩展性。
HW7
UML图
时序图
在本次的架构中,因为我们要调度好电梯的运行楼层,不能再像之前直达了,在这里我采用的是Floyd算法,所以我新定义了一个graph类,里面包含的是一些关于我所需要的图论和Floyd算法的实现过程,这个类明显也是一个共享对象,需要所有电梯共享。在时序图方面,跟hw6的是一样的。
调度策略
我采用的是look策略+自由竞争,没有使用调度器。首先一个原因是调度器由于人工的问题,有的时候可能并不会得到更好的结果,因为你无论采用什么调度策略,总能够找到一个例子,让你这个策略的电梯乘客等待时间比自由竞争慢。当然了,自由竞争带来的坏处就是电梯耗电量太多了,这一点是我之前所没有考虑到的。
电梯竞争的节省时间技巧
我们知道在同一楼层电梯很有可能会竞争同一个乘客,那么电梯有且只有一个抢到了共享对象来添加需求,由于我们采用的是自由竞争,电梯之间不知道自己有没有抢到乘客,电梯自己只知道它在这一楼层应该开门了,但是开门时间是被浪费掉了,所以我们不放采取一个二次重入的办法。也就是当前楼层所有被指示有条件搭在乘客的电梯都要再次竞争共享对象,然后只有竞争到的电梯才会开门,这样其他电梯因为没有抢到乘客所以会直接继续运行,节省了时间。
具体做法如下
if (this.getActivity().equals(Activity.OPEN)) { //应该在这一楼开门的话 synchronized (table) { //竞争共享对象 if (this.getActivity() == Activity.OPEN) { //如果电梯的策略要在这一楼开门 open(); //进行乘客的进出操作 } else { //否则继续进行while循环获取策略 continue; } } }
HW5与HW6的策略
由于这两次的电梯没有换乘的需求了,所以采用同一种策略。当电梯运行到这一楼后,判断一下是否开门(条件为:有乘客要出门 或者 需求队列里有乘客在这一楼想要搭电梯且和电梯目前运行方向一致)
当然了,为了保证电量不浪费的问题。如果这个电梯里的乘客全部下电梯了,并且需求队列里面没有新的需求了,电梯线程就进入wait状态。如果需求队列里无需求且电梯无乘客且input为NULL,自动结束该电梯线程。
HW7的策略
由于HW7新增了电梯需要有可达楼层这个需求。我们不得不考虑使用图论的知识解决问题。我所采用的是Floyd算法。每当增加或者维护电梯,我们都需要维护Graph中的图矩阵(邻接矩阵)并且我们都要重新计算一遍最短路径图矩阵。电梯的策略改变主要来自于此处,电梯的开门策略变为了:有乘客要出门 或者 这层楼有乘客,且本电梯的可达楼层中又一层能够缩短当前楼层和最终目的地楼层的最短路径。这样保证了每次的换乘都可以使得乘客能减少”最短路径“(不能是物理上的路径,因为有的情况是两层楼明明很近,但是由于电梯的可达楼层,必须进行多次换乘,导致最后的总路程很长)再加上输入的电梯必定能保证所有乘客可达目的地这个条件,所以我们必定可以乘客到达目的地并且每次换乘都是距离目的地更近了,这防止了无效换乘导致时间变长的问题。
BUG分析和Hack
HW5
这次的中测看起来很弱,导致我强测在忘记了电梯还有最大载客量的情况下竟然还能够让我过中测,因此强测理所当然地寄了,可见读题真的很重要,但是同房间因为是c房间,在我发现我这个自己的bug后我也拿这个bug去hack别人,达成了6/7的逆天成就,可见如果你轻轻松松hack了别人,那么你得小心自己也有一些严重的bug
HW6
本次没有任何bug,但是我在课下的时候发现的一个不小的bug是,原本在HW5的时候我判断一个电梯线程会被结束的条件是需求队列里无需求且电梯无乘客且input为NULL,自动结束该电梯线程。但是这个条件在本次作业是有问题的。因为当一个电梯还有乘客,但是他被维护了,那么他就会导致乘客被重新放进了需求队列。但是其他的电梯都已经结束了线程,就会导致这个乘客无法到达目的地。这个问题最简单粗暴的解决方式是直接强制所有电梯都一起结束。
HW7
本次作业有不小的bug,是REAL_TIME_EXCEEDED,原因是多部电梯在Floyd算法更新图的时候导致的线程冲突。更改后就修改了。
心得体会
“电梯月”是终于过去了,在这一个月的学习中,从零开始学习了多线程的相关知识。有许多的收获,实现了对一个知识版图的完整获取。
多线程的不可复现性
debug很痛苦,因为多线程的bug是很难重现的。你甚至不知道这个bug到底是你不小心输错了内容还是真的有bug,我HW7就碰到了凌晨1点找到的bug结果一觉醒来就过了的情况。但是这不能掉以轻心,不是图灵老爷子在天之灵帮助你修复了bug,只是多线程让你侥幸逃脱了评测机的绞杀 最好的办法当然是通过暴力的评测机强行找出bug啦,但是由于我是菜鸡,我连完成作业都心力交瘁,所以评测机当然是白嫖了各位大佬同学的了(在此对鹿同学和石同学的评测机表达感谢!)
不要重复造轮子
在HW7的时候我没有了解信号量的知识,所以我直接相当于模拟了一个“信号量”的对象来实现电梯塞满的问题。但是我身边有几位同学也跟我一样没学过信号量,但是也敏锐地在周五的上机之前察觉到了应该有一种方法来实现多个线程的同时载入。于是他们就搜到了信号量,然后就快速地完成了HW7,虽然效果是一样的,但我花费了更多时间却得到了更多的bug。可见前人的经验在我们写代码的时候不放可以先搜搜看有没有
感谢
感谢有这么一个机会让我提前学习了多线程的相关操作,我之前就有了解到大厂什么高并发之类的技术,一直没学过也一直没有途径了解。这个月做的很累,多次搞到凌晨一点两点,但是收获也是显著的。多线程是从零开始的一次新的知识体验,相信在未来的学习和工作中会让我有更多的益处!