BUAA OO 第二单元总结

一、前言

​ 本单元学习多线程,难点主要在于共享资源的互斥访问,同时要避免轮询和死锁。第一次作业指定了电梯,着重关注线程整体设计以及单部电梯的执行策略;第二次作业取消了指定电梯,由自己的调度算法来确定请求分配方式,同时加入reset操作,加深对锁的认识;第三次作业,在此基础上增加了双轿厢电梯,我们需要保证两个轿厢不能同时位于换乘楼层,同时对于一些前两次作业使用的分配策略也产生了影响,需要重新设计、规划。

​ 就完成情况而言,我第一次作业又双叒叕做得很痛苦~归结于多线程基础知识的预习不够到位,以及一些同步问题似懂非懂,直接看往年的博客又太繁琐了。不过,在实验课之后,拿到了非常漂亮简洁的实验代码,就直接把架构拿来用了。第二次作业,相对简单一些,主要问题就是分配策略和reset的处理方式,bug产生也是一些线程并发执行时,时间戳先后问题。第三次作业……寄!由于本人代码习惯不好,写着写着就一团糟了,于是被各种奇奇怪怪的不知道哪里冒出来的bug搞得焦头烂额,ddl前没能过中测,算是一个比较大的遗憾吧……

二、架构分析

1、 总体架构

线程设计

在这里插入图片描述

​ 从标准输入中读取带时间戳的输入,将请求分为两部分:personRequestresetRequest。其中,personRequest放进线程安全的类waitQueue中,resetRequest直接经由handler分配给各部电梯。这是因为reset是收到后立即就可以执行的,而正常请求需要等待电梯空闲等条件才可以被执行。这里InputThreadSchedule构成一个生产者-消费者模型,生产者是InputThread,消费者是Schedule,而之间的流水线是waitQueue,所以waitQueue的类型必须是实现了互斥读写的MyRequestQueue类,而不能用不安全的ArrayList

​ 同样地,Schedule和每个Elevator之间也构成了生产者-消费者模型,传递的对象是elevatorQueueSchedule类负责将普通请求分配给不同电梯,而ResetHandler类负责将重置请求分配给请求指定的电梯。在第六次作业之后,加入了reset,那么,从一个方面来看,已经被接收到elevatorQueue中的request,是有可能被撤回的,而撤回后又必须保证不能立刻被分配回来,这样就占用了大量cpu时间,产生无意义的循环;另一个方面,在elevator reset时,不能执行请求,但如果把这个作为调度器的判断条件,就会产生往一部电梯狂塞请求这样一种bug。所以,在电梯reset时,我同样会为它分配请求,但是此时的请求就会被加入requestToReceive,等到电梯重置结束时,统一把这个容器类里面的请求copy到elevatorQueue中,实现在reset时不进行电梯运行操作。

类图

在这里插入图片描述

​ 先说缺点吧,我这次作业设计的架构可以很明显地从图上看出来,把很多方法和属性都堆在了电梯类,同时,电梯类实现Runnable接口,还需要一个run方法,这整个类就显得繁杂冗长了 (差点就超行数了) 。同时,还有一个缺点就是Elevator类和DecisionMaker类分工不是很明确。一开始的想法是DecisionMaker负责判断Elevator执行哪个操作,然后由Elevator调用特定方法,修改自身属性等。但是,在DecisionMaker里需要判断的条件,比如ElevatorQueue是否为空、是否结束这一类条件,在写运行方法时,还需要再次判断,以正常遍历容器。也就是说,同一个条件被两个类判断了两次,是不必要的重复,也导致了一些后面迭代时的逻辑混乱。

​ 具体到最后一次迭代,也就是第三次作业。先从InputThread中读取一条请求,如果是reset请求,则直接分配给特定电梯执行;如果是普通请求,加入waitQueue传递给Schedule线程。Schedule线程计算每个request最佳去向,将waitQueue中的请求remove出去,add进特定的elevatorQueue中。然后在Elevator线程中,采用Look算法,实现单部电梯的运行逻辑。当普通reset到来时,开门放人,同时,没完成的request都放回waitQueue中重新分配。双轿厢reset,创建一个“新的”电梯,设置始末楼层。同时为当前和新的电梯设置Occupied对象用于对换乘楼层的互斥访问。特别地,当双轿厢的一方停止在换乘楼层时,立刻转向移动一层。

2、 同步块和锁

同步块

synchronized关键字可以用于创建一个同步块,以确保在同一时间只有一个线程可以执行某个代码块。

其中,作用于不同作用域可以对不同对象上锁:

​ 当synchronized关键字修饰一个非静态方法时,它锁定的是调用该方法的对象实例。

​ 当synchronized关键字修饰一个静态方法时,它锁定的是该类对应的Class对象。

​ 当synchronized关键字修饰一个代码块时,此时需要指定一个对象作为锁。

public class Example {  
    private Object lock = new Object();  
  
