OO第二单元博客

OO第二单元博客

架构设计

线程设计

在我的框架里面,算上Main,一共有四个线程,Main线程中,将所有线程进行初始化。InputHandler将request进行分类,放到WaitingRequests中,而Scheduler则将WaitingRequest进行分发,放到每个电梯的ProcessingRequests中,交由Process线程进行处理请求,调度电梯。

在第二次以及第三次作业中,增加了reset请求,因此,需要在reset时Process要将ProcessingRequests中的内容放至WaitingRequests中。

共享对象与锁

从线程的协作图中可以看出,共享对象包括WaitingRequest & ProcessingRequest,因此关于这些类内部的属性的读写,都应该加上锁。在这之中,只使用synchronized就已经可以完成要求,但是会有一些性能上的损失。比如我的WaitingRequest中,有两个ArrayList,一个是PersonRequest的,另一个是ResetRequest的。对于两个线程分别操作两个arraylist的情况,其实是不用去加锁的,而使用synchronized便会让两个线程同步。除此之外,还有一种则是同时读时,也会被锁上。因此,设置两个读写锁,分别保护两个列表,会是最好的选择。

Scheduler

在第二次迭代中,我曾尝试过影子电梯,即根据当前状态计算得到时间开销,选择最小的,进行分配,由于CPU的计算速度较快,因此即使计算,也不会出现时间上的问题。然而,当时,第二次写多线程并发程序,对于锁的理解并不透彻,导致此过程中出现电梯的克隆和计算过程中,出现了死锁的问题,使得电梯并不能正常结束。因此先采用了简单的随机策略,先完成其他的内容要求。

在完成了其他内容后,回过头来修改影子电梯时,发现了我写的有些问题:首先是,逻辑上的问题,我只考虑最快接到这一乘客,只考虑时间开销中的一部分,而为考虑全。其次,整个影子电梯相当于是一种贪心算法,在每次计算中,选取当前的最快,但不一定是整体最快。而我写的最快接收乘客的指标这一问题尤为严重,第一次的最快接受,可能会在第二个乘客请求接受后,变成并不是最快,因此这样的办法在第二次作业中便放弃了。最后采取的随机策略。

GetAndRemove

对共享对象即requests的查询,要保证一个查并删原则,就是说,在查询到一个对象,并且返回这一对象后,就应将他从列表中删除。这个原则在第一次或许没有什么体现,因为每个请求表只对应一部电梯,但是对于后面来说,其实还挺重要的。它在某种程度上和这次新增的receive是一样的,因为如果你不删除,可能会导致你当前想去接的人,可能已经被别的电梯接走了,最后要么是你白跑一趟,要么是一个人做了两趟电梯。前者只是性能分的丢失,而后者就是正确性的问题了。

单例模式

实现

我的单例模式主要是为了判断线程是否该结束以及是否该沉睡唤醒。在第一次中,没有reset,waitingRequests空了,scheduler就可以wait在waitingRequests里面了,输入结束并且waitingRequests为空,就可以结束了。而第二次有了reset,原来的请求有可能重新放回waitingRequests,不能随便结束。因此设置了一个Count的单例模式,每次waitingRequests加入请求,值会增加,电梯送到,或者中途下人后,值会减少。沉睡也可以直接wait在Count的单例对象中,每次更改count值,就会将其唤醒。

至于为什么要采取单例模式,原因主要有二,一是因为要static,这样便不需要将其传来传去,即可直接访问,修改较少。二是因为如果只声明一个static的类属性,而不声明具体对象,会导致无法wait在Count上,因为wait的地方要是一个具体的对象,而不能作为一个类。

反思

在我的架构中,其实关于scheduler类也是全局只有一个,也应该采用单例模式,如此一来,也可以节省很多不必要的对象引用。

迭代分析

易变内容

在三次迭代中,更改的最多的,应该时Scheduler线程类和Process线程类。

第二次迭代中,Scheduler经历了较大的修改,原来是只需要找到请求中对应的电梯id,进行分配即可,而第二次作业则是要求自己进行分配,因为当时写时没有使用buffer来做请求的缓冲队列,因此,得获得电梯的reset状态来看电梯是否该分配给该电梯。而第三次迭代,也在此基础上修改了是否能够分配的判断条件。

