2024春 BUAA-OO-Unit2
文章目录
同步块设置和锁的选择
为解决线程安全问题,本单元中我们要使用锁来管理多线程对共享资源的访问,并常使用wait()与notifyAll()来使线程阻塞或重新就绪。
目前我了解或使用到的管理多线程对共享资源的访问的方法有三种。
synchornized 关键字
在 java 中,synchronized
关键字是用于实现原子操作的常用方法,其本质是一个对象锁,可以用它来修饰实例方法、静态方法,也可以用来修饰代码块。
public class A {
Object lock;
public static synchronized void fun() {
// synchronized修饰静态方法,锁对象是 A 类的本身
}
public synchronized void fun() {
// synchronized修饰实例方法,锁对象是 A 类的一个实例,等价于synchronized(this)
}
public void fun() {
synchronized (lock) {
// synchronized修饰代码块,锁对象是lock
}
}
}
ReadWriteLock 读写锁
synchronized
关键字是非读写锁,此时同步区代码至多只有一个线程执行。但数据冲突只有读-写冲突和写-写冲突两种,每次只让一个线程对同步区代码进行访问,这降低了程序效率。
读写锁相当于在非读写锁的基础上放宽了线程执行同步区代码的条件,即同步区允许存在多个读者执行。
读写锁是java中自带的一个类,可以直接使用。
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
信号量思想
在hw5中,线程结束设置较为容易,当InputHandler
结束且RequestTable
为空时,电梯线程可以结束。
在hw6和hw7中,由于电梯重置,乘客请求会被返还,所以需要寻找新的线程结束方法。对此我们可以借鉴os中信号量的思想,创建RequestCounter
类,由请求放置线程InputHandler
和请求处理线程Elevator
分别对其进行PV操作,最后确保在所有请求都顺利执行后结束线程。
调度策略
hw5
未设置调度器,根据输入要求进行分配
hw6
完成作业时:采用调参法,加权考虑电梯内人数、请求队列内人数、层差和速度。我构造了一组可能的电梯情况,对权重进行了简单的配置,但总体来说对于权重的设置较为随意。
pNum(0.4) | waitNum(0.4) | delta(0.1) | time(0.1) | |
---|---|---|---|---|
1 | 4 | 4 | 2 | 0.5 |
2 | 3 | 3 | 4 | 0.4 |
3 | 6 | 2 | 11 | 0.4 |
4 | 1 | 6 | 2 | 0.4 |
5 | 3 | 1 | 4 | 0.4 |
6 | 5 | 0 | 6 | 0.4 |
此外,调度器Schedule线程在分配请求时,如果某个电梯在reset状态中,就不给该电梯分配请求。如果所有电梯都在reset状态中,Schedule还需要等待。这种调度方法在性能上无疑没有任何优势。
bug修复时:受到其他同学的启发,发现在reset状态中的电梯其实也能接收请求,只是要在reset_end后再一起输出就可以,这样调度器可以不用因为电梯的reset而阻塞。
hw7
在完成作业时:调度策略与hw6保持一致。
bug修复时:在互测中所有被hack的数据均为49.9s时涌入几十个请求,而我的加权策略在这种情况下会将所有请求分配给同一个电梯,导致时间戳超过220s的问题。因此我还是将调度策略改为了人数均衡策略。
综合bug修复经历和性能分表现,我认为我的调度策略选择是比较失败的。在与其他同学的交流中,我发现还是潮流的“影子电梯”性能最好。
架构迭代
hw5
第五次作业的架构很简明,由InputHandler
根据PersonRequest
里的电梯id
投入对应的RequestTable
中。
hw6
第六次作业新增的需求:设计调度器+新增reset请求。
对于设计调度器需求
- 增加Schedule线程
- 增加WaitQueue共享对象
对于Reset需求
- Reset前,返还该电梯未处理完的请求到WaitQueue
- Reset时,接收的请求不直接放在RequestTable,而是先放入缓冲器
- Reset后,将缓冲器的请求挪入RequestTable
此外,新增RequestCounter
类来判断线程是否可以结束(当InputHandler
投入的请求全部处理完时允许Schedule
线程结束)
hw7
第七次作业新增的需求:新增双轿厢电梯。
为使架构尽量简洁,我将双轿厢电梯对外看作一部电梯。一位乘客的请求被一部双轿厢电梯m接收后,由m-A和m-B协作将乘客送到位。
这样一来,架构方面较hw6基本不变,hw7的重点变成了如何做好双轿厢电梯的协作工作:TransFloor楼层换乘 + 避免双轿厢电梯相撞。
UML类图
hw7的UML类图展示如下:
双轿厢实现
为避免双轿厢电梯相撞,我设置InTranFlag
类标志是否有电梯处于换乘楼层中,该类是同一id的两电梯A、B的共享资源。
public synchronized void arriveTran() {
isInTran = true;
notifyAll();
}
public synchronized void leaveTran() {
isInTran = false;
notifyAll();
}
bug分析
本单元很不幸在hw6和hw7中都参与了bug修复环节。总结来看,我的bug主要是两种:
System.out
安放的位置没有慎重考虑- 线程安全问题
对于第一种bug,出现原因本质来说就是我诚实地实现了在Reset_begin后不要Receive,却忽视了在Reset_begin之后不要输出Receive。
但有一些同学与我相反,他们在Reset_begin后仍然Receive,却注意在Reset_begin之后不要输出Receive。这是一种更巧妙的方法,既能确保输出不错,还简化了调度器的任务。
所以我也增加了缓冲器buffer来践行这种思想,修复了第一类bug。
对于第二种bug,我至今仍无法保证它不存在于我的程序中。由于多线程的无法复现性,甚至连printf
的debug手段也失去了效用,所以我的查找方式只能是仔细看需要加锁的地方是否加上了锁,以及理论分析可不可能出现死锁情况。
心得体会
线程安全
第一次接触多线程编程,由于对锁的使用理解不够深入,每次写上一个sychornized
都十分小心翼翼,因为到处写很容易引发死锁,但不写却觉得这确实是一个原子操作。
查找的多线程debug方法还是没法投入到实践中。更多的时候,只能看到评测机上一个RTLE,然后无奈地再在本地跑几遍,再看一遍代码,再没有发现什么问题,再交一遍又全通过了。
层次化设计
在本单元中,抛开线程安全不谈,我认为我的层次化设计完成较好。
我觉得好的层次化设计在迭代时应做到不改只增。这就需要我们在拿到新的业务需求时多考虑它与现有架构的关系,使新增的代码能够高效对应上新增的功能。
其它
本单元无疑是有遗憾的,比如找不到的RTLE问题可能找到了一部分,降低了RTLE出现的概率,但确定没有找到所有,比如表现平平的调度策略。
但是确实对多线程有了超过概念层面的认识,所谓实践出真知。在学习OS的时候也可以更加盲目自信,毕竟也是写过多线程的人了。
OO电梯月在我敲下这行字后就真的结束了,以后在很长一段时间都不会真正写多线程程序了吧,但是北京航空航天大学新主楼电梯我未来还会继续乘坐,希望它处理好我的PersonRequest。