    public void Do() {  
        synchronized (lock) { // 使用lock对象作为锁  
            do something; 
        }  
    }  
}

java.util.concurrent.locks.Lock接口和它的实现类(如ReentrantLock),可以实现更灵活的线程同步。

import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  
  
public class Lock {  
    private Lock lock = new ReentrantLock();
  
    public void incrementCount() {  
        lock.lock(); // 获取锁  
        try {  
            do something; 
        } finally {  
            lock.unlock(); // 释放锁  
        }  
    }  
}

还可以特定读写锁,缩小锁的范围。

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyReadWriteLockExample {
    private int data = 0;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public int readData() {
        lock.readLock().lock(); // 获取读锁
        try {
            return data; // 读取共享资源
        } finally {
            lock.readLock().unlock(); // 释放读锁
        }
    }

    public void writeData(int newValue) {
        lock.writeLock().lock(); // 获取写锁
        try {
            data = newValue; // 写入共享资源
        } finally {
            lock.writeLock().unlock(); // 释放写锁
        }
    }
}

我在这次作业里用的基本都是synchronized关键字,保证在每个用synchronized修饰的方法最后都notifyAll一下。

3、 调度器设计

​ 调度器,这里是指我的Schedule类,负责分配请求。看了前几年学长学姐们的博客,发现均分策略好像是可以通过的。于是我就在均分上稍微做出了一点点优化。以第二次作业没有双轿厢电梯为例,具体地:

​ 每六条request一组,第一个进来的request通过calculate公式(是自己根据请求,电梯运行状态随便写的一个算式,用来评估分配给该电梯时性能高低),找出一个最合适的电梯,塞给它。然后,将这个电梯暂时从电梯队列“移除”。这样,在分配第二个request时,只剩下五部可供选择的电梯了。就这样一轮六个分配结束后,再将所有电梯“释放”出来,匹配下一组六条请求。这是一个均分和最近的折中算法,旨在……避免针对模6数据以及全塞一个电梯的极端情况。

4、 可扩展性

​ 这里对于两种电梯都加上了起始楼层和终止楼层的属性,并配置了访存方法。如果未来的reset还能够更改电梯执行楼层范围(比如一些酒店的电梯),或是只停靠奇数/偶数楼层(这个可能就需要增加数组了),一样可以通过set和修改if判断条件来实现。

三、bug分析

1、 三次作业的bug

​ 第一次作业,由于对多线程还止步于一个了解状态,没有做好线程安全,导致了在使用迭代器遍历容器时,容器的内容同时发生了改变,导致re。修改方式就是让所有容器都实现线程安全的访存方法。 (当然,我最后偷懒用i去遍历了)

​ 第二次作业,互测是非常经典的5个reset,然后塞一堆请求的数据点。产生错误的原因是在电梯调度时,无视了处于reset中的电梯,而是去比较剩下电梯的性能,从而做出选择。(其实有想到全部reset的情况,这时候wait一下就可以了,但是完全没想过全给一部电梯的rtle问题)

​ 第三次作业,逻辑混乱寄得惨烈,甚至出现了一些离谱bug,如下↓

在这里插入图片描述

后来debug时候发现还是同步控制没有做好,以及一些记录型变量未能正确记录当前线程运行状态。

2、 hack方式

​ 因为本人比较菜,这次只进了第二次作业的互测。然后就随手捏一个把6个电梯全reset了然后紧接着输入一条request的数据,刀了3个(思考ing)。这个是自己在写策略时发现的一个小问题,电梯都在reset的话要wait一下~~当然,加入之前提到的requestToReceive就没事了。

3、 debug方法

​ 最主要的还是print大法啦~~首先改写一下toString方法,然后打印各个中间变量,或者监视电梯状态,看看究竟是在什么情况下产生了比较离谱的情况(比如电梯乱飞等)。

​ 特别地:CTLE一般是出现轮询,这时候在每个run方法的循环中打印一下线程名字,看看哪里一直在不断循环占用cpu资源。比较好的解决方法是找到共享对象使用wait,比较简单粗暴的解决方法是直接sleep线程。

​ RTLE我主要遇到两种情况。第一种是调度策略不当,这个在上文解释过。还有一种就是kill线程的条件设置有误,导致有线程无法结束,程序卡死无法退出。这种一般是打印出kill掉的线程,看看是哪个(一般是电梯)没能成功退出。然后再进一步查询,是end设置有误还是资源计数有误等等。

四、心得体会

​ 多线程这一单元总体来说还是很难的!!比起第一单元,最大的难点是不清楚程序执行的过程,好多bug都无法在本地复现,也就不知道错误原因是什么了(只能瞪眼法)。不过,经过两次实验,也是对本来一头雾水的锁、同步控制等知识稍微有了点想法。实验代码真的是一个很伟大的东西!简洁易懂,同时对于作业还能够提供思路上和语法上的帮助。

​ 挂了一次,以后要努力了……!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值