process类的改变,大体上可以算得上增量开发,第二次在每次循环中增加了reset的相关函数,第三次在reset中增加了一个新的DCreset分支。

稳定内容

在三次作业中,电梯运行策略是最稳定不变的

电梯运行的总体逻辑是,从最底层到最高层,再从最高层到最底层,这样循环往复,最低与最高,并不一定是1与11层,而是当前方向上没有向前的必要了,即该方向上还有没有请求,并且本层乘客没有该方向运动的需求时,本层就是最高或最低层。

每一次运动前要有三个阶段:下乘客,更新运动方向,上乘客。

  • 下乘客:所有目的地是本层的乘客下电梯。从电梯的乘客列表中删除

  • 更新运动方向:该方向上还有没有请求 && 本层乘客(包括电梯)没有该方向运动的需求时 && 到达电梯的最高与最底层

  • 上乘客:捎带策略,取出该层与运动方向相同的乘客,上电梯。

双轿厢电梯

启动

我是在main函数之处,便创建了12个process线程,即12部电梯,为了区分reset前与reset后的电梯,最初创造的电梯命名为CD,让C电梯进行工作,而D电梯一直处于wait状态,在得到DCreset请求后,再将其从wait中唤醒,开始运作。

分配

由于我是在初始时,设置了12个电梯,并且使用arraylist来保存,因此,将随机数改为1-12,random / 2向上取整则是要分配给的id,random的奇偶表示该给A电梯还是B电梯,每次分配后,判断请求是否能被该电梯接受,因为电梯分了AB,部分乘客由于起始层的问题,并不能放至该该电梯中。

具体逻辑如下

