BUAA_OO 第二单元总结

本单元的目标是模拟多线程实时电梯系统,熟悉线程的创建、运行等基本操作,熟悉多线程程序的设计方法。我们要模拟楼座中的电梯系统,楼座内有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息(起点层,终点楼层),请求调度器会根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。可以采用任何电梯运行策略,即任意时刻,系统选择上下行动,是否在某层开关门都可以自定义,只要保证在电梯系统运行时间不超过题目要求时间上限的前提下将所有的乘客送至目的地即可。

一、架构设计

1、同步块的设置和锁的选择

同步块(synchronized block)是Java中用于实现线程同步的一种结构,它允许程序员精确控制哪些代码片段需要进行同步,以确保在多线程环境下对共享资源的访问是互斥的,从而避免数据不一致性和竞态条件等并发问题。同步块的基本语法如下:

synchronized (lockObject) {
    // 这里是被保护的临界区代码
}

在设置同步块时,有以下几个关键点需要注意:
(1)锁定对象的选择:
lockObject 是要作为同步监视器的对象引用。当一个线程进入同步块时,它首先尝试获取对该对象的锁。只有成功获取锁的线程才能执行同步块中的代码,其他试图进入同步块的线程将被阻塞,直到持有锁的线程退出同步块并释放锁。
(2)避免死锁:
在设计同步块时,要警惕可能导致死锁的情况。当多个线程各自持有锁并等待对方释放锁时,就会形成死锁。确保锁的获取顺序一致,避免循环依赖,或者使用超时机制可以有助于预防死锁。
(3)锁的类型:
Java标准库提供的内置锁(即上述语法中的synchronized关键字所使用的锁)是基于对象的监视器锁。除此之外,Java还提供了更高级的锁机制,如java.util.concurrent.locks包中的ReentrantLock、Semaphore、ReadWriteLock等。这些锁提供了更多的功能,如公平锁、非阻塞尝试获取锁、可中断锁等待等。根据具体需求,可以选择使用这些高级锁替代或补充同步块。
(4)锁的重入性:
Java的内置锁(包括同步块和同步方法使用的锁)都是可重入的。这意味着同一个线程在已经持有某个锁的情况下,再次请求该锁时仍能成功获取,不会造成自我阻塞。这允许递归调用和复杂的多层同步结构。

2、调度策略

2.1、分配策略

本单元第二次作业起,不再由输入指定分配给乘客的电梯,即需要我们自己设计分配策略。我采用的策略比较简单:输入一个指令,寻找六部电梯中总人数(电梯内外人数)最少的一部电梯,将此指令分配到对应电梯的缓冲队列。注意,缓冲队列是为了规避reset指令可能会带来的bug(后文会提到)。在缓冲队列里的指令还不被视为被电梯真正接受到。

2.2、电梯运行策略

本单元我的电梯策略是LOOK策略:当电梯运行时,如果遇到同方向的请求就捎带上,当电梯内没有乘客并且电梯前方没有等待的请求时就换方向。当到达某一楼层时,如果电梯未满并且请求与电梯运行方向一致时,就开门捎带上请求。

public int getFirstDesFloor() {
        int toFloor = inEleFirstDesFloor();
        //toFloor为0,则电梯内无人
        if (toFloor == 0) {
            toFloor = outEleFirstFloor();
        }
        if (toFloor == 0) {
            direction *= -1;
            toFloor = outEleFirstFloor();
        }
        //判断是否捎带
        if (toFloor < floor) {
            int thisDir = -1;
            if (haveEnterEle(thisDir) && personInEle() < capacity) {
                return floor;
            }
        } else if (toFloor > floor) {
            int thisDir = 1;
            if (haveEnterEle(thisDir) && personInEle() < capacity) {
                return floor;
            }
        }
        return toFloor;
    }

3、电梯重置

有两种重置请求:
(1)第一类重置请求仅修改电梯参数,重置参数请求包含需要重置的电梯ID和电梯相关参数(满载人数、移动时间)。程序需要在重置完成后让电梯以新的参数运行,重置完成后,电梯处于原楼层。
(2)第二类重置请求将电梯修改为双轿厢电梯,重置参数包含需要重置的电梯ID,换乘楼层,两个轿厢的相关参数(移动一层的时间和满载人数)相同。当重置完成后,轿厢 A 默认初始在换乘楼层的下面一层,轿厢B默认初始换乘楼层的上面一层。
第一类重置请求较为简单,仅需额外考虑中途下车的乘客,我的解决办法是新建一个类MidLvRequest继承Request,逻辑上MidLvRequest和PersonRequest是并列关系。
第二类重置请求较为复杂,涉及到换乘问题。我们规定,双轿厢的两个轿厢是不能碰撞的,即不能同时出现在换乘楼层。我的解决办法是设计一个信号(换乘楼层是否被占),为真则将要到达换乘楼层的电梯暂时停止,为假则正常运行。

