BUAA_OO_第二单元总结与反思

锁的应用

  • 读写加锁
synchsynchronized (outlist) {
	boolean a = inup_h(nowpos);
}
------------------------------------------
public boolean inup_h(int nowpos) {
    for (PersonRequest personRequest : list) {
        if ((personRequest.getToBuilding() - 'A') > nowpos) {
            return true;
        }
    }
    return false;
}

这个方法所遍历的list是一个共享变量,在调用它时如果不加锁,可能会遇到在for循环遍历过程中list被其他进行写改变的情况,倒是迭代器错误,for循环遍历错误。

  • 两个代码块有逻辑关联加锁
synchronized (outlist) {
	if (outlist.isEnd() && outlist.isEmpty() && inlist.isEmpty()) {
		break;
	}
    if (outlist.isEmpty() && inlist.isEmpty()) {
        try {
        	outlist.wait();
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
    }
}

这是电梯线程里面用于判断是否结束电梯线程和判断是否进入等待的两个if。outlist.isEnd()为真表示输入已经结束,outlist.isEmpt表示该列表为空。

代码逻辑就是先判断输入是否结束且请求列表为空,如果是就结束电梯线程,如果没有结束,继续判断是否请求列表为空,如果是就进入等待,等到输入线程有新的请求或者输入结束就会唤醒它。

假设我对它们分开加锁

synchronized (outlist) {
	if (outlist.isEnd() && outlist.isEmpty() && inlist.isEmpty()) {
		break;
	}
}
synchronized (outlist) {
    if (outlist.isEmpty() && inlist.isEmpty()) {
        try {
        	outlist.wait();
        } catch (InterruptedException e) {
        	e.printStackTrace();
        }
    }
}

这样会出现一个问题,假设请求列表为空,同时恰好在执行完第一个synchronized块结束,还没有进入第二个synchronized块的时候,输入线程结束。在第二块中电梯就会进入等待状态,而且由于输入线程已经结束,所以它得不到输入线程的唤醒,然后电梯线程就一直等待无法结束了。

所以说要对有逻辑关联的代码块加锁。


调度器与程序线程之间的交互

  • 第五次作业没有调度器。

  • 第六次线程需要和调度器进行通信的有输入线程和电梯线程。



Controller是第六次作业的调度器。

输入线程中具有Controller属性,直接调用Controller的dealperson和deadelevator的方法把人的请求和加电梯的请求传递给调度器。

Controller通过dealperson方法向letterlists和numlists中加入请求,而电梯线程和调度器共享这两个list,所以电梯线程和调度器可以通过lists进行通信。

  • 上图是第七次作业的Controller调度器,与第六次不同的地方在于设置了单例模式,所以输入线程不再设置Controller属性,而是通过单例调用方法。

架构分析

各次作业的UML类图和UML协作图

hw5


hw6


hw7


三次作业架构的迭代

三次架构是逐渐迭代的,下面采用通过生产者消费者的方式进行说明

第一次作业输入线程InputThread是生产者,读入请求并根据楼座分发给相应的电梯线程的列表Requestlist,电梯线程内通过调用方法访问Requestlist完成请求。

第二次作业与第一次作业可以说是食物链多了一级,输入线程InputThread作为Controller的生产者,读入请求后交给Controller进行处理分配,Controller作为电梯线程的生产者将请求分发给对应的楼座楼层的Requestlist,电梯线程通过方法访问Requestlist完成请求。

第三次作业与第二次相比可以说食物链级数没有增加,只是作为生产者Controller多了一个消费者Trans,Controller将需要分解的有换乘需求的请求传递给Trans,Trans这个请求的中转楼层,然后Controller再根据中转楼层分解请求,再发给电梯线程。另外就是多了一个用于查收请求的类Requestcounter。

未来可能的扩展方向

  1. 可能增加其他类型的电梯

    可以增加一个新的电梯线程类,然后修改Controller对新增电梯分发请求。如果各类电梯可以共用一些方法,可以建一个电梯类,让各类电梯继承这个电梯。

  2. 可能采取新的策略

    我的电梯的策略是通过电梯线程的方法实现的,基本都在flushlist和flushdirection中,flushlist是为电梯开关门进人出人做决策,flushdirection是为电梯选择方向做决策。采取新的策略基本就是改写这两个方法。

  3. 新的请求

    在Controller,也就是调度器中增加新的对应的请求的处理方法。


出现的bug和解决方法

只有第一次作业出现了bug。

强测bug:问题出在超载上,有一个方法写错了,是比较低级的错误,删改了一两行就过了。

互测bug:1.在互测过程中出现了输出不安全的bug。就是在调用官方包的时候没有加锁,导致输出时间戳不递增,加锁就解决了。2.自己发现了for循环遍历共享列表变量时没有加锁,加锁解决了。


发现别人程序bug所采用的策略

只有第一次作业发现了bug,采取的策略就是发现了自己的bug后,用自己的bug测别人的bug。

发现了同房间内的同学很多都有输出不安全的问题,然后我用下面这样的样例hack他们。

[4.0]1-FROM-D-1-TO-D-2
[4.0]2-FROM-D-1-TO-D-2
[4.0]3-FROM-D-1-TO-D-2
[4.0]4-FROM-D-1-TO-D-2
[4.0]5-FROM-D-1-TO-D-2
[4.0]6-FROM-D-1-TO-D-2
[4.0]7-FROM-D-1-TO-D-2
[4.0]8-FROM-D-1-TO-D-2
[4.0]9-FROM-D-1-TO-D-2

就是在同一个时间投入较多的相似的请求,这样这些人进电梯,出电梯这些时间都会非常接近,输入IN和OUT信息的时候就容易触发输出线程不安全的bug。

这一单元的hack策略和上一单元不同的在于这一次大家出现的问题基本是差不多的,都是线程安全方面问题大,就直接观察别人加锁的情况来找出bug。


心得体会

  1. 线程安全:线程安全问题我认为就是加锁的问题,假设在两个线程里面分别有一块代码,我用一个同样的对象把两块代码锁住,就可以防止一块代码在执行的过程中插入另一个块中的代码。

    • 线程安全的问题总是出现在共享对象的读写上,比如两个线程同时写一个list,就可能因为汇编代码的调度问题写入错误的结果,还有就是一个线程在for循环遍历一个list,另一个线程在写这个list,可能会出现迭代器错误。所以在对共享对象进行读写时要注意加锁。
    • 另一个需要注意的是当代码块之间有逻辑上的联系的情况,假设把一个函数中分成两个代码块,两个代码块都有对共享对象的读写,所以我对两个代码块都分别锁,但是两个代码块之间存在一些逻辑关系,可以假设说第二个代码块需要第一个代码块读共享对象得到的结果,这样的逻辑关系。因为我只对两个代码块分别加锁,也就是别的线程可能在两个代码块之间插入了其他线程的改变共享变量的代码。这样第二个代码块得到的第一个代码块的读共享对象的结果就是过期的了,这种问题应该是要避免的。我的解决办法是一开始就把两个代码块一起用一个锁锁住。这种锁我总结是为了防止共享变量的状态改变。

    总结来说,我认为加锁就是为了防止一块代码里面混入不该混入的代码。

  2. 层次化设计

    这一单元的层次化设计,就只能说生产者消费者这种设计模式或者说是思想。在本单元里我认为,一般是输入线程是调度器的生产者,调度器是电梯线程的生产者。在此基础上还可以有一些别的类比如帮助分解请求的分解类,调度器又可以是分解类的生产者。

    通过这种生产者消费者的设计模式,可以比较清晰地帮助我理清各个类的职责和它们之间的关系。

posted @ 2022-05-01 22:33  20373467dyt  阅读(34)  评论(1编辑  收藏  举报