public synchronized boolean canReceive(PersonRequest request) {
    boolean ret;
    if (eleClass == 'A') {//A电梯不能接受高于sharefloor的请求
        ret = request.getFromFloor() < shareFloor ||
            request.getFromFloor() == shareFloor & request.getToFloor() < shareFloor;
    } else if (eleClass == 'B') {{//B电梯不能接受低于sharefloor的请求
        ret = request.getFromFloor() > shareFloor ||
            request.getFromFloor() == shareFloor & request.getToFloor() > shareFloor;
    } else {//C电梯可以接受所有请求,D不可以
        ret = eleClass == 'C';
    }
    return ret;
}
防撞

为了防止碰撞,需要在两个电梯之间有“通信”。而对于通信的方式,我采取了设置一个共享对象,通过设置其Occupied的状态,来协调处理,其实就是os中的信号量。

//在进入该楼层前,先调用occupy设置锁
public synchronized void occupy() {
    if (isOccupied) {//如果别人已经占用,则等待
        try {
            wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    //唤醒后设置为true
    isOccupied = true;
}
​
//离开该楼层后,release释放掉占用,唤醒沉睡的内容
public synchronized void release() {
    isOccupied = false;
    notifyAll();
}

此外,还需考虑到一种情况,即一个电梯在换乘层停下后,电梯为空,并且没有被分配到请求,那么他将一直占用这一换乘层,导致另一个电梯被卡死。因此,为了解决这一问题,我在判断电梯是否移动时,增加了一个条件,如果该电梯在换乘层,那么不管有没有请求,都会再移动一次,离开换乘层,但是有些性能的损失。

反思

这次的双轿厢设计还是有些臃肿,通过和别的同学讨论后,发现使用hashmap<eleId, process>来保存电梯会更好。这样一来,二次电梯的启动可以放到读到DCreset请求时,新建一个电梯放到hashmap中。分配方面,仍然使用1-6的电梯id进行分配,选好id后,取出对应的电梯,若是单电梯,则直接放到对应的请求队列,若是双轿厢的电梯,则通过sharefloor限制关系,将其放至AB其中之一。

BUG

线程不安全

未对共享对象进行完全的保护,导致在一个线程遍历时,另一个线程进行修改,从而出错,可以直接找到对应的方法,加锁基本上能解决。

不必要唤醒

在第三次作业中,接受DCreset的两部电梯是同时启动reset,但是,由于原来正在工作的电梯1,有可能在此过程中开门,因此沉睡时间可能是1.2s也可能1.6s,因此,我采取让电梯2wait在两个电梯的写作对象Coworker中,等待原先的工作线程电梯1完成reset后从中将其唤醒。

但是由于电梯2进入wait的时间太早,导致电梯1有可能在处理请求,并且离开换乘层,因此会提前唤醒,导致出现不必要的bug。

因此对于已经有明确功能的对象和锁,要慎重的去复用!!!

Buffer缓冲队列

在第二次与第三次的作业中,经常会出现某些奇怪的数据,eg,同时reset5个电梯,只有一个电梯能运作,那么部分分配策略(reset时不给其分配请求,而是跳过再找一个)会将此时进来的所有请求全部给剩下的一个电梯,因此会导致负载不均衡,甚至超时。

而解决这个问题的策略有两种:

  • 每分配一个请求便wait一段时间,使得在reset过程中不会只会分配有限个请求,大幅度减少负载不均衡。然而,其中可能会存在一个问题,即由于每次分配数据后便wait一段时间,如果接受reset时,策略中有可能会运动两次再响应reset,因此wait有可能导致不能及时响应reset,而致使reset后移动三次。这个方法会损失一些性能。

  • 使用buffer策略,即将请求放到processRequests里面的buffer(Array List<PersonRequest>)队列中,每次电梯运动取请求时,先将buffer队列清空,全部转到正常队列中,并输出receive请求,而在reset时,只清空buffer,将其归还于waitingRequests,也可以不返回,之后继续处理。此时在分配时,便不需要考虑是否在reset中。

死锁

两个线程或多个线程同时访问多个资源时出现的互相等待的现象,在oo中常见的应该是哲学家就餐问题,即五个人桌上五根筷子,吃饭时,要定一个拿筷子顺序,放至一人拿一根筷子死等别人放手的问题出现。而我觉得比较适用于oo的,就是对要同时访问的多个资源上锁,比如刚才的问题中,给五根筷子排个序,每个人必须要先拿序号小的筷子,后拿序号大的,这样,要拿1,5筷子的和拿1,2筷子的两个人,需要对1号筷子进行竞争,只有竞争成功的人才能拿到筷子,此时,四人拿五根筷子,一定有人拿到了两根筷子,破处了循环死等。

DEBUG策略

打印输出

单独建一个debug类,里面包括静态方法和静态变量,调整isDebug属性就能控制整个程序的debug信息输出与否,较为方便。

线程起名

在调试的过程中,有时需要切换线程,或者打印输出时输出线程名,为了好区分,可以通过以下两种方式为线程进行设置名称。

大量测试

对于线程安全问题,需要进行大量的数据测试才能测出。经过实际测试发现,在测试的过程中,开始多进程并行进行多个测试,可以更容易出现线程之间的不安全问题。而对于一些常见的bug以及解决上面已经给出。

思考

向优秀学习

在写的时候,不能固步自封,多向周边人学习,多与他们讨论,将自己的丑陋的框架逐渐修改的优雅。渠道很多样:

  • 学长博客

  • oo课上实验

  • 同学讨论

  • 互测房直接下源码

线程是过程

线程是一种管理类!他是一种过程,而不是一个有着很多属性的对象,我们可以想一下主线程,即mainclass我们将很少向其内部加入属性,而是在其内部使用变量时去声明他,而我们的线程也应该这样,减少具体的内容属性,将其封装,甚至是只在run内声明并使用。所以我们的电梯与处理电梯的线程应该像这样。

而非这样

收获

最大的收获,还是通过自己的亲身实践,明白了并发线程的构造与工作原理,之前由于参加超算比赛,学过一些并行计算的多线程方式。最初以为,并行与并发基本类似,但是通过亲身实践才发现,并行只是并发的一种特殊体现。对于并行要求多线程之间执行同样的代码逻辑,而并发只是多线程协作。

同时,通过本单元的学习,明白了同步的含义,即让之前的无序的线程按照一定的顺序执行,而非同时。对于同步锁的设置,在逐渐的学习中,有了更深的理解。

之前对于各种模式,认为其只是一种特殊的命名,并没有什么特别的,但是在本次尝试单例模式后,我好像突然发现了设计模式的重要性,它是某些要求下的最简便,最安全的一种写法,在接下来的过程中,要多学习设计模式,并且多使用它们,争取做到融会贯通。

  • 30
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值