if (floor + 1 != transFloor) {
                sleep((int) (speed * 1000));
                floor++;
                TimableOutput.println(String.format("ARRIVE-%d-%s", floor, idStr));
                eleQueue.setTransIfEmpty(true);
            } else {
                while (!eleQueue.transIsEmpty()) {
                    sleep(600);
                }
                eleQueue.setTransIfEmpty(false);
                sleep((int) (speed * 1000));
                floor++;
                TimableOutput.println(String.format("ARRIVE-%d-%s", floor, idStr));
            }
} else if (direction == -1 && floor > firstDesFloor) {
            if (floor - 1 != transFloor) {
                sleep((int) (speed * 1000));
                floor--;
                TimableOutput.println(String.format("ARRIVE-%d-%s", floor, idStr));
                eleQueue.setTransIfEmpty(true);
            } else {
                while (!eleQueue.transIsEmpty()) {
                    sleep(600);
                }
                eleQueue.setTransIfEmpty(false);
                sleep((int) (speed * 1000));
                floor--;
                TimableOutput.println(String.format("ARRIVE-%d-%s", floor, idStr));
            }
}

二、UML图分析

1、类的详细信息

在这里插入图片描述

2、类复杂度

在这里插入图片描述

3、方法复杂度

在这里插入图片描述

三、Bug分析

本单元作业中我有两个印象比较深刻的bug。

1、同时多个reset

例如,在10s左右时,六个电梯中有五个处于reset转态,此时突然来了大量请求,在我最初的设计中会把这些请求全部分配给未处于重置状态的那一个电梯,显然时间上会长很多,甚至出现RTLE的错误。更有甚者,如果六个电梯同时处于reset状态,还会出现无法分配的情况。
为了解决这个问题,我引入了缓冲队列。即不管电梯是否处于reset状态,指令都会根据分配策略进入到电梯的缓冲队列,虽然没有直接被电梯receive,但只要电梯完成reset,指令即被分配。

2、双轿厢电梯线程结束条件

这是本单元最后一次作业的要求,我在开始实现的时候没有考虑太多,仅仅简单地沿袭了上两次作业的结束条件设置,果不其然出现了bug。
在之前的作业中,我的设计是只要输入指令为空(null),所有电梯队列都会置end,直到电梯内外和缓存队列里没人了,线程就结束了。
然而,在有了双轿厢电梯之后,一切就不一样了。例如,当输入为空时,如果只有一部电梯有人且正在运行,那么所有其他电梯就会结束运行。但是如果涉及到换乘,因为其他所有电梯已经结束了,指令就无法被执行。
我的策略是在结束条件中增加一个双轿厢电梯的结束判断:

if (personInEle() == 0 && personOutEle() == 0 && eleQueue.isEmpty()) {
                    eleQueue.setDouEnd(type, true);
                    if (type.equals("A") && eleQueue.isEnd() && eleQueue.isDou2End()) {
                        break;
                    } else if (type.equals("B") && eleQueue.isEnd() && eleQueue.isDou1End()) {
                        break;
                    } else {
                        try {
                            eleQueue.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
}

四、心得体会

学习多线程编程是一个逐步掌握并发编程原理、语言特性和实战技巧的过程。在这个过程中,我经历了以下几个步骤:
第一步:理解基本概念
(1)进程与线程:学习进程的概念,它是操作系统分配资源的基本单位,了解进程的生命周期、状态转换及管理机制。掌握线程的概念,它是进程中执行的独立逻辑流,理解其相对于进程的优势(如轻量级、资源共享、高效利用CPU)和挑战(如数据同步、竞态条件)。
(2)并发与并行:区分并发(concurrency)和并行(parallelism)的概念:并发是指多个任务在同一时间段内看似同时进行(逻辑上的同时性),而并行则是指这些任务真正同时在多个处理器核心或物理机器上执行。
(3)同步与互斥:学习同步(synchronization)原理,确保多个线程按照特定顺序或条件访问共享资源,避免数据不一致或死锁等问题。理解互斥(mutual exclusion)机制,如使用锁(mutexes)、信号量(semaphores)等工具来控制对共享资源的访问。

第二步:熟悉Java的相关知识
学习Java的Thread类和Runnable接口,用于创建线程。掌握synchronized关键字、java.util.concurrent包中的高级同步工具(如ReentrantLock、Semaphore、CountDownLatch等)以及线程池(ExecutorService)。

第三步:理论与实践结合
理论学习:阅读相关书籍或在线教程,如《Java并发编程实战》、《C++ Concurrency in Action》等,深入理解并发编程原理。学习经典多线程问题,如生产者-消费者、读者-写者、哲学家就餐等,并分析其解决方案。
动手实践:解决课程作业。

虽然过程并不轻松,但经过这个单元的学习,我确实对多线程有了更深刻的体会。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值